Files
plantillas-proyectos/frontend/src/routes/auth/callback/+page.server.ts

100 lines
3.5 KiB
TypeScript

import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
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] Error en autenticación:', errorParam, errorDescription);
throw redirect(303, `/login?error=${encodeURIComponent(errorDescription || errorParam)}`);
}
if (!code) {
console.error('❌ [Callback Server] No se recibió código de autorización');
throw redirect(303, '/login?error=No se recibió código de autorización');
}
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 (esto es lo importante)
// Las cookies deben ser HttpOnly y Secure en producción
const isProduction = process.env.NODE_ENV === 'production';
cookies.set('access_token', tokens.access_token, {
path: '/',
httpOnly: true,
secure: isProduction,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7 // 7 días
});
if (tokens.refresh_token) {
cookies.set('refresh_token', tokens.refresh_token, {
path: '/',
httpOnly: true,
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 = '/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');
}
}
// Redirigir a la página de destino
throw redirect(303, redirectTo);
} catch (err: any) {
console.error('❌ [Callback Server] Error procesando autenticación:', err);
throw redirect(303, `/login?error=${encodeURIComponent(err.message || 'Error procesando autenticación')}`);
}
};