- Fix API path: frontend now calls /audit/logs (was /audit) - Backend eagerly loads User relationship for audit entries - Backend response includes user_name field instead of just user_id - Frontend logs page fetches real data with pagination - Derive INFO/WARN/ERROR levels from action type - Format details JSON into readable descriptions - Add loading state and empty state handling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
79 lines
2.4 KiB
Python
79 lines
2.4 KiB
Python
from datetime import datetime
|
|
from typing import Any
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy import select, func
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
from app.models.audit import AuditLog
|
|
|
|
|
|
class AuditService:
|
|
"""Service for audit log creation and retrieval."""
|
|
|
|
async def log(
|
|
self,
|
|
db: AsyncSession,
|
|
action: str,
|
|
entity_type: str,
|
|
entity_id: str,
|
|
user_id: UUID | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
ip_address: str | None = None,
|
|
) -> AuditLog:
|
|
"""Create an audit log entry."""
|
|
entry = AuditLog(
|
|
user_id=user_id,
|
|
action=action,
|
|
entity_type=entity_type,
|
|
entity_id=entity_id,
|
|
details=details,
|
|
ip_address=ip_address,
|
|
)
|
|
db.add(entry)
|
|
await db.flush()
|
|
return entry
|
|
|
|
async def list_logs(
|
|
self,
|
|
db: AsyncSession,
|
|
user_id: UUID | None = None,
|
|
action: str | None = None,
|
|
entity_type: str | None = None,
|
|
entity_id: str | None = None,
|
|
date_from: datetime | None = None,
|
|
date_to: datetime | None = None,
|
|
page: int = 1,
|
|
page_size: int = 50,
|
|
) -> tuple[list[AuditLog], int]:
|
|
"""List audit logs with filters and pagination."""
|
|
query = select(AuditLog)
|
|
|
|
if user_id:
|
|
query = query.where(AuditLog.user_id == user_id)
|
|
if action:
|
|
query = query.where(AuditLog.action == action)
|
|
if entity_type:
|
|
query = query.where(AuditLog.entity_type == entity_type)
|
|
if entity_id:
|
|
query = query.where(AuditLog.entity_id == entity_id)
|
|
if date_from:
|
|
query = query.where(AuditLog.timestamp >= date_from)
|
|
if date_to:
|
|
query = query.where(AuditLog.timestamp <= date_to)
|
|
|
|
# Count
|
|
count_query = select(func.count()).select_from(query.subquery())
|
|
total_result = await db.execute(count_query)
|
|
total = total_result.scalar() or 0
|
|
|
|
# Paginate and eager-load user relationship for name display
|
|
query = query.order_by(AuditLog.timestamp.desc())
|
|
query = query.offset((page - 1) * page_size).limit(page_size)
|
|
query = query.options(selectinload(AuditLog.user))
|
|
|
|
result = await db.execute(query)
|
|
logs = list(result.scalars().all())
|
|
|
|
return logs, total
|