Files
nlcc-itinerary/pages/forgot-password.vue
Joshua Ryder 3f1c573a67 feat: Make menu context-aware and hide current page links
Implement intelligent menu filtering that hides self-referencing links and shows all applicable pages based on user role and authentication state.

Changes:
- Menu component now uses currentPath to filter out the current page link
- Added computed properties to detect which page user is on
- Fixed profile, admin, and users pages to dynamically detect admin status from API
- Added menu to forgot-password page for consistent navigation
- All pages now pass correct authentication state to Menu component

This ensures menus always show relevant navigation options while avoiding redundant links to the current page. Admin users now see all admin options (Manage Sermons, Manage Users) regardless of which page they're on, except the current one.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 09:40:42 -05:00

329 lines
11 KiB
Vue

<template>
<div class="min-h-screen bg-gray-50 flex flex-col">
<!-- Header -->
<header class="bg-white shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div class="flex items-center justify-between">
<NuxtLink to="/">
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto" />
</NuxtLink>
<ClientOnly fallback-tag="div">
<Menu
:is-authenticated="isAuthenticated"
:is-admin="isAdmin"
:show-home="true"
/>
</ClientOnly>
</div>
</div>
</header>
<div class="flex-1 flex items-center justify-center px-4">
<div class="max-w-md w-full">
<div class="text-center mb-8">
<h2 class="text-3xl font-bold text-gray-900">Reset Password</h2>
<p class="mt-2 text-sm text-gray-600">{{ stepMessage }}</p>
</div>
<div class="bg-white py-8 px-6 shadow-lg rounded-lg">
<!-- Step 1: Enter Email -->
<form v-if="step === 1" @submit.prevent="sendResetCode" class="space-y-6">
<div>
<label for="email" class="block text-sm font-medium text-gray-700">
Email Address
</label>
<input
id="email"
v-model="email"
type="email"
required
autocomplete="email"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div v-if="error" class="text-red-600 text-sm">
{{ error }}
</div>
<div v-if="success" class="text-green-600 text-sm">
{{ success }}
</div>
<button
type="submit"
:disabled="loading"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{{ loading ? 'Sending...' : 'Send Reset Code' }}
</button>
</form>
<!-- Step 2: Enter Code -->
<form v-if="step === 2" @submit.prevent="verifyCode" class="space-y-6">
<div>
<label for="code" class="block text-sm font-medium text-gray-700">
Reset Code
</label>
<input
id="code"
v-model="code"
type="text"
required
maxlength="8"
pattern="[0-9A-Za-z]{8}"
placeholder="ABC123XY"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-center text-2xl tracking-wider uppercase"
style="text-transform: uppercase"
/>
<p class="mt-2 text-xs text-gray-500">Enter the 8-character code sent to {{ email }}</p>
</div>
<div v-if="error" class="text-red-600 text-sm">
{{ error }}
</div>
<button
type="submit"
:disabled="loading || code.length !== 8"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{{ loading ? 'Verifying...' : 'Verify Code' }}
</button>
</form>
<!-- Step 3: Reset Password -->
<form v-if="step === 3" @submit.prevent="resetPassword" class="space-y-6">
<div>
<label for="newPassword" class="block text-sm font-medium text-gray-700">
New Password
</label>
<input
id="newPassword"
v-model="newPassword"
type="password"
required
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
/>
<!-- Password Requirements -->
<div v-if="newPassword.length > 0" class="mt-2 space-y-1 text-xs">
<div :class="passwordRequirements.minLength ? 'text-green-600' : 'text-gray-500'">
<span v-if="passwordRequirements.minLength"></span>
<span v-else></span>
At least 8 characters
</div>
<div :class="passwordRequirements.hasUppercase ? 'text-green-600' : 'text-gray-500'">
<span v-if="passwordRequirements.hasUppercase"></span>
<span v-else></span>
One uppercase letter
</div>
<div :class="passwordRequirements.hasLowercase ? 'text-green-600' : 'text-gray-500'">
<span v-if="passwordRequirements.hasLowercase"></span>
<span v-else></span>
One lowercase letter
</div>
<div :class="passwordRequirements.hasNumberOrSymbol ? 'text-green-600' : 'text-gray-500'">
<span v-if="passwordRequirements.hasNumberOrSymbol"></span>
<span v-else></span>
One number or symbol
</div>
</div>
</div>
<div>
<label for="confirmNewPassword" class="block text-sm font-medium text-gray-700">
Confirm New Password
</label>
<input
id="confirmNewPassword"
v-model="confirmNewPassword"
type="password"
required
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
/>
<!-- Password Match Indicator -->
<div v-if="confirmNewPassword.length > 0" class="mt-2 text-xs">
<div :class="passwordsMatch ? 'text-green-600' : 'text-red-600'">
<span v-if="passwordsMatch"> Passwords match</span>
<span v-else> Passwords do not match</span>
</div>
</div>
</div>
<div v-if="error" class="text-red-600 text-sm">
{{ error }}
</div>
<button
type="submit"
:disabled="loading || !isPasswordValid || !passwordsMatch"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{{ loading ? 'Resetting...' : 'Reset Password' }}
</button>
</form>
<div class="mt-6 text-center space-y-3">
<button
v-if="step > 1"
@click="goBack"
class="text-sm text-gray-600 hover:text-gray-700"
>
Go Back
</button>
<div>
<NuxtLink to="/login" class="text-sm text-blue-600 hover:text-blue-700">
Back to Login
</NuxtLink>
</div>
</div>
</div>
</div>
</div>
<Footer />
</div>
</template>
<script setup lang="ts">
// Check authentication status
const { data: authData } = await useFetch('/api/auth/verify')
const isAuthenticated = computed(() => authData.value?.authenticated || false)
const isAdmin = computed(() => authData.value?.isAdmin || false)
const email = ref('')
const code = ref('')
const newPassword = ref('')
const confirmNewPassword = ref('')
const error = ref('')
const success = ref('')
const loading = ref(false)
const step = ref(1)
const stepMessage = computed(() => {
switch (step.value) {
case 1:
return 'Enter your email to receive a reset code'
case 2:
return 'Enter the code sent to your email'
case 3:
return 'Create a new password'
default:
return ''
}
})
const passwordRequirements = computed(() => ({
minLength: newPassword.value.length >= 8,
hasUppercase: /[A-Z]/.test(newPassword.value),
hasLowercase: /[a-z]/.test(newPassword.value),
hasNumberOrSymbol: /[0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(newPassword.value)
}))
const isPasswordValid = computed(() => {
return passwordRequirements.value.minLength &&
passwordRequirements.value.hasUppercase &&
passwordRequirements.value.hasLowercase &&
passwordRequirements.value.hasNumberOrSymbol
})
const passwordsMatch = computed(() => {
return newPassword.value === confirmNewPassword.value
})
function goBack() {
if (step.value > 1) {
step.value--
error.value = ''
success.value = ''
}
}
async function sendResetCode() {
error.value = ''
success.value = ''
loading.value = true
try {
const response = await $fetch('/api/auth/forgot-password', {
method: 'POST',
body: {
email: email.value
}
})
if (response.success) {
success.value = response.message
setTimeout(() => {
step.value = 2
success.value = ''
}, 2000)
}
} catch (e: any) {
error.value = e.data?.message || 'Failed to send reset code'
} finally {
loading.value = false
}
}
async function verifyCode() {
error.value = ''
loading.value = true
try {
const response = await $fetch('/api/auth/verify-reset-code', {
method: 'POST',
body: {
email: email.value,
code: code.value
}
})
if (response.success) {
step.value = 3
}
} catch (e: any) {
error.value = e.data?.message || 'Invalid or expired code'
} finally {
loading.value = false
}
}
async function resetPassword() {
error.value = ''
if (!isPasswordValid.value) {
error.value = 'Please meet all password requirements'
return
}
if (!passwordsMatch.value) {
error.value = 'Passwords do not match'
return
}
loading.value = true
try {
const response = await $fetch('/api/auth/reset-password', {
method: 'POST',
body: {
email: email.value,
code: code.value,
newPassword: newPassword.value
}
})
if (response.success) {
await navigateTo('/login')
}
} catch (e: any) {
error.value = e.data?.message || 'Failed to reset password'
} finally {
loading.value = false
}
}
</script>