export default defineEventHandler(async (event) => { console.log('🔄 Forgot password endpoint called at:', new Date().toISOString()); try { const { email } = await readBody(event); console.log('📧 Password reset request for email:', email ? 'present' : 'missing'); // Input validation if (!email || typeof email !== 'string') { throw createError({ statusCode: 400, statusMessage: 'Email is required' }); } // Basic email validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { throw createError({ statusCode: 400, statusMessage: 'Please enter a valid email address' }); } const config = useRuntimeConfig(); // Validate Keycloak configuration if (!config.keycloak?.issuer || !config.keycloak?.clientId || !config.keycloak?.clientSecret) { console.error('❌ Missing Keycloak configuration'); throw createError({ statusCode: 500, statusMessage: 'Authentication service configuration error' }); } console.log('🔧 Using Keycloak config for password reset:', { issuer: config.keycloak.issuer, clientId: config.keycloak.clientId }); try { // Get admin token for Keycloak admin API const adminTokenResponse = await fetch(`${config.keycloak.issuer}/protocol/openid-connect/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'MonacoUSA-Portal/1.0' }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: config.keycloak.clientId, client_secret: config.keycloak.clientSecret }) }); if (!adminTokenResponse.ok) { console.error('❌ Failed to get admin token:', adminTokenResponse.status); throw new Error('Failed to authenticate with admin service'); } const adminToken = await adminTokenResponse.json(); console.log('✅ Admin token obtained'); // Find user by email using Keycloak admin API const realmName = config.keycloak.issuer.split('/realms/')[1]; const adminBaseUrl = config.keycloak.issuer.replace('/realms/', '/admin/realms/'); const usersResponse = await fetch(`${adminBaseUrl}/users?email=${encodeURIComponent(email)}&exact=true`, { headers: { 'Authorization': `Bearer ${adminToken.access_token}`, 'User-Agent': 'MonacoUSA-Portal/1.0' } }); if (!usersResponse.ok) { console.error('❌ Failed to search users:', usersResponse.status); throw new Error('Failed to search for user'); } const users = await usersResponse.json(); console.log('🔍 User search result:', { found: users.length > 0 }); if (users.length === 0) { // For security, don't reveal if email exists or not console.log('⚠️ Email not found, but returning success message for security'); return { success: true, message: 'If the email exists in our system, a reset link has been sent.' }; } const userId = users[0].id; console.log('👤 Found user:', { id: userId, email: users[0].email }); // Send reset password email using Keycloak's execute-actions-email // Add query parameters for better email template rendering const resetUrl = new URL(`${adminBaseUrl}/users/${userId}/execute-actions-email`); resetUrl.searchParams.set('clientId', config.keycloak.clientId); resetUrl.searchParams.set('redirectUri', `${config.keycloak.callbackUrl.replace('/auth/callback', '/login')}`); resetUrl.searchParams.set('lifespan', '43200'); // 12 hours console.log('🔄 Sending password reset email with parameters:', { clientId: config.keycloak.clientId, redirectUri: resetUrl.searchParams.get('redirectUri'), lifespan: resetUrl.searchParams.get('lifespan') }); // Create AbortController for timeout handling const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout const resetResponse = await fetch(resetUrl.toString(), { method: 'PUT', headers: { 'Authorization': `Bearer ${adminToken.access_token}`, 'Content-Type': 'application/json', 'User-Agent': 'MonacoUSA-Portal/1.0' }, body: JSON.stringify(['UPDATE_PASSWORD']), signal: controller.signal }); clearTimeout(timeoutId); if (!resetResponse.ok) { console.error('❌ Failed to send reset email:', resetResponse.status); const errorText = await resetResponse.text().catch(() => 'Unknown error'); console.error('Reset email error details:', errorText); // Enhanced error handling for different scenarios if (resetResponse.status === 500) { console.error('🚨 SMTP server error detected - this usually indicates email configuration issues in Keycloak'); console.error('💡 Suggestion: Check Keycloak Admin Console → Realm Settings → Email tab'); // For now, still return success to user for security, but log the issue console.log('🔄 Returning success message to user despite email failure for security'); return { success: true, message: 'If the email exists in our system, a reset link has been sent. If you don\'t receive an email, please contact your administrator.' }; } throw new Error('Failed to send reset email'); } console.log('✅ Password reset email sent successfully'); return { success: true, message: 'If the email exists in our system, a reset link has been sent.' }; } catch (keycloakError: any) { console.error('❌ Keycloak API error:', keycloakError); // Handle timeout errors specifically if (keycloakError.name === 'AbortError') { console.error('⏰ Password reset request timed out after 30 seconds'); return { success: true, message: 'Password reset request is being processed. If the email exists in our system, a reset link will be sent shortly.' }; } // Handle SMTP/email server errors if (keycloakError.message?.includes('send reset email') || keycloakError.message?.includes('SMTP')) { console.error('📧 Email server error detected, but user search was successful'); return { success: true, message: 'If the email exists in our system, a reset link has been sent. If you don\'t receive an email, please contact your administrator.' }; } // For security, don't reveal specific errors to the user return { success: true, message: 'If the email exists in our system, a reset link has been sent.' }; } } catch (error: any) { console.error('❌ Forgot password error:', error); // If it's already a createError, just throw it if (error.statusCode) { throw error; } // Generic error for unexpected issues throw createError({ statusCode: 500, statusMessage: 'Failed to process password reset request. Please try again.' }); } });