From 52577b9e6663bad0852f6bf4e41d25a6e22df360 Mon Sep 17 00:00:00 2001 From: Galindo97 Date: Mon, 11 May 2026 17:17:37 -0500 Subject: [PATCH 1/2] refactor: update SSO handling and remove deprecated components - Changed the hub-net network configuration to external in docker-compose. - Removed the Single Sign-On (SSO) service implementation and associated login form component. - Enhanced authentication callback logic to improve error handling and redirect management. - Updated various routes to streamline login and authentication processes, ensuring proper redirection to workspace login. - Cleaned up unused code and improved overall structure for better maintainability. --- docker-compose.yml | 2 +- frontend/src/lib/components/login-form.svelte | 383 ------------------ frontend/src/lib/server/workspace-auth.ts | 157 +++++++ frontend/src/lib/sso.ts | 139 ------- frontend/src/routes/+page.server.ts | 2 +- .../src/routes/auth/callback/+page.server.ts | 32 +- frontend/src/routes/auth/sso/+page.server.ts | 39 +- .../src/routes/dashboard/+layout.server.ts | 20 +- .../routes/dashboard/invoices/+page.server.ts | 6 +- frontend/src/routes/login/+page.server.ts | 110 ++--- frontend/src/routes/login/+page.svelte | 10 +- frontend/src/routes/logout/+server.ts | 30 +- 12 files changed, 266 insertions(+), 664 deletions(-) delete mode 100644 frontend/src/lib/components/login-form.svelte create mode 100644 frontend/src/lib/server/workspace-auth.ts delete mode 100644 frontend/src/lib/sso.ts diff --git a/docker-compose.yml b/docker-compose.yml index 28b35572..7b232c1b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -313,5 +313,5 @@ networks: driver: bridge hub-net: - external: false + external: true name: aduanasoft-hub_default diff --git a/frontend/src/lib/components/login-form.svelte b/frontend/src/lib/components/login-form.svelte deleted file mode 100644 index cf51a8be..00000000 --- a/frontend/src/lib/components/login-form.svelte +++ /dev/null @@ -1,383 +0,0 @@ - - -
- - - -
{ - // Interceptar solo en el paso 1 antes de hacer el submit real - if (!readyToSubmit) { - cancel(); - loading = true; - tenants = await fetchTenants(); - loading = false; - if (tenants.length === 1) { - // 1 sola org: login directo - tenantSlug = tenants[0].slug; - readyToSubmit = true; - await tick(); // esperar a que el DOM refleje tenantSlug antes de enviar - formEl?.requestSubmit(); - } else if (tenants.length > 1) { - // Varias orgs: mostrar selector - step = 2; - } else if (discoveryError) { - // Error claro del Hub (sin licencia, credenciales inválidas, etc.) - // No hacer submit — el error ya se muestra en discoveryError - } else { - // 0 orgs sin error: enviar igual, el backend rechazará - readyToSubmit = true; - await tick(); - formEl?.requestSubmit(); - } - return; - } - loading = true; - return async ({ update, result }) => { - await update({ reset: false }); - loading = false; - readyToSubmit = false; - if (result.type === 'failure') { - clearClientCookies(); - step = 1; - } - }; - }} - > - - -
- Anexo 76 -
-

Anexo 76

-

- Sistema de Cumplimiento Fiscal y Aduanal -

-
-
- - {#if error} -
- - - - {error} -
- {/if} - - - - {#if step === 2} - - - {/if} - - {#if step === 1} - - - Usuario - - - - - - - - - - - - -
-
- o continúa con -
-
- - - - - - -

- ¿No tienes cuenta?{' '} - - Regístrate - -

- {:else} - -
-

Selecciona tu organización

-

Tu cuenta tiene acceso a varias organizaciones

-
- -
- {#each tenants as t} - - {/each} -
- - - - - - - {/if} -
-
- - - -
-
-

- Al continuar, aceptas nuestros - Términos de Servicio - y - Política de Privacidad. -

-
diff --git a/frontend/src/lib/server/workspace-auth.ts b/frontend/src/lib/server/workspace-auth.ts new file mode 100644 index 00000000..317b045a --- /dev/null +++ b/frontend/src/lib/server/workspace-auth.ts @@ -0,0 +1,157 @@ +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'; + +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 points to /login so that after Workspace auth the browser lands on + // /login, which immediately attempts a prompt=none KC auth. + const loginUrl = `${systemBaseUrl}/login`; + 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: env.NODE_ENV === 'production', + 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 || 'anexo76-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: env.NODE_ENV === 'production', + 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()}`; +} + +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 buildKeycloakLogoutUrl(systemBaseUrl: string): string { + const keycloakBaseUrl = getPublicKeycloakBaseUrl(); + const workspaceLoginUrl = getWorkspaceLoginUrl(systemBaseUrl, { forPostLogout: true }); + const params = new URLSearchParams({ + client_id: getKeycloakClientId(), + post_logout_redirect_uri: workspaceLoginUrl + }); + + return `${keycloakBaseUrl}/realms/${getKeycloakRealm()}/protocol/openid-connect/logout?${params.toString()}`; +} \ No newline at end of file diff --git a/frontend/src/lib/sso.ts b/frontend/src/lib/sso.ts deleted file mode 100644 index cb67ee25..00000000 --- a/frontend/src/lib/sso.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Servicio de Single Sign-On (SSO) con proveedores externos - */ -import { browser } from '$app/environment'; - -// Tipos de proveedores SSO soportados -export type SSOProvider = 'microsoft' | 'google' | 'github'; - -/** - * Inicia el flujo de autenticación con un proveedor SSO - * @param provider - El proveedor SSO a utilizar - */ -export const loginWithProvider = async (provider: SSOProvider): Promise => { - if (!browser) { - return; - } - - try { - // Construir la URL de redirección al proveedor SSO - const keycloakUrl = import.meta.env.VITE_KEYCLOAK_URL; - const realm = import.meta.env.VITE_KEYCLOAK_REALM; - const clientId = import.meta.env.VITE_KEYCLOAK_CLIENT_ID; - - // Validar que las variables de entorno estén configuradas - if (!keycloakUrl || !realm || !clientId) { - const missing = []; - if (!keycloakUrl) missing.push('VITE_KEYCLOAK_URL'); - if (!realm) missing.push('VITE_KEYCLOAK_REALM'); - if (!clientId) missing.push('VITE_KEYCLOAK_CLIENT_ID'); - - const errorMsg = `Configuración de Keycloak incompleta. Faltan las siguientes variables de entorno: ${missing.join(', ')}. Por favor, verifica tu archivo .env y reinicia el servidor de desarrollo.`; - console.error(errorMsg); - alert(errorMsg); - throw new Error(errorMsg); - } - - const redirectUri = encodeURIComponent(window.location.origin + '/auth/callback'); - - // URL de login de Keycloak con el provider específico - const loginUrl = `${keycloakUrl}/realms/${realm}/protocol/openid-connect/auth?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&scope=openid&kc_idp_hint=${provider}`; - - // Redirigir al usuario al proveedor SSO - window.location.href = loginUrl; - } catch (error) { - console.error(`Error al iniciar login con ${provider}:`, error); - throw error; - } -}; - -/** - * Obtiene la lista de proveedores SSO disponibles - * Esta función podría consultar a Keycloak para obtener los providers configurados - */ -export const getAvailableProviders = async (): Promise => { - // Por ahora retornamos una lista estática - // En producción, esto debería consultarse desde Keycloak - return ['microsoft', 'google', 'github']; -}; - -/** - * Obtiene la configuración de visualización para un proveedor - */ -export const getProviderConfig = (provider: SSOProvider) => { - const configs = { - microsoft: { - name: 'Microsoft', - icon: '🪟', - color: 'bg-blue-600 hover:bg-blue-700' - }, - google: { - name: 'Google', - icon: '🔍', - color: 'bg-red-600 hover:bg-red-700' - }, - github: { - name: 'GitHub', - icon: '🐙', - color: 'bg-gray-800 hover:bg-gray-900' - } - }; - - return configs[provider]; -}; - -/** - * Intercambia el código de autorización por tokens - */ -export const exchangeCodeForTokens = async ( - code: string, - redirectUri: string -): Promise<{ access_token: string; refresh_token: string; id_token?: string }> => { - try { - const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000/api/'; - const baseUrl = API_BASE_URL.endsWith('/') ? API_BASE_URL : `${API_BASE_URL}/`; - - const response = await fetch(`${baseUrl}v1/auth/exchange-code`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - code, - redirect_uri: redirectUri - }) - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.detail || 'Error intercambiando código por tokens'); - } - - return await response.json(); - } catch (error) { - console.error('Error en exchangeCodeForTokens:', error); - throw error; - } -}; - -/** - * Decodifica un JWT (sin verificar la firma) - * NOTA: Esta es una decodificación simple para obtener los claims. - * La verificación de la firma debe hacerse en el backend. - */ -export const decodeJWT = (token: string): any => { - try { - const parts = token.split('.'); - if (parts.length !== 3) { - throw new Error('Token JWT inválido'); - } - - // Decodificar la parte del payload (segunda parte) - const payload = parts[1]; - const decodedPayload = atob(payload.replace(/-/g, '+').replace(/_/g, '/')); - return JSON.parse(decodedPayload); - } catch (error) { - console.error('Error decodificando JWT:', error); - return null; - } -}; diff --git a/frontend/src/routes/+page.server.ts b/frontend/src/routes/+page.server.ts index 36adcfa4..ba19381a 100644 --- a/frontend/src/routes/+page.server.ts +++ b/frontend/src/routes/+page.server.ts @@ -32,7 +32,7 @@ export const load: PageServerLoad = async ({ cookies, fetch }) => { clearAuthTokens(cookies); } } - + // Si no está autenticado, mostrar la página principal pública return { isAuthenticated: false diff --git a/frontend/src/routes/auth/callback/+page.server.ts b/frontend/src/routes/auth/callback/+page.server.ts index 13e5e20a..d6d3e80b 100644 --- a/frontend/src/routes/auth/callback/+page.server.ts +++ b/frontend/src/routes/auth/callback/+page.server.ts @@ -1,6 +1,12 @@ -import { redirect } from '@sveltejs/kit'; +import { redirect, isRedirect } 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 @@ -10,13 +16,24 @@ export const load: PageServerLoad = async ({ url, cookies, fetch }) => { const errorDescription = url.searchParams.get('error_description'); if (errorParam) { - console.error('❌ [Callback Server] Error en autenticación:', errorParam, errorDescription); - throw redirect(303, `/login?error=${encodeURIComponent(errorDescription || 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, '/login?error=No se recibió código de autorización'); + throw redirect(303, getWorkspaceLoginUrl(url.origin)); } try { @@ -78,7 +95,7 @@ export const load: PageServerLoad = async ({ url, cookies, fetch }) => { } // Obtener la URL de redirección del state o ir al dashboard - let redirectTo = '/dashboard'; + let redirectTo = readWorkspaceReturnPath(cookies, '/dashboard'); if (state) { try { const stateObj = JSON.parse(state); @@ -87,12 +104,15 @@ export const load: PageServerLoad = async ({ url, cookies, fetch }) => { 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, `/login?error=${encodeURIComponent(err.message || 'Error procesando autenticación')}`); + throw redirect(303, getWorkspaceLoginUrl(url.origin)); } }; diff --git a/frontend/src/routes/auth/sso/+page.server.ts b/frontend/src/routes/auth/sso/+page.server.ts index 4c045a9a..9cced10e 100644 --- a/frontend/src/routes/auth/sso/+page.server.ts +++ b/frontend/src/routes/auth/sso/+page.server.ts @@ -7,6 +7,7 @@ import { redirect } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; import { setAccessTokenCookies } from '$lib/server/access-token-cookie'; +import { redirectToWorkspaceLogin } from '$lib/server/workspace-auth'; // Disable client-side rendering to prevent SvelteKit from making a second // __data.json request that would consume the one-time relay token twice. @@ -17,7 +18,7 @@ export const load: PageServerLoad = async ({ url, cookies }) => { console.log('[SSO] relay token presente:', !!relayToken); if (!relayToken) { - throw redirect(303, '/login?error=sso_missing_token'); + redirectToWorkspaceLogin(cookies, url); } // Limpiar sesión anterior para que el nuevo usuario reciba sus propias cookies. @@ -51,7 +52,7 @@ export const load: PageServerLoad = async ({ url, cookies }) => { body: JSON.stringify({ relay_token: relayToken }), }); } catch (err) { - throw redirect(303, '/login?error=sso_hub_unreachable'); + redirectToWorkspaceLogin(cookies, url); } if (!response.ok) { @@ -72,10 +73,10 @@ export const load: PageServerLoad = async ({ url, cookies }) => { throw redirect(303, '/dashboard'); } - throw redirect(303, `/login?error=${encodeURIComponent(detail)}`); + redirectToWorkspaceLogin(cookies, url); } - const tokens = await response.json(); + let tokens = await response.json(); console.log('[SSO] exchange exitoso, tokens recibidos:', { hasAccessToken: !!tokens.access_token, accessTokenLen: tokens.access_token?.length, @@ -84,6 +85,36 @@ export const load: PageServerLoad = async ({ url, cookies }) => { tenant_slug: tokens.tenant_slug, }); + // ── Refresh proactivo ──────────────────────────────────────────────────── + // Los tokens del relay fueron emitidos por KC via el browser (iss=IP:8085). + // El Hub backend valida contra KC interno (hub-keycloak:8080) → issuer mismatch → 401. + // Refrescando aquí: Anexo76 backend → Hub → KC interno → iss=hub-keycloak:8080 → válido. + if (tokens.refresh_token) { + try { + const internalApiUrl = ( + process.env.INTERNAL_API_URL || + process.env.VITE_API_URL || + 'http://backend:8000/api/' + ).replace(/\/+$/, ''); + const refreshRes = await fetch(`${internalApiUrl}/v1/auth/refresh`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refresh_token: tokens.refresh_token }), + }); + if (refreshRes.ok) { + const refreshed = await refreshRes.json(); + if (refreshed.access_token && refreshed.refresh_token) { + tokens = { ...tokens, ...refreshed }; + console.log('[SSO] tokens refrescados exitosamente (iss normalizado)'); + } + } else { + console.warn('[SSO] refresh proactivo falló (status', refreshRes.status, ') — usando tokens originales del relay'); + } + } catch (err) { + console.warn('[SSO] refresh proactivo error (non-blocking):', err); + } + } + const isProduction = process.env.NODE_ENV === 'production'; console.log('[SSO] NODE_ENV:', process.env.NODE_ENV, '→ isProduction:', isProduction); diff --git a/frontend/src/routes/dashboard/+layout.server.ts b/frontend/src/routes/dashboard/+layout.server.ts index c0756033..68bdd38c 100644 --- a/frontend/src/routes/dashboard/+layout.server.ts +++ b/frontend/src/routes/dashboard/+layout.server.ts @@ -7,21 +7,22 @@ import { getUserCompanies, clearAuthTokens } from '$lib/server/api'; +import { + redirectToWorkspaceLogin +} from '$lib/server/workspace-auth'; export const load: LayoutServerLoad = async ({ cookies, url, fetch }) => { // Verificar si existe el token en las cookies const { accessToken } = getAuthTokens(cookies); console.log('[dashboard layout] access_token presente:', !!accessToken, '| url:', url.pathname); - // Si no hay token, redirigir al login, pero excluir la ruta /login para evitar bucle - if (!accessToken && url.pathname !== '/login') { - const redirectUrl = `/login?redirect=${encodeURIComponent(url.pathname)}`; - throw redirect(303, redirectUrl); + if (!accessToken) { + redirectToWorkspaceLogin(cookies, url); } // Validar el token con el backend y obtener datos del usuario // La función validateAuth maneja automáticamente el refresh de tokens - const redirectOnFail = `/login?redirect=${encodeURIComponent(url.pathname)}`; + const redirectOnFail = undefined; try { // Primero my-companies: ejecuta get_current_user y puede crear tenant/empresa/usuario @@ -75,15 +76,8 @@ export const load: LayoutServerLoad = async ({ cookies, url, fetch }) => { throw error; } - // Si estamos ya en la página de login, no intentar redirigir de nuevo - if (url.pathname === '/login') { - console.error('🔐 [Dashboard] Error validando token en login page, limpiando cookies.'); - clearAuthTokens(cookies); - return { authenticated: false, error: error }; - } - // Para cualquier otro error (conexión, etc), limpiar token y redirigir clearAuthTokens(cookies); - throw redirect(303, redirectOnFail); + redirectToWorkspaceLogin(cookies, url); } }; diff --git a/frontend/src/routes/dashboard/invoices/+page.server.ts b/frontend/src/routes/dashboard/invoices/+page.server.ts index 12ce133f..aabc158e 100644 --- a/frontend/src/routes/dashboard/invoices/+page.server.ts +++ b/frontend/src/routes/dashboard/invoices/+page.server.ts @@ -21,7 +21,7 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => { const accessToken = tokens.accessToken; if (!accessToken) { - throw redirect(302, '/auth/login'); + throw redirect(302, '/login'); } try { @@ -75,14 +75,14 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => { {}, cookies, fetch, - '/auth/login' + '/login' ), authenticatedFetch( 'v1/public/reference_data/invoice-types?page=1&page_size=100', {}, cookies, fetch, - '/auth/login' + '/login' ) ]); diff --git a/frontend/src/routes/login/+page.server.ts b/frontend/src/routes/login/+page.server.ts index a96b687a..3b159fa0 100644 --- a/frontend/src/routes/login/+page.server.ts +++ b/frontend/src/routes/login/+page.server.ts @@ -1,85 +1,37 @@ -import { redirect, fail } from '@sveltejs/kit'; -import type { Actions, PageServerLoad } from './$types'; -import { clearAuthTokens, setAuthTokens, getServerApiUrl } from '$lib/server/api'; - - +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +import { clearAuthTokens } from '$lib/server/api'; +import { + getWorkspaceLoginUrl, + readWorkspaceReturnPath, + storeReturnPath, + redirectToKeycloakAuthorization +} from '$lib/server/workspace-auth'; export const load: PageServerLoad = async ({ cookies, url }) => { - // Si hay un parámetro 'logout' en la URL, limpiar las cookies - if (url.searchParams.has('logout')) { - clearAuthTokens(cookies); - return {}; - } - - // Limpiar siempre las cookies de sesión anterior al cargar login - // Esto evita que se queden datos del tenant anterior clearAuthTokens(cookies); - // Permitir acceso al login sin redirigir automáticamente - // Esto evita bucles de redirección cuando el token existe pero puede estar expirado - return {}; -}; + // Workspace redirige de vuelta aquí con ?sso_verified=1 después de que el usuario + // se autenticó en Workspace (que usa el mismo Keycloak central). + // En ese momento la sesión KC ya existe en el browser → prompt=none funciona sin + // mostrar ninguna pantalla de login. + if (url.searchParams.get('sso_verified') === '1') { + const existingReturnPath = readWorkspaceReturnPath(cookies, ''); + const intendedPath = + existingReturnPath && existingReturnPath !== '/login' + ? existingReturnPath + : (url.searchParams.get('redirect') || '/dashboard'); -export const actions = { - default: async ({ request, cookies, url, fetch }) => { - const data = await request.formData(); - const username = data.get('username')?.toString(); - const password = data.get('password')?.toString(); - const tenant_slug = data.get('tenant_slug')?.toString(); - - if (!username || !password || !tenant_slug) { - return fail(400, { error: 'Credenciales incorrectas' }); - } - - try { - const apiUrl = getServerApiUrl(); - const loginUrl = `${apiUrl}v1/auth/login`; - - const requestBody = { - username, - password, - tenant_slug - }; - - const response = await fetch(loginUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(requestBody) - }); - - const result = await response.json(); - - if (!response.ok) { - return fail(response.status, { - error: result.detail || 'Error de autenticación', - username, - tenant_slug - }); - } - - if (result.access_token) { - // Establecer tokens usando la función centralizada - setAuthTokens(cookies, result.access_token, result.refresh_token); - - // Redirigir al dashboard o a la URL original - const redirectUrl = url.searchParams.get('redirect') || '/dashboard'; - throw redirect(303, redirectUrl); - } - - return fail(500, { error: 'No se recibió token de autenticación' }); - } catch (error) { - // Si es un redirect de SvelteKit, re-lanzarlo - if (error && typeof error === 'object' && 'status' in error && 'location' in error) { - throw error; - } - - return fail(500, { - error: 'Error de conexión con el servidor: ' + (error instanceof Error ? error.message : String(error)), - username, - tenant_slug - }); - } + storeReturnPath(cookies, intendedPath); + redirectToKeycloakAuthorization(url.origin, intendedPath); } -} satisfies Actions; + + // Sin sso_verified → primera visita o sesión expirada. + // Guardar la ruta deseada y mandar al Workspace a autenticar. + const intendedPath = url.searchParams.get('redirect') || '/dashboard'; + if (intendedPath !== '/dashboard') { + storeReturnPath(cookies, intendedPath); + } + + throw redirect(303, getWorkspaceLoginUrl(url.origin)); +}; diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte index eabd843a..bb1b2ec5 100644 --- a/frontend/src/routes/login/+page.svelte +++ b/frontend/src/routes/login/+page.svelte @@ -1,9 +1 @@ - - -
-
- -
-
+ diff --git a/frontend/src/routes/logout/+server.ts b/frontend/src/routes/logout/+server.ts index fbf58b2f..0b0f6474 100644 --- a/frontend/src/routes/logout/+server.ts +++ b/frontend/src/routes/logout/+server.ts @@ -1,16 +1,10 @@ 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 } from '$lib/server/workspace-auth'; export const POST: RequestHandler = async ({ cookies, request }) => { - const refreshToken = cookies.get('refresh_token'); - - // Redirigir al workspace (Hub) — es el sistema central de autenticación. - const hubPublicUrl = (env.HUB_URL || '').replace(/\/+$/, ''); - const postLogoutUrl = hubPublicUrl - ? `${hubPublicUrl}/login` - : `${new URL(request.url).origin}/login`; + const systemBaseUrl = new URL(request.url).origin; // Eliminar todas las cookies de autenticación (access_token puede estar fragmentado) clearAccessTokenCookies(cookies); @@ -18,23 +12,7 @@ export const POST: RequestHandler = async ({ cookies, request }) => { cookies.delete('active_company_id', { path: '/' }); cookies.delete('sso_tenant_id', { path: '/' }); cookies.delete('sso_tenant_pub', { path: '/' }); + clearWorkspaceReturnPath(cookies); - // Llamar al Hub para revocar el refresh token (best-effort). - if (refreshToken) { - try { - const hubUrl = (env.INTERNAL_HUB_URL || env.HUB_URL || 'http://localhost:8001').replace(/\/+$/, ''); - await fetch(`${hubUrl}/api/v1/auth/logout`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - refresh_token: refreshToken, - post_logout_redirect_uri: postLogoutUrl, - }), - }); - } catch { - // Si falla la llamada al Hub, continuar de todos modos - } - } - - throw redirect(303, postLogoutUrl); + throw redirect(303, buildKeycloakLogoutUrl(systemBaseUrl)); }; From 42d7b876d1c5dbf13e9ef118a70254cfded4388f Mon Sep 17 00:00:00 2001 From: Galindo97 Date: Tue, 12 May 2026 09:19:39 -0500 Subject: [PATCH 2/2] feat: add environment variables for SMTP configuration and update hub-net to internal --- docker-compose.prod.yml | 10 ++++++++++ docker-compose.yml | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 45064db3..916ce963 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -180,6 +180,16 @@ services: - CENTRAL_SERVER_URL=${CENTRAL_SERVER_URL:-""} - SYNC_SECRET_TOKEN=${SYNC_SECRET_TOKEN:-change-this-sync-token-in-production} - SPOKE_URLS=${SPOKE_URLS:-""} + - HUB_URL=${HUB_URL:-https://workspace.aduanasoft.com} + - HUB_ADMIN_EMAIL=${HUB_ADMIN_EMAIL:-} + - HUB_ADMIN_PASSWORD=${HUB_ADMIN_PASSWORD:-} + - APP_PUBLIC_URL=${APP_PUBLIC_URL:-https://anexo76-dev.aduanasoft.com} + - SMTP_HOST=${SMTP_HOST:-smtp.gmail.com} + - SMTP_PORT=${SMTP_PORT:-587} + - SMTP_USER=${SMTP_USER:-} + - SMTP_PASSWORD=${SMTP_PASSWORD:-} + - SMTP_FROM_NAME=${SMTP_FROM_NAME:-Sistema Anexo76} + - SMTP_USE_TLS=${SMTP_USE_TLS:-true} - CSV_IMPORT_STORAGE=${CSV_IMPORT_STORAGE:-minio} - S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-http://minio:9000} - S3_ACCESS_KEY=${S3_ACCESS_KEY:-${MINIO_ROOT_USER:-minioadmin}} diff --git a/docker-compose.yml b/docker-compose.yml index 7b232c1b..28b35572 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -313,5 +313,5 @@ networks: driver: bridge hub-net: - external: true + external: false name: aduanasoft-hub_default