import { env } from '$env/dynamic/private'; import { redirect, type Cookies } from '@sveltejs/kit'; 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). */ export function isSecureContext(): boolean { const origin = (env.ORIGIN || process.env.ORIGIN || '').trim(); if (origin) return origin.startsWith('https://'); return process.env.NODE_ENV === 'production'; } function stripTrailingSlashes(value: string): string { return value.replace(/\/+$/, ''); } /** * 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. */ function isDevOnlyUrl(rawUrl: string): boolean { try { const parsed = new URL(rawUrl); const host = parsed.hostname.toLowerCase(); return ( host === 'localhost' || host === '127.0.0.1' || host === 'host.docker.internal' || host === 'backend' || host === 'hub-keycloak' || /^192\.168\./.test(host) || /^10\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host) ); } catch { return false; } } export function getWorkspaceBaseUrl(): string { const candidates = [ (env.VITE_HUB_URL || '').trim(), (env.HUB_URL || '').trim(), DEFAULT_WORKSPACE_BASE_URL ].filter(Boolean); for (const candidate of candidates) { if (!isDevOnlyUrl(candidate)) { return stripTrailingSlashes(candidate); } } return DEFAULT_WORKSPACE_BASE_URL; } /** * 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) */ 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(), (env.PUBLIC_URL || '').trim(), ]; for (const candidate of candidates) { if (candidate && !isDevOnlyUrl(candidate)) { return stripTrailingSlashes(candidate); } } // 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)}`; } export function storeReturnPath(cookies: Cookies, path: string): void { if (!path || !path.startsWith('/')) return; cookies.set(RETURN_PATH_COOKIE, path, { path: '/', httpOnly: true, sameSite: 'lax', secure: isSecureContext(), maxAge: 60 * 10 }); } 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'); const queryString = cleanParams.toString(); return queryString ? `${url.pathname}?${queryString}` : url.pathname; } export function storeWorkspaceReturnPath(cookies: Cookies, url: URL): string { const returnPath = getCleanReturnPath(url); cookies.set(RETURN_PATH_COOKIE, returnPath, { path: '/', httpOnly: true, sameSite: 'lax', secure: isSecureContext(), maxAge: 60 * 10 }); return returnPath; } export function readWorkspaceReturnPath(cookies: Cookies, fallbackPath: string): string { const storedReturnPath = cookies.get(RETURN_PATH_COOKIE); if (storedReturnPath && storedReturnPath.startsWith('/')) { return storedReturnPath; } return fallbackPath; } 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 = (env.HUB_BACKEND_URL || '').trim() || (env.INTERNAL_HUB_URL || '').trim(); if (direct) return stripTrailingSlashes(direct); 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); } return `${keycloakBaseUrl}/realms/${getKeycloakRealm()}/protocol/openid-connect/logout?${params.toString()}`; }