Self-service password reset

This commit is contained in:
2025-10-06 18:26:01 -04:00
parent 53c9ba8fd7
commit c127ea35f6
13 changed files with 683 additions and 21 deletions

View File

@@ -0,0 +1,44 @@
import { getUserByEmail, createPasswordResetCode } from '~/server/utils/database'
import { sendPasswordResetEmail, generateResetCode } from '~/server/utils/email'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { email } = body
if (!email) {
throw createError({
statusCode: 400,
message: 'Email is required',
})
}
// Check if user exists with this email
const user = getUserByEmail(email)
if (!user) {
// Don't reveal if email exists or not for security
return { success: true, message: 'If an account exists with this email, a reset code has been sent.' }
}
// Generate 6-digit code
const code = generateResetCode()
// Set expiration to 15 minutes from now
const expiresAt = new Date(Date.now() + 15 * 60 * 1000).toISOString()
// Store code in database
createPasswordResetCode(email, code, expiresAt)
// Send email
try {
await sendPasswordResetEmail(email, code)
} catch (error) {
console.error('Failed to send reset email:', error)
throw createError({
statusCode: 500,
message: 'Failed to send reset email. Please try again later.',
})
}
return { success: true, message: 'If an account exists with this email, a reset code has been sent.' }
})

View File

@@ -1,14 +1,23 @@
import { createUser, getUserByUsername } from '~/server/utils/database'
import { createUser, getUserByUsername, getUserByEmail } from '~/server/utils/database'
import { setAuthCookie } from '~/server/utils/auth'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { username, password } = body
const { username, password, email, firstName, lastName } = body
if (!username || !password) {
if (!username || !password || !email || !firstName || !lastName) {
throw createError({
statusCode: 400,
message: 'Username and password are required'
message: 'All fields are required'
})
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email)) {
throw createError({
statusCode: 400,
message: 'Invalid email format'
})
}
@@ -49,7 +58,7 @@ export default defineEventHandler(async (event) => {
})
}
// Check if user already exists
// Check if username already exists
const existingUser = getUserByUsername(username.toLowerCase())
if (existingUser) {
throw createError({
@@ -58,9 +67,18 @@ export default defineEventHandler(async (event) => {
})
}
// Check if email already exists
const existingEmail = getUserByEmail(email.toLowerCase())
if (existingEmail) {
throw createError({
statusCode: 409,
message: 'Email already exists'
})
}
try {
// Create the new user
createUser(username.toLowerCase(), password)
// Create the new user with all fields
createUser(username.toLowerCase(), password, email.toLowerCase(), firstName, lastName)
// Log them in automatically
setAuthCookie(event, username.toLowerCase())

View File

@@ -0,0 +1,50 @@
import { getPasswordResetCode, resetPasswordByEmail, deletePasswordResetCode } from '~/server/utils/database'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { email, code, newPassword } = body
if (!email || !code || !newPassword) {
throw createError({
statusCode: 400,
message: 'Email, code, and new password are required',
})
}
// Validate 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',
})
}
// Verify code exists and hasn't expired
const resetCode = getPasswordResetCode(email, code)
if (!resetCode) {
throw createError({
statusCode: 400,
message: 'Invalid or expired reset code',
})
}
// Reset password
resetPasswordByEmail(email, newPassword)
// Delete used reset code
deletePasswordResetCode(email)
return { success: true, message: 'Password reset successfully' }
})

View File

@@ -0,0 +1,25 @@
import { getPasswordResetCode } from '~/server/utils/database'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { email, code } = body
if (!email || !code) {
throw createError({
statusCode: 400,
message: 'Email and code are required',
})
}
// Verify code exists and hasn't expired
const resetCode = getPasswordResetCode(email, code)
if (!resetCode) {
throw createError({
statusCode: 400,
message: 'Invalid or expired reset code',
})
}
return { success: true, message: 'Code verified successfully' }
})