Files
chore/frontend/vue-app/src/components/shared/LoginButton.vue
Ryan Kegel 3066d7d356
All checks were successful
Gitea Actions Demo / build-and-push (push) Successful in 23s
feat: add parent PIN setup functionality and email notifications
- Implemented User model updates to include PIN and related fields.
- Created email sender utility for sending verification and reset emails.
- Developed ParentPinSetup component for setting up a parent PIN with verification code.
- Enhanced UserProfile and EntityEditForm components to support new features.
- Updated routing to include PIN setup and authentication checks.
- Added styles for new components and improved existing styles for consistency.
- Introduced loading states and error handling in various components.
2026-01-27 14:47:49 -05:00

218 lines
5.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.
<script setup lang="ts">
import { ref, nextTick, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { eventBus } from '@/common/eventBus'
import { authenticateParent, isParentAuthenticated, logoutParent } from '../../stores/auth'
import '@/assets/styles.css'
import '@/assets/colors.css'
import ModalDialog from './ModalDialog.vue'
const router = useRouter()
const show = ref(false)
const pin = ref('')
const error = ref('')
const pinInput = ref<HTMLInputElement | null>(null)
const dropdownOpen = ref(false)
const open = async () => {
// Check if user has a pin
try {
const res = await fetch('/api/user/has-pin', { credentials: 'include' })
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Error checking PIN')
if (!data.has_pin) {
console.log('No PIN set, redirecting to setup')
// Route to PIN setup view
router.push('/parent/pin-setup')
return
}
} catch (e) {
error.value = 'Network error'
return
}
pin.value = ''
error.value = ''
show.value = true
await nextTick()
pinInput.value?.focus()
}
const close = () => {
show.value = false
error.value = ''
}
const submit = async () => {
const isDigits = /^\d{4,6}$/.test(pin.value)
if (!isDigits) {
error.value = 'Enter 46 digits'
return
}
try {
const res = await fetch('/api/user/check-pin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pin: pin.value }),
credentials: 'include',
})
const data = await res.json()
if (!res.ok) {
error.value = data.error || 'Error validating PIN'
return
}
if (!data.valid) {
error.value = 'Incorrect PIN'
return
}
// Authenticate parent and navigate
authenticateParent()
close()
router.push('/parent')
} catch (e) {
error.value = 'Network error'
}
}
const handleLogout = () => {
logoutParent()
router.push('/child')
}
function toggleDropdown() {
dropdownOpen.value = !dropdownOpen.value
}
async function signOut() {
try {
await fetch('/api/logout', { method: 'POST' })
logoutParent()
router.push('/auth')
} catch {
// Optionally show error
}
dropdownOpen.value = false
}
function goToProfile() {
router.push('/parent/profile')
}
onMounted(() => {
eventBus.on('open-login', open)
})
onUnmounted(() => {
eventBus.off('open-login', open)
})
</script>
<template>
<div style="position: relative">
<button v-if="!isParentAuthenticated" @click="open" aria-label="Parent login" class="login-btn">
Parent
</button>
<div v-else style="display: inline-block; position: relative">
<button
@click="toggleDropdown"
aria-label="Parent menu"
class="login-btn"
style="min-width: 80px"
>
Parent
</button>
<div
v-if="dropdownOpen"
class="dropdown-menu"
style="
position: absolute;
right: 0;
top: 100%;
background: #fff;
border: 1px solid #e6e6e6;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
min-width: 120px;
z-index: 10;
"
>
<button class="menu-item" @click="goToProfile" style="width: 100%; text-align: left">
Profile
</button>
<button class="menu-item" @click="handleLogout" style="width: 100%; text-align: left">
Log out
</button>
<button class="menu-item danger" @click="signOut" style="width: 100%; text-align: left">
Sign out
</button>
</div>
</div>
<ModalDialog v-if="show" title="Enter parent PIN" @click.self="close" @close="close">
<form @submit.prevent="submit">
<input
ref="pinInput"
v-model="pin"
inputmode="numeric"
pattern="\d*"
maxlength="6"
placeholder="46 digits"
class="pin-input"
/>
<div class="actions modal-actions">
<button type="button" class="btn btn-secondary" @click="close">Cancel</button>
<button type="submit" class="btn btn-primary">OK</button>
</div>
</form>
<div v-if="error" class="error modal-message">{{ error }}</div>
</ModalDialog>
</div>
</template>
<style>
/* modal */
.pin-input {
width: 100%;
padding: 0.5rem 0.6rem;
font-size: 1rem;
border-radius: 8px;
border: 1px solid #e6e6e6;
margin-bottom: 0.6rem;
box-sizing: border-box;
text-align: center;
}
.dropdown-menu {
padding: 0.5rem 0;
}
.menu-item {
padding: 1rem 0.9rem;
background: transparent;
border: 0;
text-align: left;
cursor: pointer;
font-weight: 600;
color: var(--menu-item-color, #333);
font-size: 0.9rem;
}
.menu-item + .menu-item {
margin-top: 0.5rem;
}
.menu-item:hover {
background: var(--menu-item-hover-bg, rgba(0, 0, 0, 0.04));
}
.menu-item.danger {
color: var(--menu-item-danger, #ff4d4f);
}
@media (max-width: 600px) {
.menu-item {
padding: 0.85rem 0.7rem;
font-size: 1rem;
}
.menu-item + .menu-item {
margin-top: 0.35rem;
}
}
</style>