138 lines
4.2 KiB
TypeScript
138 lines
4.2 KiB
TypeScript
|
|
export default defineEventHandler(async (event) => {
|
||
|
|
try {
|
||
|
|
const body = await readBody(event);
|
||
|
|
const { email } = body;
|
||
|
|
|
||
|
|
if (!email || typeof email !== 'string') {
|
||
|
|
throw createError({
|
||
|
|
statusCode: 400,
|
||
|
|
statusMessage: 'Email is required'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validate email format
|
||
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
|
|
if (!emailRegex.test(email)) {
|
||
|
|
throw createError({
|
||
|
|
statusCode: 400,
|
||
|
|
statusMessage: 'Invalid email format'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('[send-verification-email] Processing request for email:', email);
|
||
|
|
|
||
|
|
// Check if user exists in Keycloak
|
||
|
|
const { createKeycloakAdminClient } = await import('~/server/utils/keycloak-admin');
|
||
|
|
const keycloak = createKeycloakAdminClient();
|
||
|
|
|
||
|
|
let existingUsers;
|
||
|
|
try {
|
||
|
|
existingUsers = await keycloak.findUserByEmail(email.toLowerCase().trim());
|
||
|
|
} catch (error: any) {
|
||
|
|
console.error('[send-verification-email] Failed to search users:', error.message);
|
||
|
|
throw createError({
|
||
|
|
statusCode: 500,
|
||
|
|
statusMessage: 'Failed to verify account status'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!existingUsers || existingUsers.length === 0) {
|
||
|
|
throw createError({
|
||
|
|
statusCode: 404,
|
||
|
|
statusMessage: 'No account found with this email address'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const user = existingUsers[0];
|
||
|
|
|
||
|
|
// Check if user is already verified
|
||
|
|
if (user.emailVerified) {
|
||
|
|
throw createError({
|
||
|
|
statusCode: 400,
|
||
|
|
statusMessage: 'This email address is already verified'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Rate limiting: check if we recently sent an email to this address
|
||
|
|
const rateLimitKey = `verification_email_${email.toLowerCase()}`;
|
||
|
|
|
||
|
|
// Simple in-memory rate limiting (in production, use Redis)
|
||
|
|
const globalCache = globalThis as any;
|
||
|
|
if (!globalCache.verificationEmailCache) {
|
||
|
|
globalCache.verificationEmailCache = new Map();
|
||
|
|
}
|
||
|
|
|
||
|
|
const lastSent = globalCache.verificationEmailCache.get(rateLimitKey);
|
||
|
|
const cooldownPeriod = 2 * 60 * 1000; // 2 minutes
|
||
|
|
|
||
|
|
if (lastSent && Date.now() - lastSent < cooldownPeriod) {
|
||
|
|
throw createError({
|
||
|
|
statusCode: 429,
|
||
|
|
statusMessage: 'Please wait a few minutes before requesting another verification email'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate verification token
|
||
|
|
const { generateEmailVerificationToken } = await import('~/server/utils/email-tokens');
|
||
|
|
const verificationToken = await generateEmailVerificationToken(user.id, email);
|
||
|
|
|
||
|
|
// Get configuration
|
||
|
|
const config = useRuntimeConfig();
|
||
|
|
const verificationLink = `${config.public.domain}/api/auth/verify-email?token=${verificationToken}`;
|
||
|
|
|
||
|
|
// Send verification email
|
||
|
|
const { getEmailService } = await import('~/server/utils/email');
|
||
|
|
const emailService = getEmailService();
|
||
|
|
|
||
|
|
try {
|
||
|
|
await emailService.sendWelcomeEmail(email, {
|
||
|
|
firstName: user.firstName || '',
|
||
|
|
lastName: user.lastName || '',
|
||
|
|
verificationLink,
|
||
|
|
memberId: user.id
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log('[send-verification-email] Successfully sent verification email to:', email);
|
||
|
|
|
||
|
|
// Update rate limiting cache
|
||
|
|
globalCache.verificationEmailCache.set(rateLimitKey, Date.now());
|
||
|
|
|
||
|
|
// Clean up old rate limit entries periodically
|
||
|
|
if (Math.random() < 0.1) { // 10% chance
|
||
|
|
const now = Date.now();
|
||
|
|
for (const [key, timestamp] of globalCache.verificationEmailCache.entries()) {
|
||
|
|
if (now - timestamp > cooldownPeriod * 2) {
|
||
|
|
globalCache.verificationEmailCache.delete(key);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
success: true,
|
||
|
|
message: 'Verification email sent successfully'
|
||
|
|
};
|
||
|
|
|
||
|
|
} catch (emailError: any) {
|
||
|
|
console.error('[send-verification-email] Failed to send email:', emailError.message);
|
||
|
|
throw createError({
|
||
|
|
statusCode: 500,
|
||
|
|
statusMessage: 'Failed to send verification email. Please try again later.'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
} catch (error: any) {
|
||
|
|
console.error('[send-verification-email] Request failed:', error.message);
|
||
|
|
|
||
|
|
// Re-throw HTTP errors
|
||
|
|
if (error.statusCode) {
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle unexpected errors
|
||
|
|
throw createError({
|
||
|
|
statusCode: 500,
|
||
|
|
statusMessage: 'An unexpected error occurred. Please try again.'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|