From b76d42be830d2b163c67d94f9ab4402e2629bf3c Mon Sep 17 00:00:00 2001 From: Ernesto Herrera Date: Fri, 17 Jul 2026 09:01:10 -0600 Subject: [PATCH] =?UTF-8?q?refactor(auth):=20login=20100%=20v=C3=ADa=20Wor?= =?UTF-8?q?kspace/Hub=20=E2=80=94=20sin=20comunicaci=C3=B3n=20directa=20a?= =?UTF-8?q?=20Keycloak?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Patrón SIWEB: el CRM ya NO habla directo a Keycloak. Se elimina el flujo OIDC paralelo (que causaba el bucle e "issuer mismatch"): - workspace-auth.ts: se quitan builders de URL de KC (authorization/login/logout) y getPublicKeycloakBaseUrl/Realm/ClientId. getWorkspaceLoginUrl ya no lleva return_to a /login?sso_verified (evita el rebote sin sesión = bucle). - /login: sso_verified=1 → /dashboard; sin sesión → App Launcher del Workspace (relay). Se eliminan redirectToKeycloakAuthorization/Login. - /auth/callback: obsoleto — ya no intercambia code con KC; redirige a /dashboard. - /logout y /auth/post-logout: limpian sesión local y vuelven al Workspace (el logout completo del Hub/KC se hace desde el Workspace). - /join: usa redirectToWorkspaceLogin en vez de KC. - lib/auth.ts: initAuth ya no inicializa keycloak-js en el browser; getToken y refreshAccessToken operan por cookie + silent-refresh (backend → Hub). Login = solo App Launcher del Workspace (relay → /auth/sso → Hub /sso-exchange). Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/lib/auth.ts | 8 +- frontend/src/lib/server/workspace-auth.ts | 168 ++++-------------- .../src/routes/auth/callback/+page.server.ts | 141 ++------------- .../src/routes/auth/post-logout/+server.ts | 11 +- frontend/src/routes/join/+page.server.ts | 6 +- frontend/src/routes/login/+page.server.ts | 33 +--- frontend/src/routes/logout/+server.ts | 37 ++-- 7 files changed, 79 insertions(+), 325 deletions(-) diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index 0e764a7..666e621 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -407,10 +407,12 @@ export const initAuth = async (): Promise => { return true; } - // Sin token local, intentar Keycloak JS (flujo SSO) - const authenticated = await initKeycloak(); + // Sin token local: no hay sesión en el cliente. El login entra SIEMPRE por + // el App Launcher del Workspace (relay → /auth/sso → Hub /sso-exchange); + // el CRM NO inicializa Keycloak en el browser. Si no hay token, el layout + // del servidor reenvía al Workspace. authStore.setLoading(false); - return authenticated; + return false; } catch (err) { console.error('[auth] Error en initAuth:', err); authStore.setLoading(false); diff --git a/frontend/src/lib/server/workspace-auth.ts b/frontend/src/lib/server/workspace-auth.ts index cc94854..8e0dee6 100644 --- a/frontend/src/lib/server/workspace-auth.ts +++ b/frontend/src/lib/server/workspace-auth.ts @@ -5,9 +5,14 @@ const DEFAULT_WORKSPACE_BASE_URL = 'https://workspace.aduanasoft.com'; const RETURN_PATH_COOKIE = 'workspace_return_path'; /** - * Returns true only when the public-facing URL uses HTTPS. - * Use this for cookie `secure` flag instead of NODE_ENV so that - * cookies work on HTTP LAN dev environments (e.g. 192.168.x.x). + * Autenticación 100% vía el Hub/Workspace (patrón SIWEB). El CRM NUNCA habla + * directo a Keycloak: el login entra por el App Launcher del workspace (relay + * → /auth/sso → Hub /sso-exchange) y el resto de auth va por la API del Hub. + */ + +/** + * True solo cuando la URL pública usa HTTPS. Se usa para el flag `secure` de las + * cookies (en vez de NODE_ENV) para que funcionen en dev HTTP LAN (192.168.x.x). */ export function isSecureContext(): boolean { const origin = (env.ORIGIN || process.env.ORIGIN || '').trim(); @@ -20,9 +25,8 @@ function stripTrailingSlashes(value: string): string { } /** - * Detecta si una URL apunta a un host que solo es accesible localmente: - * localhost, 127.0.0.1, IPs de red LAN/privada y hostnames internos de Docker. - * Estas URLs no son válidas como redirect_uri ni como KC public URL en producción. + * Detecta URLs solo accesibles localmente (localhost, IPs LAN/privadas, hosts + * internos de Docker). No son válidas como URL pública del Workspace. */ function isDevOnlyUrl(rawUrl: string): boolean { try { @@ -60,27 +64,14 @@ export function getWorkspaceBaseUrl(): string { } /** - * Normaliza la URL base del sistema (Mi Aplicación) para construir redirect_uri seguros. - * - * Problema habitual en producción: SvelteKit deriva `url.origin` de la variable de entorno - * `ORIGIN`. Si el contenedor se despliega con `ORIGIN=http://localhost:5173` (valor del .env - * de dev), todos los redirect_uri generados por el servidor apuntan a localhost. - * - * Esta función: - * 1. Usa `requestOrigin` si ya es una URL pública (no dev-only). - * 2. Si es localhost, busca `SITE_URL` (env var de producción recomendada) como fallback. - * 3. Como último recurso devuelve requestOrigin tal cual (entorno dev genuino). - * - * Var de entorno recomendada en producción: - * SITE_URL=https://mi-app.dominio.com (además de arreglar ORIGIN) + * Normaliza la URL base del sistema (Mi Aplicación) para construir redirect_uri. + * Corrige el caso donde `ORIGIN` env var apunta a localhost en producción. */ export function resolveSystemBaseUrl(requestOrigin: string): string { if (!isDevOnlyUrl(requestOrigin)) { return stripTrailingSlashes(requestOrigin); } - // requestOrigin es dev-only → ORIGIN env var apunta a localhost en producción. - // Buscar URL pública en env vars adicionales. const candidates = [ (env.SITE_URL || '').trim(), (env.APP_URL || '').trim(), @@ -93,31 +84,17 @@ export function resolveSystemBaseUrl(requestOrigin: string): string { } } - // Entorno dev genuino: devolver requestOrigin tal cual return stripTrailingSlashes(requestOrigin); } -export type WorkspaceLoginUrlOptions = { - /** - * URL del login del Hub sin `return_to`. Usar en `post_logout_redirect_uri` para que, - * tras logout en KC, el Hub aplique myApps() (launcher si el usuario tiene varias apps). - * Con `return_to` a Mi Aplicación, el re-login siempre rebotaba a esa app aunque hubiera más. - */ - forPostLogout?: boolean; -}; - -export function getWorkspaceLoginUrl( - systemBaseUrl: string, - options?: WorkspaceLoginUrlOptions -): string { - const workspaceBaseUrl = getWorkspaceBaseUrl(); - if (options?.forPostLogout) { - return `${workspaceBaseUrl}/login`; - } - // return_to includes sso_verified=1 so the workspace preserves it when redirecting - // back, regardless of what additional params the workspace appends. - const loginUrl = `${systemBaseUrl}/login?sso_verified=1`; - return `${workspaceBaseUrl}/login?return_to=${encodeURIComponent(loginUrl)}`; +/** + * URL de login del Workspace. NO lleva `return_to` a Mi Aplicación: el Hub + * muestra el App Launcher y el usuario re-entra al CRM por relay + * (→ /auth/sso?relay=). Así se evita el rebote a /login sin sesión (bucle) y no + * se usa ningún flujo OIDC directo contra Keycloak. + */ +export function getWorkspaceLoginUrl(): string { + return `${getWorkspaceBaseUrl()}/login`; } export function storeReturnPath(cookies: Cookies, path: string): void { @@ -131,26 +108,6 @@ export function storeReturnPath(cookies: Cookies, path: string): void { }); } -export function getPublicKeycloakBaseUrl(): string { - const configuredKeycloakUrl = (env.VITE_KEYCLOAK_URL || '').trim(); - // Si VITE_KEYCLOAK_URL apunta a un host dev-only (localhost, IP LAN, Docker service), - // ignorarlo y derivar la URL del hostname público del Workspace. - // Esto protege contra builds donde el .env de dev llega a producción por error. - if (configuredKeycloakUrl && !isDevOnlyUrl(configuredKeycloakUrl)) { - return stripTrailingSlashes(configuredKeycloakUrl); - } - - return `${getWorkspaceBaseUrl()}/kcauth`; -} - -export function getKeycloakRealm(): string { - return (env.KEYCLOAK_REALM || env.VITE_KEYCLOAK_REALM || 'master').trim(); -} - -export function getKeycloakClientId(): string { - return (env.KEYCLOAK_CLIENT_ID || env.VITE_KEYCLOAK_CLIENT_ID || 'app-frontend').trim(); -} - export function getCleanReturnPath(url: URL): string { const cleanParams = new URLSearchParams(url.searchParams); cleanParams.delete('sso_verified'); @@ -186,68 +143,9 @@ export function clearWorkspaceReturnPath(cookies: Cookies): void { cookies.delete(RETURN_PATH_COOKIE, { path: '/' }); } -export function buildKeycloakAuthorizationUrl(systemBaseUrl: string, redirectPath: string): string { - const keycloakBaseUrl = getPublicKeycloakBaseUrl(); - // resolveSystemBaseUrl corrige el caso donde url.origin es localhost por ORIGIN env var mal configurado - const publicBase = resolveSystemBaseUrl(systemBaseUrl); - const redirectUri = `${publicBase}/auth/callback`; - const state = JSON.stringify({ redirect_url: redirectPath }); - const params = new URLSearchParams({ - client_id: getKeycloakClientId(), - redirect_uri: redirectUri, - response_type: 'code', - scope: 'openid', - prompt: 'none', - state - }); - - return `${keycloakBaseUrl}/realms/${getKeycloakRealm()}/protocol/openid-connect/auth?${params.toString()}`; -} - -/** - * Construye URL de login directo en KC sin prompt=none. - * Usa la sesión KC existente si la hay; si no, muestra el form de login. - * Usar cuando se recibe ?redirect= del Hub (rompe el loop Hub↔login). - */ -export function buildKeycloakLoginUrl(systemBaseUrl: string, redirectPath: string): string { - const keycloakBaseUrl = getPublicKeycloakBaseUrl(); - // resolveSystemBaseUrl corrige el caso donde url.origin es localhost por ORIGIN env var mal configurado - const publicBase = resolveSystemBaseUrl(systemBaseUrl); - const redirectUri = `${publicBase}/auth/callback`; - const state = JSON.stringify({ redirect_url: redirectPath }); - const params = new URLSearchParams({ - client_id: getKeycloakClientId(), - redirect_uri: redirectUri, - response_type: 'code', - scope: 'openid', - state - }); - - return `${keycloakBaseUrl}/realms/${getKeycloakRealm()}/protocol/openid-connect/auth?${params.toString()}`; -} - -export function redirectToWorkspaceLogin(cookies: Cookies, url: URL): never { - // Modo local: nunca salir al workspace, mostrar el login local. - if ((env.DEV_LOCAL_AUTH ?? '').toLowerCase() === 'true') { - throw redirect(303, '/login'); - } - storeWorkspaceReturnPath(cookies, url); - throw redirect(303, getWorkspaceLoginUrl(url.origin)); -} - -export function redirectToKeycloakAuthorization(systemBaseUrl: string, redirectPath: string): never { - throw redirect(303, buildKeycloakAuthorizationUrl(systemBaseUrl, redirectPath)); -} - -export function redirectToKeycloakLogin(systemBaseUrl: string, redirectPath: string): never { - throw redirect(303, buildKeycloakLoginUrl(systemBaseUrl, redirectPath)); -} - /** * URL del Hub FastAPI para llamadas server-to-server (ej. sso-exchange). * No aplica isDevOnlyUrl: las URLs internas de Docker son válidas aquí. - * Lee HUB_BACKEND_URL (override explícito) → INTERNAL_HUB_URL (ya en docker-compose) - * → fallback a URL pública del workspace (vía proxy SvelteKit del Hub). */ export function getHubBackendUrl(): string { const direct = @@ -257,19 +155,15 @@ export function getHubBackendUrl(): string { return getWorkspaceBaseUrl(); } -export function buildKeycloakLogoutUrl(systemBaseUrl: string, idTokenHint?: string): string { - const keycloakBaseUrl = getPublicKeycloakBaseUrl(); - const postLogoutRedirectUri = `${systemBaseUrl}/auth/post-logout`; - const params = new URLSearchParams({ - client_id: getKeycloakClientId(), - post_logout_redirect_uri: postLogoutRedirectUri - }); - - // Con id_token_hint KC acepta cualquier post_logout_redirect_uri sin necesidad - // de que esté registrado explícitamente en el cliente. - if (idTokenHint) { - params.set('id_token_hint', idTokenHint); +/** + * Redirige al login del Workspace (App Launcher). Único punto de entrada de + * login: el CRM no inicia ningún flujo contra Keycloak. + */ +export function redirectToWorkspaceLogin(cookies: Cookies, url: URL): never { + // Modo local: nunca salir al workspace, mostrar el login local. + if ((env.DEV_LOCAL_AUTH ?? '').toLowerCase() === 'true') { + throw redirect(303, '/login'); } - - return `${keycloakBaseUrl}/realms/${getKeycloakRealm()}/protocol/openid-connect/logout?${params.toString()}`; -} \ No newline at end of file + storeWorkspaceReturnPath(cookies, url); + throw redirect(303, getWorkspaceLoginUrl()); +} diff --git a/frontend/src/routes/auth/callback/+page.server.ts b/frontend/src/routes/auth/callback/+page.server.ts index 42cade0..da1b217 100644 --- a/frontend/src/routes/auth/callback/+page.server.ts +++ b/frontend/src/routes/auth/callback/+page.server.ts @@ -1,132 +1,15 @@ -import { redirect, isRedirect } from '@sveltejs/kit'; +import { redirect } 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 || 'app-backend'; - const KEYCLOAK_CLIENT_SECRET = process.env.KEYCLOAK_CLIENT_SECRET || ''; - - // La redirect_uri debe coincidir exactamente con la registrada en Keycloak. - // resolveSystemBaseUrl corrige el caso donde url.origin es localhost porque - // ORIGIN env var apunta a localhost en producción (usa SITE_URL como fallback). - const { resolveSystemBaseUrl } = await import('$lib/server/workspace-auth'); - const redirectUri = `${resolveSystemBaseUrl(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 - }); - } - - if (tokens.id_token) { - cookies.set('id_token', tokens.id_token, { - path: '/', - httpOnly: true, - secure: isProduction, - sameSite: 'lax', - maxAge: 60 * 60 * 24 * 7 - }); - } - - // 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)); - } +/** + * Callback OIDC — OBSOLETO. + * + * El CRM ya no inicia flujo de autorización contra Keycloak: el login entra por + * el App Launcher del Workspace (relay → /auth/sso → Hub /sso-exchange). Esta + * ruta se conserva solo para no romper enlaces viejos; cualquier acceso se + * redirige al dashboard (el layout valida la sesión y, si no hay, reenvía al + * Workspace). No se intercambia ningún `code` con Keycloak. + */ +export const load: PageServerLoad = async () => { + throw redirect(303, '/dashboard'); }; diff --git a/frontend/src/routes/auth/post-logout/+server.ts b/frontend/src/routes/auth/post-logout/+server.ts index 5c3ba8d..1dc6ec6 100644 --- a/frontend/src/routes/auth/post-logout/+server.ts +++ b/frontend/src/routes/auth/post-logout/+server.ts @@ -3,11 +3,10 @@ import type { RequestHandler } from './$types'; import { getWorkspaceLoginUrl } from '$lib/server/workspace-auth'; /** - * KC redirects here after completing the logout flow. - * This URL is covered by the app's registered wildcard in KC (e.g. mi-app.dominio.com/*). - * We then send the user to workspace login so it can apply myApps() launcher logic. + * Ruta de retorno post-logout. El CRM ya no dispara logout contra Keycloak + * (el cierre completo se hace desde el Workspace); se conserva por compatibilidad + * y redirige al App Launcher del Workspace. */ -export const GET: RequestHandler = async ({ request, url }) => { - const systemBaseUrl = url.origin; - throw redirect(303, getWorkspaceLoginUrl(systemBaseUrl, { forPostLogout: true })); +export const GET: RequestHandler = async () => { + throw redirect(303, getWorkspaceLoginUrl()); }; diff --git a/frontend/src/routes/join/+page.server.ts b/frontend/src/routes/join/+page.server.ts index bd58d8b..db0690f 100644 --- a/frontend/src/routes/join/+page.server.ts +++ b/frontend/src/routes/join/+page.server.ts @@ -1,7 +1,7 @@ import { redirect, fail } from '@sveltejs/kit'; import type { PageServerLoad, Actions } from './$types'; import { getServerApiUrl, getAuthTokens } from '$lib/server/api'; -import { redirectToKeycloakLogin } from '$lib/server/workspace-auth'; +import { redirectToWorkspaceLogin } from '$lib/server/workspace-auth'; export const load: PageServerLoad = async ({ url, cookies, fetch }) => { const code = url.searchParams.get('code')?.toUpperCase().trim() ?? ''; @@ -13,7 +13,7 @@ export const load: PageServerLoad = async ({ url, cookies, fetch }) => { if (!accessToken) { // Sesión KC expiró entre redirecciones — volver a auth - redirectToKeycloakLogin(url.origin, `/join?code=${code}&step=consume`); + redirectToWorkspaceLogin(cookies, url); } const apiUrl = getServerApiUrl(); @@ -76,7 +76,7 @@ export const actions: Actions = { if (!accessToken) { // Redirigir a Keycloak; al volver, el callback irá a /join?code=XXX&step=consume - redirectToKeycloakLogin(url.origin, `/join?code=${code}&step=consume`); + redirectToWorkspaceLogin(cookies, url); } // Si ya hay sesión, consumir directamente vía redirect a step=consume diff --git a/frontend/src/routes/login/+page.server.ts b/frontend/src/routes/login/+page.server.ts index 66d5d5f..ff64071 100644 --- a/frontend/src/routes/login/+page.server.ts +++ b/frontend/src/routes/login/+page.server.ts @@ -4,12 +4,9 @@ import type { Actions, PageServerLoad } from './$types'; import { clearAuthTokens, getAuthTokens } from '$lib/server/api'; import { setAccessTokenCookies } from '$lib/server/access-token-cookie'; import { - getWorkspaceLoginUrl, readWorkspaceReturnPath, clearWorkspaceReturnPath, - storeReturnPath, - redirectToKeycloakAuthorization, - redirectToKeycloakLogin, + redirectToWorkspaceLogin, getHubBackendUrl, isSecureContext, } from '$lib/server/workspace-auth'; @@ -65,29 +62,17 @@ export const load: PageServerLoad = async ({ cookies, url }) => { clearAuthTokens(cookies); + // Vuelta desde el Workspace tras autenticarse: el CRM NO inicia ningún flujo + // OIDC contra Keycloak. La sesión se obtiene por relay del App Launcher + // (→ /auth/sso). Si el usuario llega aquí ya autenticado en el Hub, se le + // manda al dashboard; si no hay sesión local, el layout lo reenvía al + // Workspace (App Launcher) para re-entrar por relay. if (url.searchParams.get('sso_verified') === '1') { - const existingReturnPath = readWorkspaceReturnPath(cookies, ''); - const intendedPath = - existingReturnPath && existingReturnPath !== '/login' - ? existingReturnPath - : (url.searchParams.get('redirect') || '/dashboard'); - storeReturnPath(cookies, intendedPath); - redirectToKeycloakAuthorization(url.origin, intendedPath); + throw redirect(303, '/dashboard'); } - const redirectParam = url.searchParams.get('redirect'); - if (redirectParam) { - const existingReturnPath = readWorkspaceReturnPath(cookies, ''); - const intendedPath = - existingReturnPath && existingReturnPath !== '/login' - ? existingReturnPath - : redirectParam; - storeReturnPath(cookies, intendedPath); - redirectToKeycloakLogin(url.origin, intendedPath); - } - - storeReturnPath(cookies, '/dashboard'); - throw redirect(303, getWorkspaceLoginUrl(url.origin)); + // Sin sesión → App Launcher del Workspace (relay). Nunca Keycloak directo. + redirectToWorkspaceLogin(cookies, url); }; export const actions: Actions = { diff --git a/frontend/src/routes/logout/+server.ts b/frontend/src/routes/logout/+server.ts index 8e692e7..4ccba83 100644 --- a/frontend/src/routes/logout/+server.ts +++ b/frontend/src/routes/logout/+server.ts @@ -1,37 +1,28 @@ import { redirect } from '@sveltejs/kit'; import { env } from '$env/dynamic/private'; import type { RequestHandler } from './$types'; -import { clearAccessTokenCookies } from '$lib/server/access-token-cookie'; -import { - buildKeycloakLogoutUrl, - clearWorkspaceReturnPath, - getWorkspaceLoginUrl -} from '$lib/server/workspace-auth'; +import { clearAuthTokens } from '$lib/server/api'; +import { clearWorkspaceReturnPath, getWorkspaceLoginUrl } from '$lib/server/workspace-auth'; -export const POST: RequestHandler = async ({ cookies, url }) => { - const systemBaseUrl = url.origin; - - const idToken = cookies.get('id_token'); - - // Eliminar todas las cookies de autenticación - clearAccessTokenCookies(cookies); - cookies.delete('refresh_token', { path: '/' }); +/** + * Logout del CRM. Limpia la sesión LOCAL (cookies) y devuelve al Workspace. + * NO habla directo a Keycloak: el cierre de sesión completo (Hub/KC) se hace + * desde el Workspace. Patrón SIWEB. + */ +export const POST: RequestHandler = async ({ cookies }) => { + // Eliminar todas las cookies de autenticación (incluye sesión local + token KC) + clearAuthTokens(cookies); cookies.delete('id_token', { path: '/' }); - cookies.delete('active_company_id', { path: '/' }); - cookies.delete('active_system', { path: '/' }); cookies.delete('sso_tenant_id', { path: '/' }); cookies.delete('sso_tenant_pub', { path: '/' }); clearWorkspaceReturnPath(cookies); - // En modo local no hay Keycloak ni Hub — ir directo al login local. + // Modo local: no hay Workspace — ir al login local. if ((env.DEV_LOCAL_AUTH ?? '').toLowerCase() === 'true') { throw redirect(303, '/login'); } - // Sin id_token_hint KC rechaza post_logout_redirect_uri no registrado. - if (!idToken) { - throw redirect(303, getWorkspaceLoginUrl(systemBaseUrl, { forPostLogout: true })); - } - - throw redirect(303, buildKeycloakLogoutUrl(systemBaseUrl, idToken)); + // Volver al Workspace (App Launcher). Para cerrar la sesión del Hub por + // completo, el usuario cierra sesión desde el Workspace. + throw redirect(303, getWorkspaceLoginUrl()); };