Profile enhancements & greeting
This commit is contained in:
@@ -26,19 +26,27 @@
|
|||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
<ClientOnly fallback-tag="div">
|
<ClientOnly fallback-tag="div">
|
||||||
<div v-if="isAuthenticated && isAdmin" class="flex flex-wrap items-center justify-center gap-2">
|
<div v-if="isAuthenticated" class="flex flex-wrap items-center justify-center gap-2">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
|
v-if="isAdmin"
|
||||||
to="/admin"
|
to="/admin"
|
||||||
class="px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium text-xs whitespace-nowrap"
|
class="px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium text-xs whitespace-nowrap"
|
||||||
>
|
>
|
||||||
Manage Sermons
|
Manage Sermons
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
|
v-if="isAdmin"
|
||||||
to="/users"
|
to="/users"
|
||||||
class="px-3 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 font-medium text-xs whitespace-nowrap"
|
class="px-3 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 font-medium text-xs whitespace-nowrap"
|
||||||
>
|
>
|
||||||
Manage Users
|
Manage Users
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
<NuxtLink
|
||||||
|
to="/profile"
|
||||||
|
class="px-3 py-2 text-xs font-medium text-blue-700 bg-blue-50 rounded-md hover:bg-blue-100 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Edit Profile
|
||||||
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,6 +73,12 @@
|
|||||||
>
|
>
|
||||||
Manage Users
|
Manage Users
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
<NuxtLink
|
||||||
|
to="/profile"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-blue-700 bg-blue-50 rounded-md hover:bg-blue-100"
|
||||||
|
>
|
||||||
|
Edit Profile
|
||||||
|
</NuxtLink>
|
||||||
<button
|
<button
|
||||||
@click="handleLogout"
|
@click="handleLogout"
|
||||||
class="px-4 py-2 text-sm font-medium text-red-700 bg-red-50 rounded-md hover:bg-red-100"
|
class="px-4 py-2 text-sm font-medium text-red-700 bg-red-50 rounded-md hover:bg-red-100"
|
||||||
@@ -87,6 +101,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-center mt-6">
|
<div class="text-center mt-6">
|
||||||
|
<ClientOnly>
|
||||||
|
<h2 v-if="isAuthenticated && firstName" class="text-xl text-gray-700 mb-2">
|
||||||
|
{{ greeting }}
|
||||||
|
</h2>
|
||||||
|
</ClientOnly>
|
||||||
<h1 class="text-3xl font-bold text-gray-900">
|
<h1 class="text-3xl font-bold text-gray-900">
|
||||||
Welcome to New Life!
|
Welcome to New Life!
|
||||||
</h1>
|
</h1>
|
||||||
@@ -159,6 +178,27 @@
|
|||||||
const { data: authData } = await useFetch('/api/auth/verify')
|
const { data: authData } = await useFetch('/api/auth/verify')
|
||||||
const isAuthenticated = computed(() => authData.value?.authenticated || false)
|
const isAuthenticated = computed(() => authData.value?.authenticated || false)
|
||||||
const isAdmin = computed(() => authData.value?.isAdmin || false)
|
const isAdmin = computed(() => authData.value?.isAdmin || false)
|
||||||
|
const firstName = computed(() => authData.value?.firstName || '')
|
||||||
|
|
||||||
|
// Time-based greeting
|
||||||
|
const greeting = computed(() => {
|
||||||
|
if (!firstName.value) return ''
|
||||||
|
|
||||||
|
const hour = new Date().getHours()
|
||||||
|
let timeGreeting = ''
|
||||||
|
|
||||||
|
if (hour >= 5 && hour < 12) {
|
||||||
|
timeGreeting = 'Good morning'
|
||||||
|
} else if (hour >= 12 && hour < 18) {
|
||||||
|
timeGreeting = 'Good afternoon'
|
||||||
|
} else if (hour >= 18 && hour < 21) {
|
||||||
|
timeGreeting = 'Good evening'
|
||||||
|
} else {
|
||||||
|
timeGreeting = 'Greetings'
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${timeGreeting}, ${firstName.value}!`
|
||||||
|
})
|
||||||
|
|
||||||
// Fetch non-archived sermons for the most recent sermon
|
// Fetch non-archived sermons for the most recent sermon
|
||||||
const { data: activeSermons } = await useFetch('/api/sermons?includeArchived=false')
|
const { data: activeSermons } = await useFetch('/api/sermons?includeArchived=false')
|
||||||
|
|||||||
316
pages/profile.vue
Normal file
316
pages/profile.vue
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="bg-white shadow-sm">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<NuxtLink to="/">
|
||||||
|
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto cursor-pointer hover:opacity-80" />
|
||||||
|
</NuxtLink>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<NuxtLink
|
||||||
|
to="/"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-blue-700 bg-blue-50 rounded-md hover:bg-blue-100"
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</NuxtLink>
|
||||||
|
<button
|
||||||
|
@click="handleLogout"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-red-700 bg-red-50 rounded-md hover:bg-red-100"
|
||||||
|
>
|
||||||
|
Log Out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900">Edit Profile</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<div class="bg-white shadow-lg rounded-lg overflow-hidden">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900">Profile Information</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleSubmit" class="p-6 space-y-6">
|
||||||
|
<!-- Username (read-only) -->
|
||||||
|
<div>
|
||||||
|
<label for="username" class="block text-sm font-medium text-gray-700">
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
v-model="profile.username"
|
||||||
|
type="text"
|
||||||
|
disabled
|
||||||
|
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-100 text-gray-500 cursor-not-allowed"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-gray-500">Username cannot be changed</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- First Name -->
|
||||||
|
<div>
|
||||||
|
<label for="firstName" class="block text-sm font-medium text-gray-700">
|
||||||
|
First Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="firstName"
|
||||||
|
v-model="profile.firstName"
|
||||||
|
type="text"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Last Name -->
|
||||||
|
<div>
|
||||||
|
<label for="lastName" class="block text-sm font-medium text-gray-700">
|
||||||
|
Last Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="lastName"
|
||||||
|
v-model="profile.lastName"
|
||||||
|
type="text"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email -->
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm font-medium text-gray-700">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
v-model="profile.email"
|
||||||
|
type="email"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Change Password Section -->
|
||||||
|
<div class="pt-6 border-t border-gray-200">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-4">Change Password (Optional)</h3>
|
||||||
|
|
||||||
|
<!-- Current Password -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="currentPassword" class="block text-sm font-medium text-gray-700">
|
||||||
|
Current Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="currentPassword"
|
||||||
|
v-model="passwords.current"
|
||||||
|
type="password"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<!-- New Password -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="newPassword" class="block text-sm font-medium text-gray-700">
|
||||||
|
New Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="newPassword"
|
||||||
|
v-model="passwords.new"
|
||||||
|
type="password"
|
||||||
|
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="passwords.new.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 New Password -->
|
||||||
|
<div>
|
||||||
|
<label for="confirmPassword" class="block text-sm font-medium text-gray-700">
|
||||||
|
Confirm New Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="confirmPassword"
|
||||||
|
v-model="passwords.confirm"
|
||||||
|
type="password"
|
||||||
|
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="passwords.confirm.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>
|
||||||
|
|
||||||
|
<div v-if="error" class="text-red-600 text-sm">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="success" class="text-green-600 text-sm">
|
||||||
|
{{ success }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="loading"
|
||||||
|
class="flex-1 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{{ loading ? 'Saving...' : 'Save Changes' }}
|
||||||
|
</button>
|
||||||
|
<NuxtLink
|
||||||
|
to="/"
|
||||||
|
class="flex-1 px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400 text-center"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'auth'
|
||||||
|
})
|
||||||
|
|
||||||
|
const profile = ref({
|
||||||
|
username: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
email: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const passwords = ref({
|
||||||
|
current: '',
|
||||||
|
new: '',
|
||||||
|
confirm: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const error = ref('')
|
||||||
|
const success = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const passwordRequirements = computed(() => ({
|
||||||
|
minLength: passwords.value.new.length >= 8,
|
||||||
|
hasUppercase: /[A-Z]/.test(passwords.value.new),
|
||||||
|
hasLowercase: /[a-z]/.test(passwords.value.new),
|
||||||
|
hasNumberOrSymbol: /[0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(passwords.value.new)
|
||||||
|
}))
|
||||||
|
|
||||||
|
const isPasswordValid = computed(() => {
|
||||||
|
if (passwords.value.new.length === 0) return true // Password change is optional
|
||||||
|
return passwordRequirements.value.minLength &&
|
||||||
|
passwordRequirements.value.hasUppercase &&
|
||||||
|
passwordRequirements.value.hasLowercase &&
|
||||||
|
passwordRequirements.value.hasNumberOrSymbol
|
||||||
|
})
|
||||||
|
|
||||||
|
const passwordsMatch = computed(() => {
|
||||||
|
return passwords.value.new === passwords.value.confirm
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadProfile() {
|
||||||
|
try {
|
||||||
|
const data = await $fetch('/api/profile')
|
||||||
|
profile.value = data
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e.data?.message || 'Failed to load profile'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
error.value = ''
|
||||||
|
success.value = ''
|
||||||
|
|
||||||
|
// Validate password change if attempted
|
||||||
|
if (passwords.value.new || passwords.value.current || passwords.value.confirm) {
|
||||||
|
if (!passwords.value.current) {
|
||||||
|
error.value = 'Current password is required to change password'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!passwords.value.new) {
|
||||||
|
error.value = 'New password is required'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isPasswordValid.value) {
|
||||||
|
error.value = 'Please meet all password requirements'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!passwordsMatch.value) {
|
||||||
|
error.value = 'New passwords do not match'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $fetch('/api/profile/update', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: {
|
||||||
|
firstName: profile.value.firstName,
|
||||||
|
lastName: profile.value.lastName,
|
||||||
|
email: profile.value.email,
|
||||||
|
currentPassword: passwords.value.current || undefined,
|
||||||
|
newPassword: passwords.value.new || undefined,
|
||||||
|
confirmPassword: passwords.value.confirm || undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
success.value = 'Profile updated successfully!'
|
||||||
|
|
||||||
|
// Clear password fields
|
||||||
|
passwords.value = {
|
||||||
|
current: '',
|
||||||
|
new: '',
|
||||||
|
confirm: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload profile data
|
||||||
|
await loadProfile()
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e.data?.message || 'Failed to update profile'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
await $fetch('/api/auth/logout', { method: 'POST' })
|
||||||
|
await navigateTo('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadProfile()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -25,6 +25,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
return {
|
return {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
|
firstName: user.first_name || '',
|
||||||
isAdmin: user.is_admin === 1
|
isAdmin: user.is_admin === 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
29
server/api/profile/index.get.ts
Normal file
29
server/api/profile/index.get.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { getUserByUsername } from '~/server/utils/database'
|
||||||
|
import { getAuthUser } from '~/server/utils/auth'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const authUser = await getAuthUser(event)
|
||||||
|
|
||||||
|
if (!authUser) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Unauthorized',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = getUserByUsername(authUser.username)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: 'User not found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
username: user.username,
|
||||||
|
email: user.email || '',
|
||||||
|
firstName: user.first_name || '',
|
||||||
|
lastName: user.last_name || '',
|
||||||
|
}
|
||||||
|
})
|
||||||
112
server/api/profile/update.put.ts
Normal file
112
server/api/profile/update.put.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { getUserByUsername, getUserByEmail } from '~/server/utils/database'
|
||||||
|
import { getAuthUser } from '~/server/utils/auth'
|
||||||
|
import bcrypt from 'bcrypt'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const authUser = await getAuthUser(event)
|
||||||
|
|
||||||
|
if (!authUser) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Unauthorized',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readBody(event)
|
||||||
|
const { firstName, lastName, email, currentPassword, newPassword, confirmPassword } = body
|
||||||
|
|
||||||
|
if (!firstName || !lastName || !email) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'First name, last name, and email are required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate email format
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
|
if (!emailRegex.test(email)) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Invalid email format',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = (await import('~/server/utils/database')).getDatabase()
|
||||||
|
|
||||||
|
// Get current user data
|
||||||
|
const currentUser = getUserByUsername(authUser.username)
|
||||||
|
if (!currentUser) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: 'User not found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if email is already taken by another user
|
||||||
|
if (email.toLowerCase() !== currentUser.email?.toLowerCase()) {
|
||||||
|
const existingEmail = getUserByEmail(email)
|
||||||
|
if (existingEmail && existingEmail.id !== currentUser.id) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 409,
|
||||||
|
message: 'Email already exists',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If changing password, validate it
|
||||||
|
if (newPassword) {
|
||||||
|
if (!currentPassword) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Current password is required to change password',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify current password
|
||||||
|
const isValidPassword = bcrypt.compareSync(currentPassword, currentUser.password)
|
||||||
|
if (!isValidPassword) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Current password is incorrect',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'New passwords do not match',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate new password requirements
|
||||||
|
if (newPassword.length < 8) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Password must be at least 8 characters long',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasUpperCase = /[A-Z]/.test(newPassword)
|
||||||
|
const hasLowerCase = /[a-z]/.test(newPassword)
|
||||||
|
const hasNumberOrSymbol = /[0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(newPassword)
|
||||||
|
|
||||||
|
if (!hasUpperCase || !hasLowerCase || !hasNumberOrSymbol) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Password must contain uppercase, lowercase, and number/symbol',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update with new password
|
||||||
|
const saltRounds = 10
|
||||||
|
const hashedPassword = bcrypt.hashSync(newPassword, saltRounds)
|
||||||
|
db.prepare('UPDATE users SET first_name = ?, last_name = ?, email = ?, password = ? WHERE id = ?')
|
||||||
|
.run(firstName, lastName, email.toLowerCase(), hashedPassword, currentUser.id)
|
||||||
|
} else {
|
||||||
|
// Update without changing password
|
||||||
|
db.prepare('UPDATE users SET first_name = ?, last_name = ?, email = ? WHERE id = ?')
|
||||||
|
.run(firstName, lastName, email.toLowerCase(), currentUser.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, message: 'Profile updated successfully' }
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user