import type { APIRoute } from 'astro'; import { createSupabaseServerClient } from '../../../lib/supabase-ssr'; // Helper function to retry authentication with exponential backoff async function retryAuth(supabase: any, email: string, password: string, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const { data, error } = await supabase.auth.signInWithPassword({ email, password, }); // If successful or non-rate-limit error, return immediately if (!error || (!error.message.includes('over_request_rate_limit') && error.status !== 429)) { return { data, error }; } // If rate limited and not final attempt, wait and retry if (attempt < maxRetries) { const delay = Math.pow(2, attempt) * 1000; // Exponential backoff: 2s, 4s, 8s console.log(`[LOGIN] Rate limited, waiting ${delay}ms before retry ${attempt + 1}`); await new Promise(resolve => setTimeout(resolve, delay)); continue; } // Final attempt failed, return the error return { data, error }; } catch (err) { // Non-auth errors, return immediately return { data: null, error: err }; } } } export const POST: APIRoute = async ({ request, cookies }) => { try { const formData = await request.json(); const { email, password } = formData; console.log('[LOGIN] Attempting login for:', email); if (!email || !password) { return new Response(JSON.stringify({ error: 'Email and password are required' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } const supabase = createSupabaseServerClient(cookies); console.log('[LOGIN] Created Supabase client'); const { data, error } = await retryAuth(supabase, email, password); console.log('[LOGIN] Supabase response:', { hasUser: !!data?.user, hasSession: !!data?.session, error: error?.message }); if (error) { console.log('[LOGIN] Authentication failed:', error.message); // Handle specific error types if (error.message.includes('over_request_rate_limit') || error.status === 429) { return new Response(JSON.stringify({ error: 'Too many login attempts. Please wait 5 minutes and try again.', code: 'RATE_LIMITED' }), { status: 429, headers: { 'Content-Type': 'application/json' } }); } // Handle invalid credentials if (error.message.includes('Invalid login credentials') || error.message.includes('Email not confirmed')) { return new Response(JSON.stringify({ error: 'Invalid email or password. Please check your credentials.', code: 'INVALID_CREDENTIALS' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); } // Generic error fallback return new Response(JSON.stringify({ error: error.message || 'Authentication failed', code: 'AUTH_ERROR' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); } // Get user organization console.log('[LOGIN] Looking up user data for ID:', data.user.id); const { data: userData, error: userError } = await supabase .from('users') .select('organization_id, role') .eq('id', data.user.id) .single(); console.log('[LOGIN] User data lookup:', { userData, userError: userError?.message, hasOrganization: !!userData?.organization_id, userId: data.user.id }); // If user lookup failed, log detailed error and return error response if (userError) { console.error('[LOGIN] User lookup failed:', { error: userError, userId: data.user.id, userEmail: data.user.email }); return new Response(JSON.stringify({ error: 'Failed to retrieve user profile. Please try again.' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } const redirectTo = !userData?.organization_id ? '/onboarding/organization' : '/dashboard'; console.log('[LOGIN] Redirecting to:', redirectTo); return new Response(JSON.stringify({ success: true, user: data.user, organizationId: userData?.organization_id, isAdmin: userData?.role === 'admin', isSuperAdmin: false, // Super admin logic can be added later if needed redirectTo }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } catch (error) { console.error('Login error:', error); return new Response(JSON.stringify({ error: 'An error occurred during login' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } };