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: '/' });