- Manufacturing router now uses shared/audit.log_action (Postgres) instead of the separate manufacturing/audit.py (SQLite mfg_audit_log), so all manufacturing events appear in the Log Viewer - Added log_action calls to 5 previously unlogged endpoints: lifecycle patch, lifecycle create, lifecycle delete, flash asset upload, flash asset note - Removed the now-redundant /manufacturing/audit-log endpoint - Log Viewer restricted to sysadmin only: backend uses require_sysadmin (was require_admin_or_above), frontend adds role guard on the page - Fixed Action badge column clipping: table-layout auto + whiteSpace nowrap so the column sizes to fit the widest badge (Status Change) - Added device_batch entity type to Log Viewer entity labels and filters Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
75 lines
2.2 KiB
Python
75 lines
2.2 KiB
Python
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
from sqlalchemy import select, and_
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from database.postgres import get_pg_session
|
|
from shared.orm import AuditLog
|
|
from auth.dependencies import require_sysadmin
|
|
from auth.models import TokenPayload
|
|
|
|
router = APIRouter(prefix="/api/audit-log", tags=["audit-log"])
|
|
|
|
_MAX_LIMIT = 200
|
|
_DEFAULT_LIMIT = 50
|
|
|
|
|
|
@router.get("")
|
|
async def list_audit_log(
|
|
actor_id: Optional[str] = Query(None),
|
|
entity_type: Optional[str] = Query(None),
|
|
entity_id: Optional[str] = Query(None),
|
|
action: Optional[str] = Query(None),
|
|
from_date: Optional[datetime] = Query(None),
|
|
to_date: Optional[datetime] = Query(None),
|
|
limit: int = Query(_DEFAULT_LIMIT, ge=1, le=_MAX_LIMIT),
|
|
offset: int = Query(0, ge=0),
|
|
_user: TokenPayload = Depends(require_sysadmin),
|
|
db: AsyncSession = Depends(get_pg_session),
|
|
):
|
|
filters = []
|
|
if actor_id:
|
|
filters.append(AuditLog.actor_id == actor_id)
|
|
if entity_type:
|
|
filters.append(AuditLog.entity_type == entity_type)
|
|
if entity_id:
|
|
filters.append(AuditLog.entity_id == entity_id)
|
|
if action:
|
|
filters.append(AuditLog.action == action)
|
|
if from_date:
|
|
filters.append(AuditLog.occurred_at >= from_date)
|
|
if to_date:
|
|
filters.append(AuditLog.occurred_at <= to_date)
|
|
|
|
stmt = (
|
|
select(AuditLog)
|
|
.where(and_(*filters) if filters else True)
|
|
.order_by(AuditLog.occurred_at.desc())
|
|
.offset(offset)
|
|
.limit(limit)
|
|
)
|
|
result = await db.execute(stmt)
|
|
rows = result.scalars().all()
|
|
|
|
return {
|
|
"entries": [
|
|
{
|
|
"id": r.id,
|
|
"occurred_at": r.occurred_at.isoformat(),
|
|
"actor_id": r.actor_id,
|
|
"actor_name": r.actor_name,
|
|
"action": r.action,
|
|
"entity_type": r.entity_type,
|
|
"entity_id": r.entity_id,
|
|
"entity_label": r.entity_label,
|
|
"changes": r.changes,
|
|
"meta": r.meta,
|
|
}
|
|
for r in rows
|
|
],
|
|
"limit": limit,
|
|
"offset": offset,
|
|
}
|