import jwt from 'jsonwebtoken'; export interface EmailVerificationTokenPayload { userId: string; email: string; purpose: 'email-verification'; iat: number; } // In-memory token storage for validation (in production, consider Redis) const activeTokens = new Map(); /** * Generate a secure JWT token for email verification */ export async function generateEmailVerificationToken(userId: string, email: string): Promise { const runtimeConfig = useRuntimeConfig(); // Get JWT secret with multiple fallbacks const jwtSecret = (runtimeConfig.jwtSecret as string) || (runtimeConfig.sessionSecret as string) || (runtimeConfig.encryptionKey as string) || process.env.NUXT_JWT_SECRET || process.env.NUXT_SESSION_SECRET || process.env.NUXT_ENCRYPTION_KEY || 'fallback-secret-key-for-email-tokens-please-configure-proper-jwt-secret'; if (!jwtSecret || typeof jwtSecret !== 'string' || jwtSecret.length < 10) { throw new Error('JWT secret not configured properly. Please set NUXT_JWT_SECRET environment variable'); } console.log('[email-tokens] Using JWT secret source:', runtimeConfig.jwtSecret ? 'runtimeConfig.jwtSecret' : runtimeConfig.sessionSecret ? 'runtimeConfig.sessionSecret' : runtimeConfig.encryptionKey ? 'runtimeConfig.encryptionKey' : process.env.NUXT_JWT_SECRET ? 'NUXT_JWT_SECRET' : process.env.NUXT_SESSION_SECRET ? 'NUXT_SESSION_SECRET' : process.env.NUXT_ENCRYPTION_KEY ? 'NUXT_ENCRYPTION_KEY' : 'fallback'); const payload: EmailVerificationTokenPayload = { userId, email: email.toLowerCase().trim(), purpose: 'email-verification', iat: Date.now() }; const token = jwt.sign(payload, jwtSecret, { expiresIn: '24h', issuer: 'monacousa-portal', audience: 'email-verification' }); // Store token metadata for additional validation activeTokens.set(token, payload); // Clean up expired tokens periodically setTimeout(() => { activeTokens.delete(token); }, 24 * 60 * 60 * 1000); // 24 hours console.log('[email-tokens] Generated verification token for user:', userId, 'email:', email); return token; } /** * Verify and decode an email verification token */ export async function verifyEmailToken(token: string): Promise<{ userId: string; email: string }> { const runtimeConfig = useRuntimeConfig(); // Get JWT secret with multiple fallbacks (same as generation) const jwtSecret = (runtimeConfig.jwtSecret as string) || (runtimeConfig.sessionSecret as string) || (runtimeConfig.encryptionKey as string) || process.env.NUXT_JWT_SECRET || process.env.NUXT_SESSION_SECRET || process.env.NUXT_ENCRYPTION_KEY || 'fallback-secret-key-for-email-tokens-please-configure-proper-jwt-secret'; if (!jwtSecret || typeof jwtSecret !== 'string' || jwtSecret.length < 10) { throw new Error('JWT secret not configured properly. Please set NUXT_JWT_SECRET environment variable'); } if (!token) { throw new Error('Token is required'); } try { // Verify JWT signature and expiration const decoded = jwt.verify(token, jwtSecret, { issuer: 'monacousa-portal', audience: 'email-verification' }) as any as EmailVerificationTokenPayload; // Validate token purpose if (decoded.purpose !== 'email-verification') { throw new Error('Invalid token purpose'); } // Check if token exists in our active tokens (prevents replay attacks) const storedPayload = activeTokens.get(token); if (!storedPayload) { throw new Error('Token not found or already used'); } // Validate payload consistency if (storedPayload.userId !== decoded.userId || storedPayload.email !== decoded.email) { throw new Error('Token payload mismatch'); } // Remove token after successful verification (single use) activeTokens.delete(token); console.log('[email-tokens] Successfully verified token for user:', decoded.userId, 'email:', decoded.email); return { userId: decoded.userId, email: decoded.email }; } catch (error: any) { console.error('[email-tokens] Token verification failed:', error.message); // Provide user-friendly error messages if (error.name === 'TokenExpiredError') { throw new Error('Verification link has expired. Please request a new one.'); } else if (error.name === 'JsonWebTokenError') { throw new Error('Invalid verification link.'); } else { throw new Error(error.message || 'Token verification failed'); } } } /** * Check if a token is still valid without consuming it */ export async function isTokenValid(token: string): Promise { try { const runtimeConfig = useRuntimeConfig(); // Get JWT secret with multiple fallbacks (same as generation) const jwtSecret = (runtimeConfig.jwtSecret as string) || (runtimeConfig.sessionSecret as string) || (runtimeConfig.encryptionKey as string) || process.env.NUXT_JWT_SECRET || process.env.NUXT_SESSION_SECRET || process.env.NUXT_ENCRYPTION_KEY || 'fallback-secret-key-for-email-tokens-please-configure-proper-jwt-secret'; if (!jwtSecret || typeof jwtSecret !== 'string' || jwtSecret.length < 10 || !token) { return false; } const decoded = jwt.verify(token, jwtSecret, { issuer: 'monacousa-portal', audience: 'email-verification' }) as any as EmailVerificationTokenPayload; return decoded.purpose === 'email-verification' && activeTokens.has(token); } catch (error) { return false; } } /** * Clean up expired tokens from memory */ export function cleanupExpiredTokens(): void { const now = Date.now(); const expirationTime = 24 * 60 * 60 * 1000; // 24 hours for (const [token, payload] of activeTokens.entries()) { if (now - payload.iat > expirationTime) { activeTokens.delete(token); } } console.log('[email-tokens] Cleaned up expired tokens. Active tokens:', activeTokens.size); } /** * Get statistics about active tokens */ export function getTokenStats(): { activeTokens: number; oldestToken: number | null } { const now = Date.now(); let oldestToken: number | null = null; for (const payload of activeTokens.values()) { if (oldestToken === null || payload.iat < oldestToken) { oldestToken = payload.iat; } } return { activeTokens: activeTokens.size, oldestToken: oldestToken ? Math.floor((now - oldestToken) / 1000 / 60) : null // minutes ago }; } // Periodic cleanup of expired tokens (every hour) setInterval(cleanupExpiredTokens, 60 * 60 * 1000);