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 const resetResponse = await fetch(`${adminBaseUrl}/users/${userId}/execute-actions-email`, { method: 'PUT', headers: { 'Authorization': `Bearer ${adminToken.access_token}`, 'Content-Type': 'application/json', 'User-Agent': 'MonacoUSA-Portal/1.0' }, body: JSON.stringify(['UPDATE_PASSWORD']) }); 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); 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); // 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.' }); } });