Files
saas_backend/app/routes/admin/audit_logs.py
T
2026-08-31 20:39:41 -04:00

100 lines
4.3 KiB
Python

from typing import List, Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from sqlalchemy import asc, desc, or_, cast, String
from app.config.database import get_db
from app.middleware.auth_middleware import get_current_user, User
from app.middleware.tenant_middleware import is_superadmin
from app.models.system.audit_log import AuditLog
from app.schemas.auth.audit_schema import AuditLogListResponse
router = APIRouter()
@router.get("/", response_model=AuditLogListResponse)
def get_audit_logs(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
module_name: Optional[str] = Query(None, description="Filter by module name"),
action_type: Optional[str] = Query(None, description="CREATE | UPDATE | DELETE"),
performed_by_email: Optional[str] = Query(None, description="Filter by actor email"),
search: Optional[str] = Query(None, description="Search audit logs"),
module_names: Optional[List[str]] = Query(None, description="Filter by module names"),
action_types: Optional[List[str]] = Query(None, description="Filter by action types"),
performed_by_emails: Optional[List[str]] = Query(None, description="Filter by actor emails"),
sort_by: Optional[str] = Query("created_at", description="Sort by created_at, module_name, action_type, or performed_by_email"),
sort_order: Optional[str] = Query("desc", description="Sort order: asc or desc"),
limit: int = Query(50, ge=1, le=500),
offset: int = Query(0, ge=0),
):
"""Audit entries for the caller's workspace, newest first.
"Super-admin in practice" is what the previous comment claimed, and the only
dependency was `get_current_user`. There was no workspace filter and, until
migration f6b8c2d4e104, no workspace column to filter on — so any user of any
customer could read every audit entry on the platform: who did what, to which
named entity, from which address, with the full before-and-after values of
workspace and plan changes, and every administrator's email address across
every customer.
A superadmin sees everything, including the unattributed entries written
before the column existed. Everyone else sees their own workspace and nothing
beyond it.
"""
query = db.query(AuditLog)
if not is_superadmin(current_user):
query = query.filter(AuditLog.tenant_id == current_user.tenant_id)
if module_name:
query = query.filter(AuditLog.module_name == module_name)
if module_names:
query = query.filter(AuditLog.module_name.in_(module_names))
if action_type:
query = query.filter(AuditLog.action_type == action_type.upper())
if action_types:
normalized_action_types = [item.upper() for item in action_types]
query = query.filter(AuditLog.action_type.in_(normalized_action_types))
if performed_by_email:
query = query.filter(
AuditLog.performed_by_email.ilike(f"%{performed_by_email}%")
)
if performed_by_emails:
query = query.filter(AuditLog.performed_by_email.in_(performed_by_emails))
if search:
search_term = search.strip()
query = query.filter(
or_(
AuditLog.module_name.ilike(f"%{search_term}%"),
AuditLog.action_type.ilike(f"%{search_term}%"),
AuditLog.entity_name.ilike(f"%{search_term}%"),
AuditLog.description.ilike(f"%{search_term}%"),
AuditLog.performed_by_email.ilike(f"%{search_term}%"),
AuditLog.ip_address.ilike(f"%{search_term}%"),
cast(AuditLog.entity_id, String).ilike(f"%{search_term}%"),
)
)
total = query.count()
sort_column_map = {
"created_at": AuditLog.created_at,
"module_name": AuditLog.module_name,
"action_type": AuditLog.action_type,
"performed_by_email": AuditLog.performed_by_email,
}
sort_column = sort_column_map.get(sort_by or "created_at", AuditLog.created_at)
order_fn = asc if (sort_order or "").lower() == "asc" else desc
items = (
query.order_by(order_fn(sort_column))
.offset(offset)
.limit(limit)
.all()
)
return AuditLogListResponse(
items=items,
total=total,
limit=limit,
offset=offset,
)