import { redirect, isRedirect } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; import { setAccessTokenCookies } from '$lib/server/access-token-cookie'; import { clearWorkspaceReturnPath, getWorkspaceLoginUrl, readWorkspaceReturnPath, storeReturnPath, } from '$lib/server/workspace-auth'; export const load: PageServerLoad = async ({ url, cookies, fetch }) => { // Obtener el código y state de los query params const code = url.searchParams.get('code'); const state = url.searchParams.get('state'); const errorParam = url.searchParams.get('error'); const errorDescription = url.searchParams.get('error_description'); if (errorParam) { console.error('❌ [Callback Server] KC auth error:', errorParam, errorDescription); // login_required means no KC session exists yet → send to Workspace login. // Preserve the intended destination through the detour so /login can pick it up. if (state) { try { const stateObj = JSON.parse(state); const returnPath = stateObj.redirect_url; if (returnPath && returnPath.startsWith('/') && returnPath !== '/login') { storeReturnPath(cookies, returnPath); } } catch { /* ignore malformed state */ } } throw redirect(303, getWorkspaceLoginUrl(url.origin)); } if (!code) { console.error('❌ [Callback Server] No se recibió código de autorización'); throw redirect(303, getWorkspaceLoginUrl(url.origin)); } try { // Intercambiar código por tokens usando el backend de Keycloak // En el servidor (SSR), usar KEYCLOAK_URL que apunta a http://keycloak:8080 // En producción o fuera de Docker, usar VITE_KEYCLOAK_URL como fallback const KEYCLOAK_URL = process.env.KEYCLOAK_URL || process.env.VITE_KEYCLOAK_URL || 'http://localhost:8080'; const KEYCLOAK_REALM = process.env.KEYCLOAK_REALM || process.env.VITE_KEYCLOAK_REALM || 'master'; const KEYCLOAK_CLIENT_ID = process.env.KEYCLOAK_CLIENT_ID || process.env.VITE_KEYCLOAK_CLIENT_ID || 'anexo76-backend'; const KEYCLOAK_CLIENT_SECRET = process.env.KEYCLOAK_CLIENT_SECRET || ''; // La redirect_uri debe coincidir exactamente con la registrada en Keycloak const redirectUri = `${url.origin}/auth/callback`; const tokenEndpoint = `${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token`; const body = new URLSearchParams({ grant_type: 'authorization_code', code: code, redirect_uri: redirectUri, client_id: KEYCLOAK_CLIENT_ID, ...(KEYCLOAK_CLIENT_SECRET && { client_secret: KEYCLOAK_CLIENT_SECRET }) }); const tokenResponse = await fetch(tokenEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString() }); if (!tokenResponse.ok) { const errorData = await tokenResponse.text(); console.error('❌ [Callback Server] Error al intercambiar código:', errorData); throw new Error('Error al obtener tokens'); } const tokens = await tokenResponse.json(); // Establecer las cookies en el servidor // access_token → NO HttpOnly (el cliente JS lo usa para el header Authorization) // refresh_token → HttpOnly (el JS nunca lo lee; el servidor lo gestiona) const { isSecureContext } = await import('$lib/server/workspace-auth'); const isProduction = isSecureContext(); setAccessTokenCookies(cookies, tokens.access_token, { secure: isProduction, maxAge: 60 * 60 * 24 * 7 // 7 días }); if (tokens.refresh_token) { cookies.set('refresh_token', tokens.refresh_token, { path: '/', httpOnly: true, // *** HttpOnly: nunca expuesto a JS *** secure: isProduction, sameSite: 'lax', maxAge: 60 * 60 * 24 * 30 // 30 días }); } // Obtener la URL de redirección del state o ir al dashboard let redirectTo = readWorkspaceReturnPath(cookies, '/dashboard'); if (state) { try { const stateObj = JSON.parse(state); redirectTo = stateObj.redirect_url || '/dashboard'; } catch (e) { console.warn('⚠️ [Callback Server] No se pudo obtener redirect_url del state'); } } clearWorkspaceReturnPath(cookies); // Redirigir a la página de destino throw redirect(303, redirectTo); } catch (err: any) { if (isRedirect(err)) throw err; console.error('❌ [Callback Server] Error procesando autenticación:', err); throw redirect(303, getWorkspaceLoginUrl(url.origin)); } };