Files
chore/frontend/vue-app/src/components/auth/ParentPinSetup.vue
Ryan Kegel 725bf518ea
All checks were successful
Chore App Build and Push Docker Images / build-and-push (push) Successful in 1m23s
Refactor and enhance various components and tests
- Remove OverrideEditModal.spec.ts test file.
- Update ParentPinSetup.vue to handle Enter key for code and PIN inputs.
- Modify ChildEditView.vue to add maxlength for age input.
- Enhance ChildView.vue with reward confirmation and cancellation dialogs.
- Update ParentView.vue to handle pending rewards and confirm edits.
- Revise PendingRewardDialog.vue to accept a dynamic message prop.
- Expand ChildView.spec.ts to cover reward dialog interactions.
- Add tests for ParentView.vue to validate pending reward handling.
- Update UserProfile.vue to simplify button styles.
- Adjust RewardView.vue to improve delete confirmation handling.
- Modify ChildrenListView.vue to clarify child creation instructions.
- Refactor EntityEditForm.vue to improve input handling and focus management.
- Enhance ItemList.vue to support item selection.
- Update LoginButton.vue to focus PIN input on error.
- Change ScrollingList.vue empty state color for better visibility.
- Remove capture attribute from ImagePicker.vue file input.
- Update router/index.ts to redirect logged-in users from auth routes.
- Add authGuard.spec.ts to test router authentication logic.
2026-02-19 09:57:59 -05:00

271 lines
7.3 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"
@keyup.enter="isCodeValid && verifyCode()"
/>
<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"
@keyup.enter="!loading && isPinValid && setPin()"
maxlength="6"
inputmode="numeric"
pattern="\d*"
class="pin-input"
placeholder="New PIN"
/>
<input
v-model="pin2"
@input="handlePin2Input"
@keyup.enter="!loading && isPinValid && setPin()"
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>