diff --git a/backend/app/api/v1/endpoints/audit.py b/backend/app/api/v1/endpoints/audit.py index 3320d39..feba95b 100644 --- a/backend/app/api/v1/endpoints/audit.py +++ b/backend/app/api/v1/endpoints/audit.py @@ -266,9 +266,22 @@ async def get_audit_stats( by_resource_result = await db.execute(by_resource_query) by_resource_type = {row.resource_type: row.count for row in by_resource_result} - # Top usuarios (necesitamos hacer join - simplificado por ahora) - # En producci├│n podr├¡as hacer un join con users para obtener nombres - top_users = {} # Placeholder - implementar con join si es necesario + # Top usuarios (con join a users para obtener nombres) + top_users_query = select( + User.email, + func.count(AuditLog.id).label('count') + ).join( + User, AuditLog.user_id == User.id + ).where( + AuditLog.tenant_id == current_tenant.id + ).group_by( + User.email + ).order_by( + desc('count') + ).limit(5) + + top_users_result = await db.execute(top_users_query) + top_users = {row.email: row.count for row in top_users_result} # Acciones cr├¡ticas hoy (delete, update sensibles, etc.) critical_actions_query = select(func.count()).select_from(AuditLog).where( diff --git a/backend/app/api/v1/endpoints/auth.py b/backend/app/api/v1/endpoints/auth.py index 97fe54b..4687266 100644 --- a/backend/app/api/v1/endpoints/auth.py +++ b/backend/app/api/v1/endpoints/auth.py @@ -18,6 +18,7 @@ from app.core.security import security from app.core.config import get_settings from app.models.user import User from app.models.tenant import Tenant +from app.services.audit_service import AuditService router = APIRouter() logger = structlog.get_logger(__name__) @@ -99,6 +100,23 @@ async def login( "Login failed - invalid credentials", email=login_data.email ) + + # Registrar intento fallido en auditoría (si el usuario existe) + if user: + try: + await AuditService.log( + db=db, + tenant_id=user.tenant_id, + user_id=None, # Login fallido = sin user_id + action="user.login_failed", + resource_type="user", + resource_id=user.id, + metadata={"email": login_data.email, "reason": "invalid_password"} + ) + await db.commit() + except Exception as e: + logger.warning("Failed to log audit entry", error=str(e)) + raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Credenciales inválidas" @@ -126,6 +144,21 @@ async def login( access_token = security.create_access_token(token_data) refresh_token = security.create_refresh_token(token_data) + # Registrar login exitoso en auditoría + try: + await AuditService.log( + db=db, + tenant_id=user.tenant_id, + user_id=user.id, + action="user.login", + resource_type="user", + resource_id=user.id, + metadata={"email": user.email, "success": True} + ) + await db.commit() + except Exception as e: + logger.warning("Failed to log audit entry", error=str(e)) + logger.info( "Login successful", email=login_data.email, @@ -227,6 +260,25 @@ async def logout( # TODO: Revoke refresh token in database + # Registrar logout en auditoría + try: + import uuid + user_id = uuid.UUID(payload["sub"]) + tenant_id = uuid.UUID(payload["tenant_id"]) + + await AuditService.log( + db=db, + tenant_id=tenant_id, + user_id=user_id, + action="user.logout", + resource_type="user", + resource_id=user_id, + metadata={"email": payload.get("email")} + ) + await db.commit() + except Exception as e: + logger.warning("Failed to log audit entry", error=str(e)) + logger.info("Logout successful", user_id=payload["sub"]) return {"message": "Successfully logged out"} diff --git a/backend/app/api/v1/endpoints/tickets.py b/backend/app/api/v1/endpoints/tickets.py index 9dac149..44d61ec 100644 --- a/backend/app/api/v1/endpoints/tickets.py +++ b/backend/app/api/v1/endpoints/tickets.py @@ -21,6 +21,7 @@ from app.models.comment import TicketComment from app.models.attachment import TicketAttachment from app.api.schemas.attachment import AttachmentResponse from app.core.file_handler import file_handler +from app.services.audit_service import AuditService import uuid router = APIRouter() @@ -143,6 +144,27 @@ async def create_ticket( await db.commit() await db.refresh(db_ticket) + # Registrar creación en auditoría + try: + await AuditService.log( + db=db, + tenant_id=current_user.tenant_id, + user_id=current_user.id, + action="ticket.create", + resource_type="ticket", + resource_id=db_ticket.id, + new_values={ + "ticket_number": db_ticket.ticket_number, + "subject": db_ticket.subject, + "priority": db_ticket.priority.value, + "status": db_ticket.status.value + } + ) + await db.commit() + except Exception as e: + # No fallar si falla el audit log + pass + # ✅ Éxito - retornar ticket creado return { "id": str(db_ticket.id), @@ -501,6 +523,15 @@ async def update_ticket( detail=f"Ticket {ticket_id} not found" ) + # Guardar valores anteriores para audit + old_values = { + "subject": db_ticket.subject, + "description": db_ticket.description, + "status": db_ticket.status.value, + "priority": db_ticket.priority.value, + "assigned_to": str(db_ticket.assigned_to) if db_ticket.assigned_to else None + } + try: update_data = ticket_update.dict(exclude_unset=True) @@ -519,6 +550,34 @@ async def update_ticket( await db.commit() await db.refresh(db_ticket) + # Registrar actualización en auditoría + try: + new_values = { + "subject": db_ticket.subject, + "description": db_ticket.description, + "status": db_ticket.status.value, + "priority": db_ticket.priority.value, + "assigned_to": str(db_ticket.assigned_to) if db_ticket.assigned_to else None + } + + # Si cambió assigned_to, registrar como acción de asignación + action = "ticket.assign" if old_values["assigned_to"] != new_values["assigned_to"] else "ticket.update" + + await AuditService.log( + db=db, + tenant_id=current_user.tenant_id, + user_id=current_user.id, + action=action, + resource_type="ticket", + resource_id=db_ticket.id, + old_values=old_values, + new_values=new_values + ) + await db.commit() + except Exception as e: + # No fallar si falla el audit log + pass + # ✅ CORREGIDO: Usar affected_system_id return { "id": str(db_ticket.id), @@ -787,9 +846,33 @@ async def delete_ticket( detail=f"Ticket {ticket_id} not found" ) + # Guardar datos del ticket antes de eliminar para audit + old_values = { + "ticket_number": db_ticket.ticket_number, + "subject": db_ticket.subject, + "status": db_ticket.status.value, + "priority": db_ticket.priority.value + } + await db.delete(db_ticket) await db.commit() + # Registrar eliminación en auditoría + try: + await AuditService.log( + db=db, + tenant_id=current_user.tenant_id, + user_id=current_user.id, + action="ticket.delete", + resource_type="ticket", + resource_id=ticket_uuid, + old_values=old_values + ) + await db.commit() + except Exception as e: + # No fallar si falla el audit log + pass + return {"message": "Ticket deleted successfully"} # =================================== diff --git a/backend/app/api/v1/endpoints/users.py b/backend/app/api/v1/endpoints/users.py index 0f52932..102dca1 100644 --- a/backend/app/api/v1/endpoints/users.py +++ b/backend/app/api/v1/endpoints/users.py @@ -9,6 +9,7 @@ import uuid from app.core.database import get_db from app.core.security import security from app.models.user import User, UserRole +from app.services.audit_service import AuditService from app.api import deps router = APIRouter() @@ -147,6 +148,28 @@ async def create_user( db.add(db_user) await db.commit() await db.refresh(db_user) + + # Registrar creación en auditoría + try: + await AuditService.log( + db=db, + tenant_id=current_user.tenant_id, + user_id=current_user.id, + action="user.create", + resource_type="user", + resource_id=db_user.id, + new_values=AuditService.sanitize_values({ + "email": db_user.email, + "first_name": db_user.first_name, + "last_name": db_user.last_name, + "role": db_user.role.value + }) + ) + await db.commit() + except Exception as e: + # No fallar si falla el audit log + pass + return db_user @@ -214,6 +237,15 @@ async def update_user( detail="User not found" ) + # Guardar valores anteriores para audit + old_values = { + "email": db_user.email, + "first_name": db_user.first_name, + "last_name": db_user.last_name, + "role": db_user.role.value, + "is_active": db_user.is_active + } + # Verificar email único si se está cambiando update_data = user_update.model_dump(exclude_unset=True) if "email" in update_data and update_data["email"] != db_user.email: @@ -239,6 +271,32 @@ async def update_user( await db.commit() await db.refresh(db_user) + + # Registrar actualización en auditoría + try: + new_values = { + "email": db_user.email, + "first_name": db_user.first_name, + "last_name": db_user.last_name, + "role": db_user.role.value, + "is_active": db_user.is_active + } + + await AuditService.log( + db=db, + tenant_id=current_user.tenant_id, + user_id=current_user.id, + action="user.update", + resource_type="user", + resource_id=db_user.id, + old_values=AuditService.sanitize_values(old_values), + new_values=AuditService.sanitize_values(new_values) + ) + await db.commit() + except Exception as e: + # No fallar si falla el audit log + pass + return db_user @@ -306,6 +364,28 @@ async def delete_user( # Soft delete db_user.is_active = False await db.commit() + + # Registrar eliminación en auditoría + try: + await AuditService.log( + db=db, + tenant_id=current_user.tenant_id, + user_id=current_user.id, + action="user.delete", + resource_type="user", + resource_id=db_user.id, + old_values={ + "email": db_user.email, + "role": db_user.role.value, + "was_active": True + }, + metadata={"action_type": "soft_delete"} + ) + await db.commit() + except Exception as e: + # No fallar si falla el audit log + pass + return None diff --git a/db/schema.sql b/db/schema.sql index c38f58d..2285005 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -369,10 +369,14 @@ CREATE TABLE audit_logs ( CREATE INDEX idx_audit_logs_tenant_id ON audit_logs(tenant_id); CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id); CREATE INDEX idx_audit_logs_action ON audit_logs(action); -CREATE INDEX idx_audit_logs_resource ON audit_logs(resource_type, resource_id); CREATE INDEX idx_audit_logs_correlation_id ON audit_logs(correlation_id); CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at); +-- Índices compuestos para queries comunes de auditoría +CREATE INDEX idx_audit_logs_tenant_action ON audit_logs(tenant_id, action); +CREATE INDEX idx_audit_logs_resource ON audit_logs(resource_type, resource_id); +CREATE INDEX idx_audit_logs_user_created ON audit_logs(user_id, created_at); + -- =================================== -- FUNCIONES Y TRIGGERS -- =================================== diff --git a/frontend-internal/src/lib/components/Modal.svelte b/frontend-internal/src/lib/components/Modal.svelte index 9100d6a..62010be 100644 --- a/frontend-internal/src/lib/components/Modal.svelte +++ b/frontend-internal/src/lib/components/Modal.svelte @@ -1,8 +1,9 @@ @@ -23,21 +38,45 @@