feature/bitacora-correccion-filtro-tenant

This commit is contained in:
2026-05-06 12:12:09 -06:00
parent 86a3d703ba
commit 8c8cd0e247
4 changed files with 166 additions and 63 deletions

View File

@@ -1,6 +1,7 @@
"""
Audit Log Service
"""
import logging
from datetime import datetime, timedelta, date, time
from decimal import Decimal
import uuid
@@ -9,8 +10,8 @@ from typing import Optional, List, Dict, Any
from sqlalchemy.orm import Session
from ..models import AuditLog
from .core import AuditMapper, ReferenceGenerator
from core.security import verify_token # keep if needed or simpler just remove if unused
# We don't need security import here anymore as context is passed explicitly or handled by events
logger = logging.getLogger(__name__)
def _make_json_safe(obj: Any) -> Any:
@@ -188,49 +189,62 @@ 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)
def log_login(
db: Session,
username: str,
ip_address: str = None,
user_agent: str = None,
company_id: Optional[int] = None,
tenant_id: Optional[int] = None,
):
"""
``audit_logs`` exige ``tenant_id`` y ``company_id``. El login vía Hub no
define compañía activa; sin ambos argumentos no se inserta fila (antes fallaba NOT NULL).
"""
if company_id is None or tenant_id is None:
return None
try:
now = datetime.now(pytz.UTC)
five_seconds_ago = now - 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
AuditLog.company_id == company_id,
AuditLog.tenant_id == tenant_id,
AuditLog.timestamp >= five_seconds_ago,
).first()
if existing:
return existing
except Exception as e:
import traceback
traceback.print_exc()
logger.warning("Login audit debounce query failed: %s", e)
return AuditService.create_audit_log(
db=db,
reference="LOGIN",
procedure="SYSTEM SCAF",
movement="SYSTEM LOGIN",
username=username,
operation_type="LOGIN",
ip_address=ip_address,
user_agent=user_agent
db=db,
reference="LOGIN",
procedure="SYSTEM SCAF",
movement="SYSTEM LOGIN",
username=username,
operation_type="LOGIN",
ip_address=ip_address,
user_agent=user_agent,
company_id=company_id,
tenant_id=tenant_id,
)
@staticmethod
def log_logout(db: Session, username: str, ip_address: str = None, user_agent: str = None):
"""
Registra un evento de cierre de sesión
"""
def log_logout(
db: Session,
username: str,
ip_address: str = None,
user_agent: str = None,
company_id: Optional[int] = None,
tenant_id: Optional[int] = None,
):
"""Misma condición que ``log_login``: sin alcance compañía/tenant no se escribe."""
if company_id is None or tenant_id is None:
return None
try:
# Reutilizamos create_audit_log para mantener consistencia
AuditService.create_audit_log(
db=db,
reference="LOGOUT",
@@ -239,8 +253,9 @@ class AuditService:
username=username,
operation_type="LOGOUT",
ip_address=ip_address,
user_agent=user_agent
user_agent=user_agent,
company_id=company_id,
tenant_id=tenant_id,
)
except Exception as e:
# No re-lanzamos la excepción para no interrumpir el flujo de logout
pass
logger.warning("Logout audit insert failed: %s", e)