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>
198 lines
6.3 KiB
TypeScript
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()}`;
|
|
} |