import { NextRequest, NextResponse } from 'next/server'; /** * API Route pour échanger le code d'autorisation contre des tokens */ export async function POST(request: NextRequest) { try { let body; try { body = await request.json(); } catch (jsonError) { console.error('❌ Erreur parsing JSON:', jsonError.message); return NextResponse.json( { error: 'JSON invalide' }, { status: 400 } ); } const { code, state } = body; if (!code) { return NextResponse.json( { error: 'Code d\'autorisation manquant' }, { status: 400 } ); } // Configuration Keycloak const keycloakUrl = process.env.NEXT_PUBLIC_KEYCLOAK_URL || 'https://security.lions.dev'; const realm = process.env.NEXT_PUBLIC_KEYCLOAK_REALM || 'btpxpress'; const clientId = process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID || 'btpxpress-frontend'; // Préparer l'URL de base const baseUrl = process.env.NODE_ENV === 'production' ? 'https://btpxpress.lions.dev' : 'http://localhost:3000'; // Récupérer le code verifier depuis les cookies const codeVerifier = request.cookies.get('pkce_code_verifier')?.value; console.log('🍪 Cookies reçus:', { codeVerifier: codeVerifier ? codeVerifier.substring(0, 20) + '...' : 'MANQUANT', hasCookie: !!codeVerifier }); if (!codeVerifier) { console.error('❌ Code verifier manquant dans les cookies'); return NextResponse.json( { error: 'Code verifier manquant' }, { status: 400 } ); } console.log('🔄 Échange de token:', { code: code.substring(0, 20) + '...', codeVerifier: codeVerifier.substring(0, 20) + '...', redirectUri: `${baseUrl}/dashboard` }); // Préparer les données pour l'échange de token const tokenData = new URLSearchParams({ grant_type: 'authorization_code', client_id: clientId, code: code, redirect_uri: `${baseUrl}/dashboard`, code_verifier: codeVerifier, }); // Échanger le code contre des tokens const tokenResponse = await fetch( `${keycloakUrl}/realms/${realm}/protocol/openid-connect/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: tokenData.toString(), } ); if (!tokenResponse.ok) { const errorText = await tokenResponse.text(); console.error('❌ Erreur échange token:', { status: tokenResponse.status, statusText: tokenResponse.statusText, error: errorText }); return NextResponse.json( { error: 'Échec de l\'échange de token', details: errorText }, { status: 400 } ); } const tokens = await tokenResponse.json(); console.log('✅ Tokens reçus avec succès:', { hasAccessToken: !!tokens.access_token, hasRefreshToken: !!tokens.refresh_token, hasIdToken: !!tokens.id_token }); // Supprimer le cookie du code verifier const response = NextResponse.json({ ...tokens, returnUrl: '/dashboard' // URL par défaut, sera remplacée côté client si returnUrl existe }); response.cookies.delete('pkce_code_verifier'); return response; } catch (error) { console.error('❌ Erreur lors de l\'échange de token:', { message: error.message, stack: error.stack, name: error.name, cause: error.cause }); // Si c'est une erreur de code invalide, suggérer un nouveau cycle d'authentification if (error.message && error.message.includes('invalid_grant')) { console.log('🔄 Code d\'autorisation expiré, nettoyage des cookies...'); const response = NextResponse.json( { error: 'Code d\'autorisation expiré', details: 'Le code d\'autorisation a expiré. Un nouveau cycle d\'authentification est nécessaire.', shouldRetry: true }, { status: 400 } ); // Nettoyer le cookie du code verifier expiré response.cookies.delete('pkce_code_verifier'); return response; } return NextResponse.json( { error: 'Erreur interne du serveur', details: error.message }, { status: 500 } ); } } /** * Gestion des autres méthodes HTTP */ export async function GET() { return NextResponse.json( { error: 'Méthode non supportée. Utilisez POST pour échanger un code.' }, { status: 405 } ); }