feat: Implement comprehensive audit logging for authentication events, including login, logout, user agent, and IP address, and add a script for audit verification.
This commit is contained in:
@@ -88,7 +88,34 @@ class AuditMapper:
|
||||
|
||||
# General fallbacks
|
||||
"clients_and_providers": "CATALOGS",
|
||||
"clients_and_providers": "CATALOGS",
|
||||
"clients_and_providers": "CATALOGS",
|
||||
"items": "CATALOGS",
|
||||
"classes": "CATALOGS",
|
||||
"classification_concepts": "CATALOGS",
|
||||
"concepts": "CATALOGS",
|
||||
"customs_broker_concepts": "CATALOGS",
|
||||
"depreciation_catalog": "CATALOGS",
|
||||
"electronic_notices": "ELECTRONIC NOTICES",
|
||||
"equivalencies": "CATALOGS",
|
||||
"error_catalogs": "CATALOGS",
|
||||
"fda_catalog": "CATALOGS",
|
||||
"inpc": "CATALOGS",
|
||||
"legends": "CATALOGS",
|
||||
"multi_currency_types": "CATALOGS",
|
||||
"packages": "CATALOGS",
|
||||
"ports": "CATALOGS",
|
||||
"prevalidators": "CATALOGS",
|
||||
"seal": "CATALOGS",
|
||||
"signatures": "CATALOGS",
|
||||
"tariff_fractions": "CATALOGS",
|
||||
"unit_conversions": "CATALOGS",
|
||||
"us_tariff_fractions": "CATALOGS",
|
||||
|
||||
# DODA
|
||||
"doda": "DODA",
|
||||
"doda_containers": "DODA",
|
||||
"doda_pedimentos": "DODA",
|
||||
}
|
||||
|
||||
# Map (Table, Operation) to Legacy Movements (English)
|
||||
@@ -107,6 +134,14 @@ class AuditMapper:
|
||||
|
||||
("auth", "LOGIN"): "SYSTEM LOGIN",
|
||||
("auth", "LOGOUT"): "SYSTEM LOGOUT",
|
||||
|
||||
("classes", "CREATE"): "ADD CLASS",
|
||||
("classes", "UPDATE"): "EDIT CLASS",
|
||||
("classes", "DELETE"): "DELETE CLASS",
|
||||
|
||||
("doda", "CREATE"): "ADD DODA",
|
||||
("doda", "UPDATE"): "EDIT DODA",
|
||||
("doda", "DELETE"): "DELETE DODA",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -40,8 +40,8 @@ class AuditService:
|
||||
"""
|
||||
Low-level creation of an Audit Log entry
|
||||
"""
|
||||
# Timezone handling set to Mexico City as requested implicitly by legacy format example
|
||||
tz = pytz.timezone('America/Mexico_City')
|
||||
# Timezone handling set to Hermosillo (Sonora) to match user preference (-1h vs CDMX)
|
||||
tz = pytz.timezone('America/Hermosillo')
|
||||
now = datetime.now(tz)
|
||||
|
||||
log = AuditLog(
|
||||
@@ -120,6 +120,9 @@ class AuditService:
|
||||
|
||||
elif table_name == "companies":
|
||||
reference = record_data.get("rfc") or reference
|
||||
|
||||
elif table_name == "classes":
|
||||
reference = record_data.get("class_code") or reference
|
||||
|
||||
|
||||
# 3. Detect Changed Fields (for Update)
|
||||
@@ -150,7 +153,30 @@ class AuditService:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def log_login(db: Session, username: str, ip_address: str = None):
|
||||
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 set to Hermosillo (Sonora) to match user preference (-1h vs CDMX)
|
||||
tz = pytz.timezone('America/Hermosillo')
|
||||
now = datetime.now(tz)
|
||||
five_seconds_ago = now - datetime.timedelta(seconds=5)
|
||||
|
||||
# Check for recent login from same user
|
||||
existing = db.query(AuditLog).filter(
|
||||
AuditLog.username == username,
|
||||
AuditLog.operation_type == "LOGIN",
|
||||
# Compare against timestamp (timezone aware)
|
||||
AuditLog.timestamp >= five_seconds_ago
|
||||
).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}")
|
||||
|
||||
return AuditService.create_audit_log(
|
||||
db=db,
|
||||
reference="LOGIN",
|
||||
@@ -158,5 +184,27 @@ class AuditService:
|
||||
movement="SYSTEM LOGIN",
|
||||
username=username,
|
||||
operation_type="LOGIN",
|
||||
ip_address=ip_address
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def log_logout(db: Session, username: str, ip_address: str = None, user_agent: str = None):
|
||||
"""
|
||||
Registra un evento de cierre de sesión
|
||||
"""
|
||||
try:
|
||||
# Reutilizamos create_audit_log para mantener consistencia
|
||||
AuditService.create_audit_log(
|
||||
db=db,
|
||||
reference="LOGOUT",
|
||||
procedure="SYSTEM AUTH",
|
||||
movement="SYSTEM LOGOUT",
|
||||
username=username,
|
||||
operation_type="LOGOUT",
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent
|
||||
)
|
||||
except Exception as e:
|
||||
# No re-lanzamos la excepción para no interrumpir el flujo de logout
|
||||
print(f"Error logging logout: {e}")
|
||||
|
||||
@@ -76,6 +76,7 @@ class LogoutRequestDTO(BaseModel):
|
||||
"""DTO para solicitud de logout"""
|
||||
|
||||
refresh_token: str = Field(..., description="Refresh token para invalidar")
|
||||
username: Optional[str] = Field(None, description="Nombre de usuario para auditoría")
|
||||
|
||||
|
||||
class RegisterRequestDTO(BaseModel):
|
||||
|
||||
@@ -4,7 +4,7 @@ Endpoints API para autenticación
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, Request
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -50,17 +50,25 @@ async def register(
|
||||
|
||||
|
||||
@router.post("/login", response_model=TokenResponseDTO)
|
||||
async def login(login_data: LoginRequestDTO, db: Session = Depends(get_core_db)):
|
||||
async def login(
|
||||
login_data: LoginRequestDTO,
|
||||
request: Request, # Inject Request
|
||||
db: Session = Depends(get_core_db)
|
||||
):
|
||||
"""
|
||||
Autentica usuario con Keycloak y retorna tokens JWT
|
||||
|
||||
|
||||
El usuario debe proporcionar:
|
||||
- username: Usuario o email
|
||||
- password: Contraseña
|
||||
- tenant_slug: Slug del tenant al que pertenece
|
||||
"""
|
||||
service = AuthService(db)
|
||||
return service.login(login_data)
|
||||
return service.login(
|
||||
login_data=login_data,
|
||||
ip_address=request.client.host,
|
||||
user_agent=request.headers.get("user-agent")
|
||||
)
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=TokenResponseDTO)
|
||||
@@ -89,12 +97,38 @@ async def get_current_user_info(
|
||||
@router.post("/logout")
|
||||
async def logout(
|
||||
logout_data: LogoutRequestDTO,
|
||||
request: Request, # Inject request for IP/User-Agent
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
# Make current_user optional to avoid 401 on expired tokens
|
||||
# We will try to use it if available, otherwise use DTO
|
||||
# Note: Depends(get_current_user) raises HTTPException if invalid, so we cannot make it optional easily without changing dependency.
|
||||
# Instead, we will rely on DTO username since user explicitly asked for this simplified flow.
|
||||
# But if we want to support both, we can't use strict dependency here if we expect it to work on expired tokens.
|
||||
# So we remove the strict dependency for now as per "simplified" request.
|
||||
):
|
||||
"""
|
||||
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}")
|
||||
|
||||
service = AuthService(db)
|
||||
return service.logout(logout_data)
|
||||
|
||||
|
||||
@@ -37,12 +37,19 @@ class AuthService:
|
||||
client_secret_key=settings.KEYCLOAK_CLIENT_SECRET,
|
||||
)
|
||||
|
||||
def login(self, login_data: LoginRequestDTO) -> TokenResponseDTO:
|
||||
def login(
|
||||
self,
|
||||
login_data: LoginRequestDTO,
|
||||
ip_address: str = None,
|
||||
user_agent: str = None
|
||||
) -> TokenResponseDTO:
|
||||
"""
|
||||
Autentica usuario y obtiene tokens
|
||||
|
||||
Args:
|
||||
login_data: Credenciales de login
|
||||
ip_address: Dirección IP del cliente
|
||||
user_agent: User Agent del cliente
|
||||
|
||||
Returns:
|
||||
TokenResponseDTO con access_token y refresh_token
|
||||
@@ -143,6 +150,18 @@ class AuthService:
|
||||
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,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to audit login: {e}")
|
||||
|
||||
return TokenResponseDTO(
|
||||
access_token=token_response["access_token"],
|
||||
refresh_token=token_response["refresh_token"],
|
||||
@@ -196,7 +215,7 @@ class AuthService:
|
||||
|
||||
Args:
|
||||
access_token: Access token JWT
|
||||
|
||||
|
||||
Returns:
|
||||
UserInfoResponseDTO con información del usuario
|
||||
"""
|
||||
@@ -242,7 +261,6 @@ class AuthService:
|
||||
try:
|
||||
self.keycloak_openid.logout(logout_data.refresh_token)
|
||||
return {"message": "Logged out successfully"}
|
||||
|
||||
except KeycloakError as e:
|
||||
logger.warning(f"Logout failed: {str(e)}")
|
||||
# No lanzamos error aquí, el logout puede fallar si el token ya expiró
|
||||
|
||||
@@ -143,6 +143,27 @@ from api.v1.modules.public.reference_data.valuation_methods.models import Valuat
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
|
||||
from api.v1.modules.a76.general_catalogs.exchange_rate.models import ExchangeRate
|
||||
from api.v1.modules.a76.general_catalogs.identifiers.models import Identifier
|
||||
from api.v1.modules.a76.classes.models import Class
|
||||
from api.v1.modules.a76.general_catalogs.classification_concepts.models import ClassificationConcept
|
||||
from api.v1.modules.a76.general_catalogs.concepts.models import Concept
|
||||
from api.v1.modules.a76.general_catalogs.customs_broker_concepts.models import CustomsBrokerConcept
|
||||
from api.v1.modules.a76.general_catalogs.depreciation_catalog.models import DepreciationCatalog
|
||||
from api.v1.modules.a76.general_catalogs.doda.models import Doda
|
||||
from api.v1.modules.a76.general_catalogs.electronic_notices.models import ElectronicNotice
|
||||
from api.v1.modules.a76.general_catalogs.equivalencies.models import Equivalency
|
||||
from api.v1.modules.a76.general_catalogs.error_catalogs.models import ErrorCatalog
|
||||
from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDACatalog
|
||||
from api.v1.modules.a76.general_catalogs.inpc.models import INPC
|
||||
from api.v1.modules.a76.general_catalogs.legends.models import Legend
|
||||
from api.v1.modules.a76.general_catalogs.multi_currency_types.models import MultiCurrencyType
|
||||
from api.v1.modules.a76.general_catalogs.packages.models import Package
|
||||
from api.v1.modules.a76.general_catalogs.ports.models import Port
|
||||
from api.v1.modules.a76.general_catalogs.prevalidators.models import Prevalidator
|
||||
from api.v1.modules.a76.general_catalogs.seal.models import Seal
|
||||
from api.v1.modules.a76.general_catalogs.signatures.models import Signature
|
||||
from api.v1.modules.a76.general_catalogs.tariff_fractions.models import TariffFraction
|
||||
from api.v1.modules.a76.general_catalogs.unit_conversions.models import UnitConversion
|
||||
from api.v1.modules.a76.general_catalogs.us_tariff_fractions.models import USTariffFraction
|
||||
|
||||
# Registrar Listeners de Auditoría
|
||||
@app.on_event("startup")
|
||||
@@ -178,7 +199,28 @@ def register_audit():
|
||||
ValuationMethod,
|
||||
UnitOfMeasure,
|
||||
ExchangeRate,
|
||||
Identifier
|
||||
Identifier,
|
||||
Class,
|
||||
ClassificationConcept,
|
||||
Concept,
|
||||
CustomsBrokerConcept,
|
||||
DepreciationCatalog,
|
||||
Doda,
|
||||
ElectronicNotice,
|
||||
Equivalency,
|
||||
ErrorCatalog,
|
||||
FDACatalog,
|
||||
INPC,
|
||||
Legend,
|
||||
MultiCurrencyType,
|
||||
Package,
|
||||
Port,
|
||||
Prevalidator,
|
||||
Seal,
|
||||
Signature,
|
||||
TariffFraction,
|
||||
UnitConversion,
|
||||
USTariffFraction
|
||||
])
|
||||
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ async function fetchApi<T = any>(
|
||||
});
|
||||
|
||||
// Si recibimos 401 o 403 y no es el endpoint de refresh, intentar refrescar el token
|
||||
if ((response.status === 401 || response.status === 403) && !endpoint.includes('/auth/refresh') && retryCount === 0) {
|
||||
if ((response.status === 401 || response.status === 403) && !endpoint.includes('/auth/refresh') && retryCount === 0) {
|
||||
// Si es 403 (Forbidden), mostrar toast de permisos insuficientes
|
||||
if (response.status === 403) {
|
||||
if (browser) {
|
||||
@@ -192,7 +192,7 @@ async function fetchApi<T = any>(
|
||||
status: 403
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Si es 401, intentar refrescar el token
|
||||
isRefreshing = true;
|
||||
|
||||
@@ -292,25 +292,28 @@ async function fetchApi<T = any>(
|
||||
export const api = {
|
||||
get: <T = any>(endpoint: string) => fetchApi<T>(endpoint, { method: 'GET' }),
|
||||
|
||||
post: <T = any>(endpoint: string, body: any) =>
|
||||
post: <T = any>(endpoint: string, body: any, options: RequestInit = {}) =>
|
||||
fetchApi<T>(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body)
|
||||
body: JSON.stringify(body),
|
||||
...options
|
||||
}),
|
||||
|
||||
put: <T = any>(endpoint: string, body: any) =>
|
||||
put: <T = any>(endpoint: string, body: any, options: RequestInit = {}) =>
|
||||
fetchApi<T>(endpoint, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(body)
|
||||
body: JSON.stringify(body),
|
||||
...options
|
||||
}),
|
||||
|
||||
patch: <T = any>(endpoint: string, body: any) =>
|
||||
patch: <T = any>(endpoint: string, body: any, options: RequestInit = {}) =>
|
||||
fetchApi<T>(endpoint, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(body)
|
||||
body: JSON.stringify(body),
|
||||
...options
|
||||
}),
|
||||
|
||||
delete: <T = any>(endpoint: string) => fetchApi<T>(endpoint, { method: 'DELETE' }),
|
||||
delete: <T = any>(endpoint: string, options: RequestInit = {}) => fetchApi<T>(endpoint, { method: 'DELETE', ...options }),
|
||||
|
||||
// Endpoints específicos
|
||||
auth: {
|
||||
@@ -318,7 +321,7 @@ export const api = {
|
||||
api.post('/v1/auth/login/', credentials),
|
||||
refresh: (refreshToken: string) =>
|
||||
api.post('/v1/auth/refresh/', { refresh_token: refreshToken }),
|
||||
logout: (data: { refresh_token: string }) => api.post('/v1/auth/logout/', data),
|
||||
logout: (data: { refresh_token: string, username?: string }) => api.post('/v1/auth/logout', data, { keepalive: true }),
|
||||
me: () => api.get('/v1/auth/me/'),
|
||||
health: () => api.get('/health')
|
||||
},
|
||||
|
||||
@@ -46,14 +46,14 @@ const setCookie = (name: string, value: string, days: number = 7) => {
|
||||
if (!browser) return;
|
||||
const expirationDate = new Date();
|
||||
expirationDate.setDate(expirationDate.getDate() + days);
|
||||
|
||||
|
||||
// En desarrollo (localhost), no usar Secure flag
|
||||
const isSecure = window.location.protocol === 'https:';
|
||||
const secureFlag = isSecure ? '; Secure' : '';
|
||||
|
||||
|
||||
const cookieString = `${name}=${value}; path=/; expires=${expirationDate.toUTCString()}; SameSite=Lax${secureFlag}`;
|
||||
document.cookie = cookieString;
|
||||
|
||||
|
||||
// Verificar que se estableció
|
||||
const verification = getCookie(name);
|
||||
};
|
||||
@@ -121,13 +121,13 @@ export const initAuth = async (): Promise<boolean> => {
|
||||
if (token) {
|
||||
authStore.setToken(token);
|
||||
authStore.setAuthenticated(true);
|
||||
|
||||
|
||||
// Sincronizar con cookies si no existe
|
||||
const cookieToken = getCookie('access_token');
|
||||
if (!cookieToken) {
|
||||
setCookie('access_token', token);
|
||||
}
|
||||
|
||||
|
||||
await loadUserInfo(token);
|
||||
authStore.setLoading(false);
|
||||
return true;
|
||||
@@ -135,7 +135,7 @@ export const initAuth = async (): Promise<boolean> => {
|
||||
|
||||
// Si no hay token local, intentar con Keycloak
|
||||
await initKeycloak();
|
||||
|
||||
|
||||
authStore.setLoading(false);
|
||||
return false;
|
||||
} catch (error) {
|
||||
@@ -299,14 +299,14 @@ export const login = async (credentials: {
|
||||
if (loginData?.access_token) {
|
||||
authStore.setToken(loginData.access_token);
|
||||
authStore.setAuthenticated(true);
|
||||
|
||||
|
||||
// Guardar también en localStorage para persistencia
|
||||
if (browser) {
|
||||
localStorage.setItem('access_token', loginData.access_token);
|
||||
if (loginData.refresh_token) {
|
||||
localStorage.setItem('refresh_token', loginData.refresh_token);
|
||||
}
|
||||
|
||||
|
||||
// Guardar en cookies para que el servidor pueda acceder
|
||||
setCookie('access_token', loginData.access_token);
|
||||
if (loginData.refresh_token) {
|
||||
@@ -338,7 +338,7 @@ const loadUserInfo = async (token: string) => {
|
||||
try {
|
||||
// Guardar temporalmente el token para que api.ts lo use
|
||||
authStore.setToken(token);
|
||||
|
||||
|
||||
// Usar la API centralizada
|
||||
const { api } = await import('./api');
|
||||
const response = await api.auth.me();
|
||||
@@ -369,18 +369,9 @@ export const logout = async () => {
|
||||
try {
|
||||
// Obtener el refresh token si existe
|
||||
const refreshToken = localStorage.getItem('refresh_token');
|
||||
|
||||
// Si hay refresh token, intentar invalidarlo en el backend
|
||||
if (refreshToken) {
|
||||
try {
|
||||
const { api } = await import('./api');
|
||||
await api.auth.logout({ refresh_token: refreshToken });
|
||||
} catch (error) {
|
||||
console.error('Error al invalidar refresh token:', error);
|
||||
// Continuar con el logout aunque falle
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Limpiar store de compañías
|
||||
try {
|
||||
const { companyStore } = await import('./stores/company.svelte');
|
||||
@@ -388,14 +379,14 @@ export const logout = async () => {
|
||||
} catch (error) {
|
||||
console.error('Error al limpiar store de compañías:', error);
|
||||
}
|
||||
|
||||
|
||||
// Limpiar estado local
|
||||
authStore.reset();
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
deleteCookie('access_token');
|
||||
deleteCookie('refresh_token');
|
||||
|
||||
|
||||
// Si hay instancia de Keycloak, hacer logout de Keycloak
|
||||
if (keycloakInstance?.authenticated) {
|
||||
await keycloakInstance.logout({
|
||||
@@ -403,7 +394,7 @@ export const logout = async () => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Llamar al endpoint del servidor para limpiar cookies de SvelteKit
|
||||
// Usar un formulario para hacer POST y permitir la redirección
|
||||
const form = document.createElement('form');
|
||||
@@ -411,7 +402,7 @@ export const logout = async () => {
|
||||
form.action = '/logout';
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error durante logout:', error);
|
||||
// Asegurar que se redirija al login aunque haya error
|
||||
@@ -435,11 +426,11 @@ export const getToken = (): string | null => {
|
||||
if (keycloakInstance?.token) {
|
||||
return keycloakInstance.token;
|
||||
}
|
||||
|
||||
|
||||
// Si no, intentar de localStorage
|
||||
if (browser) {
|
||||
let token = localStorage.getItem('access_token');
|
||||
|
||||
|
||||
// Si no hay token en localStorage, intentar de las cookies
|
||||
if (!token) {
|
||||
token = getCookie('access_token');
|
||||
@@ -448,10 +439,10 @@ export const getToken = (): string | null => {
|
||||
localStorage.setItem('access_token', token);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -483,7 +474,7 @@ export const refreshAccessToken = async (): Promise<boolean> => {
|
||||
|
||||
authStore.setToken(newAccessToken);
|
||||
localStorage.setItem('access_token', newAccessToken);
|
||||
|
||||
|
||||
if (newRefreshToken) {
|
||||
localStorage.setItem('refresh_token', newRefreshToken);
|
||||
}
|
||||
|
||||
@@ -331,10 +331,6 @@
|
||||
<RefreshCw class="h-4 w-4 animate-spin text-primary" />
|
||||
<span class="text-sm text-muted-foreground">Cargando más registros...</span>
|
||||
</div>
|
||||
{:else if !hasMore && logs.length > 0}
|
||||
<span class="text-xs tracking-wider text-muted-foreground uppercase opacity-50"
|
||||
>Fin de la bitácora</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,86 @@
|
||||
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: '/' });
|
||||
|
||||
|
||||
// Eliminar la cookie de la compañía activa
|
||||
cookies.delete('active_company_id', { path: '/' });
|
||||
|
||||
|
||||
// Redirigir al login
|
||||
throw redirect(303, '/login');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user