Files
chore/frontend/vue-app/src/components/auth/ParentPinSetup.vue
Ryan Kegel 47541afbbf
All checks were successful
Gitea Actions Demo / build-and-push (push) Successful in 46s
Add unit tests for LoginButton component with comprehensive coverage
2026-02-05 16:37:10 -05:00

236 lines
6.4 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">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"
maxlength="6"
inputmode="numeric"
pattern="\d*"
class="pin-input"
placeholder="New PIN"
/>
<input
v-model="pin2"
maxlength="6"
inputmode="numeric"
pattern="\d*"
class="pin-input"
placeholder="Confirm PIN"
/>
<button class="btn btn-primary" @click="setPin" :disabled="loading">
{{ 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 } 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()
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>