2026-04-17 10:34:51 +05:30
|
|
|
from typing import List, Optional
|
2026-04-03 12:15:56 +05:30
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
|
|
|
from sqlalchemy.orm import Session
|
2026-04-17 10:34:51 +05:30
|
|
|
from sqlalchemy import asc, desc, or_, cast, String
|
2026-04-03 12:15:56 +05:30
|
|
|
|
|
|
|
|
from app.config.database import get_db
|
|
|
|
|
from app.middleware.auth_middleware import get_current_user, User
|
2026-08-31 20:04:12 -04:00
|
|
|
from app.middleware.tenant_middleware import is_superadmin
|
2026-04-03 12:15:56 +05:30
|
|
|
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"),
|
2026-04-17 10:34:51 +05:30
|
|
|
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"),
|
2026-04-03 12:15:56 +05:30
|
|
|
limit: int = Query(50, ge=1, le=500),
|
|
|
|
|
offset: int = Query(0, ge=0),
|
|
|
|
|
):
|
2026-08-31 20:04:12 -04:00
|
|
|
"""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.
|
2026-04-03 12:15:56 +05:30
|
|
|
"""
|
|
|
|
|
query = db.query(AuditLog)
|
2026-08-31 20:04:12 -04:00
|
|
|
|
|
|
|
|
if not is_superadmin(current_user):
|
|
|
|
|
query = query.filter(AuditLog.tenant_id == current_user.tenant_id)
|
2026-04-03 12:15:56 +05:30
|
|
|
|
|
|
|
|
if module_name:
|
|
|
|
|
query = query.filter(AuditLog.module_name == module_name)
|
2026-04-17 10:34:51 +05:30
|
|
|
if module_names:
|
|
|
|
|
query = query.filter(AuditLog.module_name.in_(module_names))
|
2026-04-03 12:15:56 +05:30
|
|
|
if action_type:
|
|
|
|
|
query = query.filter(AuditLog.action_type == action_type.upper())
|
2026-04-17 10:34:51 +05:30
|
|
|
if action_types:
|
|
|
|
|
normalized_action_types = [item.upper() for item in action_types]
|
|
|
|
|
query = query.filter(AuditLog.action_type.in_(normalized_action_types))
|
2026-04-03 12:15:56 +05:30
|
|
|
if performed_by_email:
|
|
|
|
|
query = query.filter(
|
|
|
|
|
AuditLog.performed_by_email.ilike(f"%{performed_by_email}%")
|
|
|
|
|
)
|
2026-04-17 10:34:51 +05:30
|
|
|
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}%"),
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-04-03 12:15:56 +05:30
|
|
|
|
|
|
|
|
total = query.count()
|
2026-04-17 10:34:51 +05:30
|
|
|
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
|
2026-04-03 12:15:56 +05:30
|
|
|
items = (
|
2026-04-17 10:34:51 +05:30
|
|
|
query.order_by(order_fn(sort_column))
|
2026-04-03 12:15:56 +05:30
|
|
|
.offset(offset)
|
|
|
|
|
.limit(limit)
|
|
|
|
|
.all()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return AuditLogListResponse(
|
|
|
|
|
items=items,
|
|
|
|
|
total=total,
|
|
|
|
|
limit=limit,
|
|
|
|
|
offset=offset,
|
2026-08-31 20:39:41 -04:00
|
|
|
)
|