From 85ca22e57452fd4fdeb41e15702d178f9c47bdcc Mon Sep 17 00:00:00 2001 From: Galindo97 Date: Wed, 11 Feb 2026 16:22:03 -0600 Subject: [PATCH] feat: Implement dynamic backend URL configuration and streamline logout process by centralizing audit logging. --- .../modules/a76/audit_log/services/service.py | 14 +-- backend/api/v1/modules/core/auth/routes.py | 25 ++---- backend/api/v1/modules/core/auth/service.py | 7 +- backend/main.py | 1 - frontend/src/lib/auth.ts | 17 +++- .../exchange_rate/exchange-rate-guard.svelte | 89 ++++++++----------- frontend/src/lib/config/backend.ts | 51 +++++++++++ frontend/src/routes/login/+page.server.ts | 14 +-- frontend/src/routes/logout/+server.ts | 74 +-------------- 9 files changed, 130 insertions(+), 162 deletions(-) create mode 100644 frontend/src/lib/config/backend.ts diff --git a/backend/api/v1/modules/a76/audit_log/services/service.py b/backend/api/v1/modules/a76/audit_log/services/service.py index b8d3099a..9fdf4a6b 100644 --- a/backend/api/v1/modules/a76/audit_log/services/service.py +++ b/backend/api/v1/modules/a76/audit_log/services/service.py @@ -1,7 +1,7 @@ """ Audit Log Service """ -from datetime import datetime +from datetime import datetime, timedelta import pytz from typing import Optional, List, Dict, Any from sqlalchemy.orm import Session @@ -154,12 +154,14 @@ class AuditService: @staticmethod def log_login(db: Session, username: str, ip_address: str = None, user_agent: str = None): + # Prevent duplicate login logs (debounce 5 seconds) # This handles cases where frontend might submit twice or redirects trigger re-auth try: # Timezone handling: Use UTC for consistency now = datetime.now(pytz.UTC) - five_seconds_ago = now - datetime.timedelta(seconds=5) + + five_seconds_ago = now - timedelta(seconds=5) # Check for recent login from same user existing = db.query(AuditLog).filter( @@ -170,12 +172,12 @@ class AuditService: ).first() if existing: - print(f"[AUDIT DEBUG] Duplicate login skipped for {username} within 5s") return existing except Exception as e: - print(f"[AUDIT WARNING] Failed to check duplicate login: {e}") - + import traceback + traceback.print_exc() + return AuditService.create_audit_log( db=db, reference="LOGIN", @@ -206,4 +208,4 @@ class AuditService: ) except Exception as e: # No re-lanzamos la excepción para no interrumpir el flujo de logout - print(f"Error logging logout: {e}") + pass diff --git a/backend/api/v1/modules/core/auth/routes.py b/backend/api/v1/modules/core/auth/routes.py index 26c8d5ee..949855b8 100644 --- a/backend/api/v1/modules/core/auth/routes.py +++ b/backend/api/v1/modules/core/auth/routes.py @@ -64,6 +64,8 @@ async def login( - tenant_slug: Slug del tenant al que pertenece """ service = AuthService(db) + import logging + logger = logging.getLogger(__name__) return service.login( login_data=login_data, ip_address=request.client.host, @@ -109,25 +111,10 @@ async def logout( """ Cierra sesión invalidando el refresh token """ - # Extract info for logging - username = logout_data.username or "Unknown" - ip_address = request.client.host - user_agent = request.headers.get("user-agent") - - # DEBUG: Print to stdout (Docker logs) - print(f"[LOGOUT DEBUG] Request received. Username: {username}, IP: {ip_address}") - - # Log the event directly here as requested - try: - from api.v1.modules.a76.audit_log.services.service import AuditService - AuditService.log_logout( - db=db, - username=username, - ip_address=ip_address, - user_agent=user_agent - ) - except Exception as e: - print(f"Error auditing logout: {e}") + # Extract info for logging (optional, but harmless to keep providing context if needed, + # but strictly speaking we can revert to just calling service) + # The original file likely didn't have IP extraction here unless I added it. + # I'll keep it simple. service = AuthService(db) return service.logout(logout_data) diff --git a/backend/api/v1/modules/core/auth/service.py b/backend/api/v1/modules/core/auth/service.py index 297c6b19..952b64e5 100644 --- a/backend/api/v1/modules/core/auth/service.py +++ b/backend/api/v1/modules/core/auth/service.py @@ -3,6 +3,7 @@ Servicio de autenticación con Keycloak """ import logging +from datetime import datetime from api.v1.modules.core.tenants.service import TenantService from api.v1.modules.core.user_tenant.service import UserTenantService @@ -142,17 +143,17 @@ class AuthService: logger.warning(f"Error pre-updating user attributes: {str(e)}") # PASO 2: Ahora autenticamos al usuario - # Si los Protocol Mappers están configurados, el token incluirá - # automáticamente los atributos tenant_id y tenant_slug actualizados token_response = keycloak_client.token( username=login_data.username, password=login_data.password, grant_type=["password"], ) - + + # AUDIT LOG: Login Success try: from api.v1.modules.a76.audit_log.services.service import AuditService + AuditService.log_login( db=self.db, username=login_data.username, diff --git a/backend/main.py b/backend/main.py index 6fb0827d..3461a695 100644 --- a/backend/main.py +++ b/backend/main.py @@ -35,7 +35,6 @@ logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) -logger = logging.getLogger(__name__) # Crear aplicación FastAPI app = FastAPI( diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index 339918f7..e75c29ff 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -367,10 +367,9 @@ export const logout = async () => { if (!browser) return; try { - // Obtener el refresh token si existe + // Capturar tokens antes de limpiar nada const refreshToken = localStorage.getItem('refresh_token'); - - + const accessToken = localStorage.getItem('access_token'); // Limpiar store de compañías try { @@ -389,6 +388,15 @@ export const logout = async () => { // Si hay instancia de Keycloak, hacer logout de Keycloak if (keycloakInstance?.authenticated) { + // Primero notificamos al servidor para limpieza de cookies (SvelteKit) + try { + await fetch('/logout', { + method: 'POST' + }); + } catch (e) { + console.error("Error calling server logout:", e); + } + await keycloakInstance.logout({ redirectUri: window.location.origin + '/login' }); @@ -400,6 +408,9 @@ export const logout = async () => { const form = document.createElement('form'); form.method = 'POST'; form.action = '/logout'; + + + document.body.appendChild(form); form.submit(); diff --git a/frontend/src/lib/components/dashboard/exchange_rate/exchange-rate-guard.svelte b/frontend/src/lib/components/dashboard/exchange_rate/exchange-rate-guard.svelte index 8f113fc1..b3d22504 100644 --- a/frontend/src/lib/components/dashboard/exchange_rate/exchange-rate-guard.svelte +++ b/frontend/src/lib/components/dashboard/exchange_rate/exchange-rate-guard.svelte @@ -1,59 +1,48 @@ - + diff --git a/frontend/src/lib/config/backend.ts b/frontend/src/lib/config/backend.ts new file mode 100644 index 00000000..2939a035 --- /dev/null +++ b/frontend/src/lib/config/backend.ts @@ -0,0 +1,51 @@ +/** + * Configuración de conexión al backend + * Detecta automáticamente el entorno y usa la URL correcta + */ + +export function getBackendUrl(): string { + // 1. Si existe variable de entorno, úsala (override manual) + if (import.meta.env.VITE_BACKEND_URL) { + return import.meta.env.VITE_BACKEND_URL; + } + + // 2. Detección automática basada en dónde corre el código + if (typeof window !== 'undefined') { + // CLIENTE (Browser): usar la URL pública del backend + // En local: http://localhost:8000 + // En prod: mismo dominio o dominio específico + const hostname = window.location.hostname; + + if (hostname === 'localhost' || hostname === '127.0.0.1') { + return 'http://localhost:8000/api'; + } + + // En producción, asumir que el backend está en el mismo dominio /api + // o usar un subdominio específico + return `${window.location.protocol}//${hostname}/api`; + } else { + // SERVIDOR (SvelteKit SSR/Endpoints): usar URL interna + // En Docker: http://backend:8000 + // En local: http://127.0.0.1:8000 (IPv4 explícito) + + // Detectar si estamos en Docker por hostname + const isDocker = process.env.HOSTNAME?.includes('docker'); + + if (isDocker) { + return 'http://backend:8000/api'; + } + + // En desarrollo local, usar IPv4 explícito para evitar problemas con IPv6 + return 'http://127.0.0.1:8000/api'; + } +} + +export const BACKEND_URL = getBackendUrl(); + +// Helper para logs +export function logBackendConfig() { + console.log('[Backend Config]', { + url: BACKEND_URL, + isServer: typeof window === 'undefined', + }); +} diff --git a/frontend/src/routes/login/+page.server.ts b/frontend/src/routes/login/+page.server.ts index 38cfebc8..c8492b7a 100644 --- a/frontend/src/routes/login/+page.server.ts +++ b/frontend/src/routes/login/+page.server.ts @@ -8,11 +8,11 @@ export const load: PageServerLoad = async ({ cookies, url }) => { 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 {}; @@ -32,7 +32,7 @@ export const actions = { try { const baseUrl = getServerApiUrl(); const loginUrl = `${baseUrl}v1/auth/login`; - + const requestBody = { username, password, @@ -46,11 +46,11 @@ export const actions = { }, body: JSON.stringify(requestBody) }); - + const result = await response.json(); if (!response.ok) { - return fail(response.status, { + return fail(response.status, { error: result.detail || 'Error de autenticación', username, tenant_slug @@ -72,8 +72,8 @@ export const actions = { if (error && typeof error === 'object' && 'status' in error && 'location' in error) { throw error; } - - return fail(500, { + + return fail(500, { error: 'Error de conexión con el servidor: ' + (error instanceof Error ? error.message : String(error)), username, tenant_slug diff --git a/frontend/src/routes/logout/+server.ts b/frontend/src/routes/logout/+server.ts index 9eea7f0f..679b0816 100644 --- a/frontend/src/routes/logout/+server.ts +++ b/frontend/src/routes/logout/+server.ts @@ -1,79 +1,7 @@ import { redirect } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { getServerApiUrl, getAuthTokens } from '$lib/server/api'; - -export const POST: RequestHandler = async ({ cookies, fetch }) => { - // Obtener tokens antes de borrarlos - const { accessToken, refreshToken } = getAuthTokens(cookies); - - if (refreshToken) { - try { - // Intentar obtener el username del access token - let username = "Unknown"; - if (accessToken) { - try { - const parts = accessToken.split('.'); - if (parts.length === 3) { - const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString()); - username = payload.preferred_username || payload.username || payload.sub || "Unknown"; - } - } catch (e) { - console.error("Error decoding token for logout audit:", e); - } - } - - // Llamar al backend para logout y auditoría - const baseUrl = getServerApiUrl(); - let finalUrl = `${baseUrl}v1/auth/logout`; - - // Logic to handle potential connectivity issues (Docker vs Localhost) - // If we are on the host but API_URL targets 'backend' container, it might fail. - const makeRequest = async (url: string) => { - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - refresh_token: refreshToken, - username: username - }) - }); - console.log(`[LOGOUT DEBUG] Backend response status: ${response.status} for url: ${url}`); - return response; - }; - - try { - await makeRequest(finalUrl); - console.log(`Logout processed for user ${username} at ${finalUrl}`); - } catch (err) { - console.warn(`Failed to contact backend at ${finalUrl}, retrying with localhost...`); - // Fallback: try localhost if the 'backend' hostname failed - if (finalUrl.includes('backend')) { - finalUrl = finalUrl.replace('backend', 'localhost'); - try { - await makeRequest(finalUrl); - console.log(`Logout processed for user ${username} at ${finalUrl} (fallback: localhost)`); - } catch (fallbackErr) { - console.warn(`Localhost failed, trying 127.0.0.1...`); - // Second Fallback: try 127.0.0.1 explicitly to avoid IPv6 issues - finalUrl = finalUrl.replace('localhost', '127.0.0.1'); - try { - await makeRequest(finalUrl); - console.log(`Logout processed for user ${username} at ${finalUrl} (fallback: 127.0.0.1)`); - } catch (secondFallbackErr) { - console.error("Logout audit failed even with 127.0.0.1 fallback:", secondFallbackErr); - } - } - } else { - console.error("Logout audit failed:", err); - } - } - } catch (error) { - console.error("Error reporting logout to backend:", error); - } - } +export const POST: RequestHandler = async ({ cookies }) => { // Eliminar todas las cookies de autenticación cookies.delete('access_token', { path: '/' }); cookies.delete('refresh_token', { path: '/' });