import type { NextApiRequest, NextApiResponse } from 'next' import crypto from 'crypto' import { findUserByEmail, getUserById } from './[...nextauth]' import { AuthResponse, VerifyEmailInput, TokenPayload } from '@/lib/auth/types' import { withRateLimit } from '@/lib/auth/withAuth' // In-memory token store (will be replaced with database in Agent 2) const emailVerificationTokens = new Map() // Token expiry: 24 hours const TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000 export function generateVerificationToken(userId: string, email: string): string { const token = crypto.randomBytes(32).toString('hex') const payload: TokenPayload = { userId, email, type: 'email_verification', expiresAt: Date.now() + TOKEN_EXPIRY_MS, } emailVerificationTokens.set(token, payload) return token } function verifyToken(token: string): TokenPayload | null { const payload = emailVerificationTokens.get(token) if (!payload) return null if (Date.now() > payload.expiresAt) { emailVerificationTokens.delete(token) return null } return payload } function invalidateToken(token: string): void { emailVerificationTokens.delete(token) } async function handler( req: NextApiRequest, res: NextApiResponse ) { // Handle GET request (verify token from email link) if (req.method === 'GET') { const { token } = req.query if (!token || typeof token !== 'string') { return res.status(400).json({ success: false, message: 'Verification token is required', error: 'VALIDATION_ERROR', }) } const payload = verifyToken(token) if (!payload) { return res.status(400).json({ success: false, message: 'Invalid or expired verification token', error: 'INVALID_TOKEN', }) } const user = getUserById(payload.userId) if (!user) { return res.status(400).json({ success: false, message: 'User not found', error: 'USER_NOT_FOUND', }) } // Mark email as verified user.emailVerified = new Date() // Invalidate the used token invalidateToken(token) console.log(`Email verified for user: ${user.email}`) // Redirect to success page or return success response if (req.headers.accept?.includes('text/html')) { res.redirect(302, '/auth/email-verified') return } return res.status(200).json({ success: true, message: 'Email verified successfully', }) } // Handle POST request (resend verification email) if (req.method === 'POST') { const { email } = req.body if (!email) { return res.status(400).json({ success: false, message: 'Email is required', error: 'VALIDATION_ERROR', }) } const user = findUserByEmail(email) if (!user) { // Return success to prevent email enumeration return res.status(200).json({ success: true, message: 'If an account exists with this email, a verification link has been sent.', }) } if (user.emailVerified) { return res.status(400).json({ success: false, message: 'Email is already verified', error: 'ALREADY_VERIFIED', }) } // Generate new verification token const verificationToken = generateVerificationToken(user.id, user.email) // Build verification URL const baseUrl = process.env.NEXTAUTH_URL || `http://${req.headers.host}` const verifyUrl = `${baseUrl}/api/auth/verify-email?token=${verificationToken}` // TODO: Send verification email (will be implemented with Agent 8 - Notifications) if (process.env.NODE_ENV === 'development') { console.log(`Email verification link for ${email}: ${verifyUrl}`) } return res.status(200).json({ success: true, message: 'If an account exists with this email, a verification link has been sent.', }) } return res.status(405).json({ success: false, message: 'Method not allowed', error: 'Only GET and POST requests are accepted', }) } // Apply rate limiting: 3 requests per minute per IP export default withRateLimit(handler, { limit: 3, windowMs: 60000, })