Files
nlcc-itinerary/pages/login.vue
Joshua Ryder 8c66550987 feat: Add comprehensive terms & conditions with required acceptance
- Create responsive TermsModal component for mobile and desktop
- Add comprehensive legal terms with prominent AI disclaimer
- Update footer to show terms modal and AI warning on all pages
- Require terms acceptance checkbox during account registration
- Add validation to ensure users accept terms before registration
- Include clickable link in checkbox to view full terms in modal

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 13:14:36 -05:00

351 lines
12 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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">{{ isRegistering ? 'Create Account' : 'Log In' }}</h2>
<p class="mt-2 text-sm text-gray-600">{{ isRegistering ? 'Create a new account' : 'Sign in to your account' }}</p>
</div>
<div class="bg-white py-8 px-6 shadow-lg rounded-lg">
<form @submit.prevent="isRegistering ? handleRegister() : handleLogin()" class="space-y-6">
<!-- First Name (only show when registering) -->
<div v-if="isRegistering">
<label for="firstName" class="block text-sm font-medium text-gray-700">
First Name
</label>
<input
id="firstName"
v-model="firstName"
type="text"
required
autocomplete="given-name"
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>
<!-- Last Name (only show when registering) -->
<div v-if="isRegistering">
<label for="lastName" class="block text-sm font-medium text-gray-700">
Last Name
</label>
<input
id="lastName"
v-model="lastName"
type="text"
required
autocomplete="family-name"
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>
<!-- Email (only show when registering) -->
<div v-if="isRegistering">
<label for="email" class="block text-sm font-medium text-gray-700">
Email
</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>
<label for="username" class="block text-sm font-medium text-gray-700">
Username
</label>
<input
id="username"
v-model="username"
type="text"
required
autocapitalize="none"
autocomplete="username"
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>
<label for="password" class="block text-sm font-medium text-gray-700">
Password
</label>
<input
id="password"
v-model="password"
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 (only show when registering) -->
<div v-if="isRegistering && password.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>
<!-- Confirm Password (only show when registering) -->
<div v-if="isRegistering">
<label for="confirmPassword" class="block text-sm font-medium text-gray-700">
Confirm Password
</label>
<input
id="confirmPassword"
v-model="confirmPassword"
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="confirmPassword.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>
<!-- Terms Acceptance (only show when registering) -->
<div v-if="isRegistering" class="flex items-start">
<div class="flex items-center h-5">
<input
id="termsAccepted"
v-model="termsAccepted"
type="checkbox"
required
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
</div>
<div class="ml-3 text-sm">
<label for="termsAccepted" class="font-medium text-gray-700">
I agree to the
<button
type="button"
@click="showTermsModal = true"
class="text-blue-600 hover:text-blue-700 underline"
>
Terms of Use & Privacy Policy
</button>
</label>
<p class="text-xs text-gray-500 mt-1">
This system was created with AI assistance and may contain errors. Please read the terms carefully.
</p>
</div>
</div>
<div v-if="error" class="text-red-600 text-sm">
{{ error }}
</div>
<button
type="submit"
:disabled="loading || (isRegistering && !termsAccepted)"
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 disabled:cursor-not-allowed"
>
{{ loading ? (isRegistering ? 'Creating account...' : 'Signing in...') : (isRegistering ? 'Create Account' : 'Sign In') }}
</button>
</form>
<div class="mt-6 text-center space-y-3">
<button
@click="toggleMode"
class="text-sm text-blue-600 hover:text-blue-700 font-medium"
>
{{ isRegistering ? 'Already have an account? Sign in' : "Don't have an account? Create one" }}
</button>
<div v-if="!isRegistering">
<NuxtLink to="/forgot-password" class="text-sm text-blue-600 hover:text-blue-700">
Forgot Password?
</NuxtLink>
</div>
<div>
<NuxtLink to="/" class="text-sm text-gray-600 hover:text-gray-700">
Back to Sermons
</NuxtLink>
</div>
</div>
</div>
</div>
</div>
<Footer />
<TermsModal v-model="showTermsModal" />
</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 route = useRoute()
const router = useRouter()
const username = ref('')
const password = ref('')
const confirmPassword = ref('')
const email = ref('')
const firstName = ref('')
const lastName = ref('')
const error = ref('')
const loading = ref(false)
const termsAccepted = ref(false)
const showTermsModal = ref(false)
// Initialize from URL query parameter
const isRegistering = ref(route.query.mode === 'register')
const passwordRequirements = computed(() => ({
minLength: password.value.length >= 8,
hasUppercase: /[A-Z]/.test(password.value),
hasLowercase: /[a-z]/.test(password.value),
hasNumberOrSymbol: /[0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password.value)
}))
const isPasswordValid = computed(() => {
return passwordRequirements.value.minLength &&
passwordRequirements.value.hasUppercase &&
passwordRequirements.value.hasLowercase &&
passwordRequirements.value.hasNumberOrSymbol
})
const passwordsMatch = computed(() => {
return password.value === confirmPassword.value
})
function toggleMode() {
isRegistering.value = !isRegistering.value
error.value = ''
username.value = ''
password.value = ''
confirmPassword.value = ''
email.value = ''
firstName.value = ''
lastName.value = ''
termsAccepted.value = false
// Update URL to reflect mode
const query = { ...route.query }
if (isRegistering.value) {
query.mode = 'register'
} else {
delete query.mode
}
router.replace({ query })
}
async function handleLogin() {
error.value = ''
loading.value = true
try {
const response = await $fetch('/api/auth/login', {
method: 'POST',
body: {
username: username.value,
password: password.value
}
})
if (response.success) {
// Check if there's a redirect parameter
const redirect = route.query.redirect as string
await navigateTo(redirect || '/')
}
} catch (e: any) {
error.value = e.data?.message || 'Invalid credentials'
} finally {
loading.value = false
}
}
async function handleRegister() {
error.value = ''
// Validate password requirements
if (!isPasswordValid.value) {
error.value = 'Please meet all password requirements'
return
}
// Validate passwords match
if (!passwordsMatch.value) {
error.value = 'Passwords do not match'
return
}
// Validate terms acceptance
if (!termsAccepted.value) {
error.value = 'You must agree to the Terms of Use & Privacy Policy'
return
}
loading.value = true
try {
const response = await $fetch('/api/auth/register', {
method: 'POST',
body: {
username: username.value,
password: password.value,
email: email.value,
firstName: firstName.value,
lastName: lastName.value
}
})
if (response.success) {
// Check if there's a redirect parameter
const redirect = route.query.redirect as string
await navigateTo(redirect || '/')
}
} catch (e: any) {
error.value = e.data?.message || 'Registration failed'
} finally {
loading.value = false
}
}
</script>