v1.15.1 - modulo de reportes implementado
- Nuevo módulo de reportes: backend/app/api/v1/endpoints/reports.py - Schemas de reportes: backend/app/api/schemas/reports.py - Frontend: frontend-internal/src/routes/reports/ - Mejoras al módulo de auditoría (audit.py, audit_helpers.py) - Modelo de auditoría actualizado - Sidebar actualizado con enlace a reportes
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Audit Log Model - ServiceManagerWeb
|
||||
|
||||
Modelo para bitácora de auditoría y compliance.
|
||||
Modelo para bitácora de auditoría y compliance.
|
||||
Registra todas las acciones importantes del sistema.
|
||||
"""
|
||||
|
||||
@@ -10,7 +10,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import INET, JSONB
|
||||
from typing import Optional, Dict, Any, TYPE_CHECKING
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from app.core.database import Base, GUID
|
||||
|
||||
@@ -21,131 +21,154 @@ if TYPE_CHECKING:
|
||||
|
||||
class AuditLog(Base):
|
||||
"""
|
||||
Bitácora de auditoría para tracking completo de acciones.
|
||||
|
||||
Bitácora de auditoría para tracking completo de acciones.
|
||||
|
||||
Registra:
|
||||
- Qui├®n hizo la acci├│n (user_id)
|
||||
- Qu├® hizo (action)
|
||||
- Sobre qu├® recurso (resource_type + resource_id)
|
||||
- Cuándo lo hizo (created_at)
|
||||
- Desde d├│nde (ip_address, user_agent)
|
||||
- Qu├® cambi├│ (old_values, new_values)
|
||||
- Quién hizo la acción (user_id)
|
||||
- Qué hizo (action)
|
||||
- Sobre qué recurso (resource_type + resource_id)
|
||||
- Cuándo lo hizo (created_at)
|
||||
- Desde dónde (ip_address, user_agent)
|
||||
- Qué cambió (old_values, new_values)
|
||||
"""
|
||||
|
||||
|
||||
__tablename__ = "audit_logs"
|
||||
|
||||
# Multi-tenancy
|
||||
|
||||
# Multi-tenancy: cada registro pertenece a un tenant específico
|
||||
tenant_id: Mapped[uuid.UUID] = mapped_column(
|
||||
GUID(),
|
||||
ForeignKey("tenants.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True
|
||||
)
|
||||
|
||||
# Usuario que ejecut├│ la acci├│n (NULL = acci├│n del sistema)
|
||||
|
||||
# Usuario que ejecutó la acción (NULL = acción del sistema)
|
||||
user_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
||||
GUID(),
|
||||
ForeignKey("users.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True
|
||||
)
|
||||
|
||||
# Acci├│n realizada (ej: "user.login", "ticket.create", "ticket.assign")
|
||||
|
||||
# Acción realizada en formato "recurso.verbo"
|
||||
# Ejemplos: "user.login", "ticket.create", "ticket.assign"
|
||||
action: Mapped[str] = mapped_column(
|
||||
String(100),
|
||||
nullable=False,
|
||||
index=True
|
||||
)
|
||||
|
||||
|
||||
# Tipo de recurso afectado (user, ticket, comment, category, etc.)
|
||||
resource_type: Mapped[str] = mapped_column(
|
||||
String(50),
|
||||
nullable=False,
|
||||
index=True
|
||||
)
|
||||
|
||||
|
||||
# ID del recurso afectado
|
||||
resource_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
||||
GUID(),
|
||||
nullable=True
|
||||
)
|
||||
|
||||
# Contexto de la request
|
||||
|
||||
# Contexto de la request: IP y navegador del usuario
|
||||
ip_address: Mapped[Optional[str]] = mapped_column(
|
||||
String(45).with_variant(INET, "postgresql"),
|
||||
nullable=True,
|
||||
)
|
||||
user_agent: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
|
||||
# Correlation ID para rastrear requests relacionadas
|
||||
|
||||
# Correlation ID para rastrear todas las requests relacionadas
|
||||
# en una misma operación o sesión
|
||||
correlation_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
||||
GUID(),
|
||||
nullable=True,
|
||||
index=True
|
||||
)
|
||||
|
||||
# Valores antes del cambio (JSON)
|
||||
|
||||
# Estado del recurso antes del cambio (para auditoría de cambios)
|
||||
old_values: Mapped[Optional[Dict[str, Any]]] = mapped_column(
|
||||
JSON().with_variant(JSONB, "postgresql"),
|
||||
nullable=True
|
||||
)
|
||||
|
||||
# Valores despu├®s del cambio (JSON)
|
||||
|
||||
# Estado del recurso después del cambio (para auditoría de cambios)
|
||||
new_values: Mapped[Optional[Dict[str, Any]]] = mapped_column(
|
||||
JSON().with_variant(JSONB, "postgresql"),
|
||||
nullable=True
|
||||
)
|
||||
|
||||
# Metadata adicional (cualquier info relevante)
|
||||
# Nota: 'metadata' está reservado en SQLAlchemy, usamos 'extra_metadata'
|
||||
|
||||
# Metadata adicional con cualquier información relevante del contexto
|
||||
# Nota: 'metadata' está reservado en SQLAlchemy, se usa 'extra_metadata'
|
||||
# como nombre del atributo Python, pero la columna en BD se llama 'metadata'
|
||||
extra_metadata: Mapped[Optional[Dict[str, Any]]] = mapped_column(
|
||||
'metadata', # Nombre real de la columna en BD
|
||||
'metadata',
|
||||
JSON().with_variant(JSONB, "postgresql"),
|
||||
nullable=True
|
||||
)
|
||||
|
||||
# Timestamp
|
||||
|
||||
# Timestamp de creación con timezone
|
||||
# CORRECCIÓN: default=lambda: datetime.now(timezone.utc) genera un
|
||||
# datetime aware en UTC, compatible con DateTime(timezone=True).
|
||||
# El default anterior (datetime.utcnow) generaba datetimes naive,
|
||||
# causando que los filtros de fecha fallaran silenciosamente porque
|
||||
# SQLAlchemy no podía comparar aware vs naive correctamente.
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
default=datetime.utcnow,
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
nullable=False,
|
||||
index=True
|
||||
)
|
||||
|
||||
# Relaciones
|
||||
|
||||
# Relaciones con otros modelos
|
||||
tenant: Mapped["Tenant"] = relationship("Tenant", foreign_keys=[tenant_id])
|
||||
user: Mapped[Optional["User"]] = relationship("User", foreign_keys=[user_id])
|
||||
|
||||
# Índices compuestos para queries comunes
|
||||
|
||||
# Índices compuestos para optimizar las queries más frecuentes
|
||||
__table_args__ = (
|
||||
# Filtrar logs por tenant y tipo de acción (uso más común)
|
||||
Index('idx_audit_logs_tenant_action', 'tenant_id', 'action'),
|
||||
# Buscar el historial de un recurso específico
|
||||
Index('idx_audit_logs_resource', 'resource_type', 'resource_id'),
|
||||
# Ver la actividad de un usuario ordenada por fecha
|
||||
Index('idx_audit_logs_user_created', 'user_id', 'created_at'),
|
||||
)
|
||||
|
||||
# Configuraci├│n del mapper: excluir updated_at porque audit logs son inmutables
|
||||
|
||||
# Los audit logs son inmutables: nunca se actualizan, solo se crean
|
||||
# Por eso se excluye updated_at del mapper
|
||||
__mapper_args__ = {
|
||||
"exclude_properties": ["updated_at"]
|
||||
}
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<AuditLog(action='{self.action}', resource='{self.resource_type}:{self.resource_id}')>"
|
||||
|
||||
return (
|
||||
f"<AuditLog("
|
||||
f"action='{self.action}', "
|
||||
f"resource='{self.resource_type}:{self.resource_id}'"
|
||||
f")>"
|
||||
)
|
||||
|
||||
@property
|
||||
def action_display(self) -> str:
|
||||
"""Formato amigable de la acci├│n."""
|
||||
"""
|
||||
Formato legible de la acción para mostrar en la interfaz.
|
||||
|
||||
Convierte el formato interno "recurso.verbo" a texto descriptivo.
|
||||
Ejemplo: "ticket.create" → "creó ticket"
|
||||
"""
|
||||
parts = self.action.split('.')
|
||||
if len(parts) == 2:
|
||||
resource, verb = parts
|
||||
verb_map = {
|
||||
'create': 'cre├│',
|
||||
'update': 'actualiz├│',
|
||||
'delete': 'elimin├│',
|
||||
'login': 'inici├│ sesi├│n',
|
||||
'logout': 'cerr├│ sesi├│n',
|
||||
'assign': 'asign├│',
|
||||
'close': 'cerr├│',
|
||||
'reopen': 'reabri├│'
|
||||
'create': 'creó',
|
||||
'update': 'actualizó',
|
||||
'delete': 'eliminó',
|
||||
'login': 'inició sesión',
|
||||
'logout': 'cerró sesión',
|
||||
'login_failed': 'intentó iniciar sesión',
|
||||
'assign': 'asignó',
|
||||
'close': 'cerró',
|
||||
'reopen': 'reabrió'
|
||||
}
|
||||
return f"{verb_map.get(verb, verb)} {resource}"
|
||||
return self.action
|
||||
return self.action
|
||||
Reference in New Issue
Block a user