Files
chore/frontend/vue-app/src/components/auth/ParentPinSetup.vue
Ryan Kegel 31ea76f013
All checks were successful
Chore App Build and Push Docker Images / build-and-push (push) Successful in 1m4s
feat: enhance child edit and view components with improved form handling and validation
- Added `requireDirty` prop to `EntityEditForm` for dirty state management.
- Updated `ChildEditView` to handle initial data loading and image selection more robustly.
- Refactored `ChildView` to remove unused reward dialog logic and prevent API calls in child mode.
- Improved type definitions for form fields and initial data in `ChildEditView`.
- Enhanced error handling in form submissions across components.
- Implemented cross-tab logout synchronization on password reset in the auth store.
- Added tests for login and entity edit form functionalities to ensure proper behavior.
- Introduced global fetch interceptor for handling unauthorized responses.
- Documented password reset flow and its implications on session management.
2026-02-17 17:18:03 -05:00

263 lines
7.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="pin-setup-view">
<div v-if="step === 1">
<h2>Set up your Parent PIN</h2>
<p>
To protect your account, you need to set a Parent PIN. This PIN is required to access parent
features.
</p>
<button class="btn btn-primary" @click="requestCode" :disabled="loading">
{{ loading ? 'Sending...' : 'Send Verification Code' }}
</button>
<div v-if="error" class="error-message">{{ error }}</div>
<div v-if="info" class="info-message">{{ info }}</div>
</div>
<div v-else-if="step === 2">
<h2>Enter Verification Code</h2>
<p>
We've sent a 6-digit code to your email. Enter it below to continue. The code is valid for
10 minutes.
</p>
<input v-model="code" maxlength="6" class="code-input" placeholder="6-digit code" />
<div class="button-group">
<button
v-if="!loading"
class="btn btn-primary"
@click="verifyCode"
:disabled="!isCodeValid"
>
Verify Code
</button>
<button class="btn btn-link" @click="resendCode" v-if="showResend" :disabled="loading">
Resend Code
</button>
<div v-if="error" class="error-message">{{ error }}</div>
</div>
</div>
<div v-else-if="step === 3">
<h2>Create Parent PIN</h2>
<p>Enter a new 46 digit Parent PIN. This will be required for parent access.</p>
<input
v-model="pin"
@input="handlePinInput"
maxlength="6"
inputmode="numeric"
pattern="\d*"
class="pin-input"
placeholder="New PIN"
/>
<input
v-model="pin2"
@input="handlePin2Input"
maxlength="6"
inputmode="numeric"
pattern="\d*"
class="pin-input"
placeholder="Confirm PIN"
/>
<button class="btn btn-primary" @click="setPin" :disabled="loading || !isPinValid">
{{ loading ? 'Saving...' : 'Set PIN' }}
</button>
<div v-if="error" class="error-message">{{ error }}</div>
</div>
<div v-else-if="step === 4">
<h2>Parent PIN Set!</h2>
<p>Your Parent PIN has been set. You can now use it to access parent features.</p>
<button class="btn btn-primary" @click="goBack">Back</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { logoutParent } from '@/stores/auth'
import '@/assets/styles.css'
const step = ref(1)
const loading = ref(false)
const error = ref('')
const info = ref('')
const code = ref('')
const pin = ref('')
const pin2 = ref('')
const showResend = ref(false)
let resendTimeout: ReturnType<typeof setTimeout> | null = null
const router = useRouter()
const isCodeValid = computed(() => code.value.length === 6)
const isPinValid = computed(() => {
const p1 = pin.value
const p2 = pin2.value
return /^\d{4,6}$/.test(p1) && /^\d{4,6}$/.test(p2) && p1 === p2
})
function handlePinInput(event: Event) {
const target = event.target as HTMLInputElement
pin.value = target.value.replace(/\D/g, '').slice(0, 6)
}
function handlePin2Input(event: Event) {
const target = event.target as HTMLInputElement
pin2.value = target.value.replace(/\D/g, '').slice(0, 6)
}
async function requestCode() {
error.value = ''
info.value = ''
loading.value = true
try {
const res = await fetch('/api/user/request-pin-setup', {
method: 'POST',
credentials: 'include',
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Failed to send code')
info.value = 'A verification code has been sent to your email.'
step.value = 2
code.value = ''
showResend.value = false
if (resendTimeout) clearTimeout(resendTimeout)
resendTimeout = setTimeout(() => {
showResend.value = true
}, 10000)
} catch (e: any) {
error.value = e.message || 'Failed to send code.'
} finally {
loading.value = false
}
}
async function resendCode() {
error.value = ''
info.value = ''
loading.value = true
try {
const res = await fetch('/api/user/request-pin-setup', {
method: 'POST',
credentials: 'include',
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Failed to resend code')
info.value = 'A new verification code has been sent to your email.'
// Stay on code input step
step.value = 2
code.value = ''
showResend.value = false
if (resendTimeout) clearTimeout(resendTimeout)
resendTimeout = setTimeout(() => {
showResend.value = true
}, 10000)
} catch (e: any) {
error.value = e.message || 'Failed to resend code.'
} finally {
loading.value = false
}
}
// When entering step 2, start the resend timer
watch(step, (newStep) => {
if (newStep === 2) {
showResend.value = false
if (resendTimeout) clearTimeout(resendTimeout)
resendTimeout = setTimeout(() => {
showResend.value = true
}, 10000)
}
})
onMounted(() => {
if (step.value === 2) {
showResend.value = false
if (resendTimeout) clearTimeout(resendTimeout)
resendTimeout = setTimeout(() => {
showResend.value = true
}, 10000)
}
})
async function verifyCode() {
error.value = ''
loading.value = true
try {
const res = await fetch('/api/user/verify-pin-setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: code.value }),
credentials: 'include',
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Invalid code')
step.value = 3
} catch (e: any) {
error.value = e.message || 'Invalid code.'
} finally {
loading.value = false
}
}
async function setPin() {
error.value = ''
if (!/^\d{4,6}$/.test(pin.value)) {
error.value = 'PIN must be 46 digits.'
return
}
if (pin.value !== pin2.value) {
error.value = 'PINs do not match.'
return
}
loading.value = true
try {
const res = await fetch('/api/user/set-pin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pin: pin.value }),
credentials: 'include',
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Failed to set PIN')
step.value = 4
} catch (e: any) {
error.value = e.message || 'Failed to set PIN.'
} finally {
loading.value = false
}
}
function goBack() {
logoutParent()
router.push('/child')
}
</script>
<style scoped>
.pin-setup-view {
max-width: 400px;
margin: 2.5rem auto;
background: var(--form-bg, #fff);
border-radius: 12px;
box-shadow: 0 4px 24px var(--form-shadow, #e6e6e6);
padding: 2.2rem 2.2rem 1.5rem 2.2rem;
text-align: center;
}
.pin-input,
.code-input {
width: 100%;
padding: 0.7rem;
border-radius: 7px;
border: 1px solid var(--form-input-border, #e6e6e6);
font-size: 1.1rem;
margin-bottom: 1rem;
box-sizing: border-box;
text-align: center;
}
.button-group {
display: flex;
gap: 1rem;
margin-top: 1rem;
flex-direction: column;
align-items: center;
}
</style>