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(/\/+$/, ''); } function isInternalOnlyHost(rawUrl: string): boolean { try { const parsed = new URL(rawUrl); const host = parsed.hostname.toLowerCase(); return host === 'host.docker.internal' || host === 'backend' || host === 'hub-keycloak'; } 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 (!isInternalOnlyHost(candidate)) { return stripTrailingSlashes(candidate); } } return DEFAULT_WORKSPACE_BASE_URL; } 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 Anexo76, 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(); if (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 || 'hub-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(); const redirectUri = `${systemBaseUrl}/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(); const redirectUri = `${systemBaseUrl}/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 { 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)); } 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()}`; }