Files
plantillas-proyectos/frontend/src/lib/server/workspace-auth.ts
Galindo97 557f904af0 fix(auth): cambiar client_id de Keycloak de anexo76-frontend a hub-frontend
Anexo76 no es independiente del Hub/Workspace — comparten el mismo
cliente de Keycloak (hub-frontend). anexo76-frontend no existe en KC.

- Dockerfile.prod: agrega ARG/ENV VITE_KEYCLOAK_CLIENT_ID=hub-frontend
- workspace-auth.ts: actualiza fallback de 'anexo76-frontend' a 'hub-frontend'
- .env.example: actualiza a hub-frontend
- Jenkinsfile: pasa --build-arg VITE_KEYCLOAK_CLIENT_ID=hub-frontend al build

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 15:49:28 -05:00

198 lines
6.3 KiB
TypeScript

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()}`;
}