doxa-maps-cdn/server/api/profile/email.post.ts
2026-04-21 12:12:54 +01:00

163 lines
4.1 KiB
TypeScript

import bcrypt from 'bcrypt'
import crypto from 'crypto'
import { sql } from 'kysely'
export default defineEventHandler(async (event) => {
// Require authentication
const user = requireAuth(event)
// Get request body
const body = await readBody(event)
const { new_email, current_password } = body
// Validate input
if (!new_email || typeof new_email !== 'string') {
throw createError({
statusCode: 400,
statusMessage: 'New email is required'
})
}
if (!current_password || typeof current_password !== 'string') {
throw createError({
statusCode: 400,
statusMessage: 'Current password is required'
})
}
const trimmedEmail = new_email.trim().toLowerCase()
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(trimmedEmail)) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid email format'
})
}
// Get current user data including password
const currentUser = await db
.selectFrom('users')
.select(['email', 'password'])
.where('id', '=', user.userId)
.executeTakeFirst()
if (!currentUser) {
throw createError({
statusCode: 404,
statusMessage: 'User not found'
})
}
// Check if new email is same as current
if (trimmedEmail === currentUser.email) {
throw createError({
statusCode: 400,
statusMessage: 'New email must be different from current email'
})
}
// Verify current password
const isPasswordValid = await bcrypt.compare(current_password, currentUser.password)
if (!isPasswordValid) {
// Log failed attempt
logEvent({
eventType: 'EMAIL_CHANGE_FAILED',
tableName: 'users',
recordId: user.userId,
userId: user.userId,
userAgent: getHeader(event, 'user-agent') || undefined,
metadata: {
reason: 'invalid_password',
attempted_new_email: trimmedEmail
}
})
throw createError({
statusCode: 401,
statusMessage: 'Current password is incorrect'
})
}
// Check if new email is already taken by another user
const existingUser = await db
.selectFrom('users')
.select('id')
.where('email', '=', trimmedEmail)
.executeTakeFirst()
if (existingUser) {
throw createError({
statusCode: 400,
statusMessage: 'This email address is already in use'
})
}
// Generate verification token
const emailChangeToken = crypto.randomUUID()
// Store pending email and token
await db
.updateTable('users')
.set({
pending_email: trimmedEmail,
email_change_token: emailChangeToken,
updated: sql`now()`,
})
.where('id', '=', user.userId)
.execute()
// Get runtime config for email
const config = useRuntimeConfig()
// Send verification email to NEW email address
try {
const verificationUrl = `${process.env.APP_URL || 'http://localhost:3002'}/api/auth/verify-email-change?token=${emailChangeToken}`
await sendTemplateEmail({
to: trimmedEmail,
subject: 'Verify Your New Email Address',
template: 'emailChangeVerification',
data: {
userName: user.display_name,
oldEmail: currentUser.email,
newEmail: trimmedEmail,
verificationUrl: verificationUrl
}
})
// Log successful email change request
logEvent({
eventType: 'EMAIL_CHANGE_REQUESTED',
tableName: 'users',
recordId: user.userId,
userId: user.userId,
userAgent: getHeader(event, 'user-agent') || undefined,
metadata: {
old_email: currentUser.email,
new_email: trimmedEmail
}
})
return {
success: true,
message: 'Verification email sent to new address'
}
} catch (error) {
console.error('Error sending verification email:', error)
// Clean up pending email change on error
await db
.updateTable('users')
.set({ pending_email: null, email_change_token: null })
.where('id', '=', user.userId)
.execute()
throw createError({
statusCode: 500,
statusMessage: 'Failed to send verification email'
})
}
})