Phase 1 (Foundation): - Project restructure (presenton-main → backend/ + frontend/) - Database schema (8 new models, Alembic config, seed script) - Auth (Azure AD SSO + dev bypass, JWT sessions, AuthMiddleware) - RBAC (access_service, rbac_middleware, admin routers) - Audit logging (fire-and-forget, AuditMiddleware, admin router) - i18n (react-i18next with 5 namespace files) Phase 2 (Admin Panel & Client Management): - Admin panel shell (sidebar layout, role guard, 12 pages) - Redux admin slice with 18 async thunks - User management (role changes, deactivation) - Client management (CRUD, brand config, team management) - Brand config editor (colors, fonts, logos, voice rules) - Master deck upload & parser (PPTX → HTML → React pipeline) - Audit log viewer with filters and CSV/JSON export Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
126 lines
4.2 KiB
Python
126 lines
4.2 KiB
Python
"""Service for creating and querying audit log entries."""
|
|
import asyncio
|
|
import csv
|
|
import io
|
|
import json
|
|
import uuid
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlmodel import select
|
|
|
|
from models.sql.audit_log import AuditLogModel
|
|
from services.database import async_session_maker
|
|
|
|
|
|
def log(
|
|
user_id: Optional[uuid.UUID],
|
|
action: str,
|
|
resource_type: str,
|
|
resource_id: Optional[uuid.UUID] = None,
|
|
client_id: Optional[uuid.UUID] = None,
|
|
details: Optional[dict] = None,
|
|
ip_address: Optional[str] = None,
|
|
) -> None:
|
|
"""Fire-and-forget audit log entry creation."""
|
|
asyncio.create_task(
|
|
_write_log(user_id, action, resource_type, resource_id, client_id, details, ip_address)
|
|
)
|
|
|
|
|
|
async def _write_log(
|
|
user_id: Optional[uuid.UUID],
|
|
action: str,
|
|
resource_type: str,
|
|
resource_id: Optional[uuid.UUID],
|
|
client_id: Optional[uuid.UUID],
|
|
details: Optional[dict],
|
|
ip_address: Optional[str],
|
|
) -> None:
|
|
try:
|
|
async with async_session_maker() as session:
|
|
entry = AuditLogModel(
|
|
user_id=user_id,
|
|
action=action,
|
|
resource_type=resource_type,
|
|
resource_id=resource_id,
|
|
client_id=client_id,
|
|
details=details,
|
|
ip_address=ip_address,
|
|
)
|
|
session.add(entry)
|
|
await session.commit()
|
|
except Exception as e:
|
|
# Audit logging should never break the request
|
|
print(f"Audit log error: {e}")
|
|
|
|
|
|
async def query(
|
|
session: AsyncSession,
|
|
user_id: Optional[uuid.UUID] = None,
|
|
action: Optional[str] = None,
|
|
resource_type: Optional[str] = None,
|
|
client_id: Optional[uuid.UUID] = None,
|
|
date_from: Optional[datetime] = None,
|
|
date_to: Optional[datetime] = None,
|
|
offset: int = 0,
|
|
limit: int = 50,
|
|
) -> list:
|
|
"""Query audit logs with optional filters."""
|
|
q = select(AuditLogModel)
|
|
|
|
if user_id:
|
|
q = q.where(AuditLogModel.user_id == user_id)
|
|
if action:
|
|
q = q.where(AuditLogModel.action == action)
|
|
if resource_type:
|
|
q = q.where(AuditLogModel.resource_type == resource_type)
|
|
if client_id:
|
|
q = q.where(AuditLogModel.client_id == client_id)
|
|
if date_from:
|
|
q = q.where(AuditLogModel.created_at >= date_from)
|
|
if date_to:
|
|
q = q.where(AuditLogModel.created_at <= date_to)
|
|
|
|
q = q.order_by(AuditLogModel.created_at.desc()).offset(offset).limit(limit)
|
|
result = await session.execute(q)
|
|
return list(result.scalars().all())
|
|
|
|
|
|
def export_csv(logs: list) -> str:
|
|
"""Export audit logs as CSV string."""
|
|
output = io.StringIO()
|
|
writer = csv.writer(output)
|
|
writer.writerow(["id", "user_id", "action", "resource_type", "resource_id", "client_id", "details", "ip_address", "created_at"])
|
|
for log_entry in logs:
|
|
writer.writerow([
|
|
str(log_entry.id),
|
|
str(log_entry.user_id) if log_entry.user_id else "",
|
|
log_entry.action,
|
|
log_entry.resource_type,
|
|
str(log_entry.resource_id) if log_entry.resource_id else "",
|
|
str(log_entry.client_id) if log_entry.client_id else "",
|
|
json.dumps(log_entry.details) if log_entry.details else "",
|
|
log_entry.ip_address or "",
|
|
log_entry.created_at.isoformat() if log_entry.created_at else "",
|
|
])
|
|
return output.getvalue()
|
|
|
|
|
|
def export_json(logs: list) -> str:
|
|
"""Export audit logs as JSON string."""
|
|
entries = []
|
|
for log_entry in logs:
|
|
entries.append({
|
|
"id": str(log_entry.id),
|
|
"user_id": str(log_entry.user_id) if log_entry.user_id else None,
|
|
"action": log_entry.action,
|
|
"resource_type": log_entry.resource_type,
|
|
"resource_id": str(log_entry.resource_id) if log_entry.resource_id else None,
|
|
"client_id": str(log_entry.client_id) if log_entry.client_id else None,
|
|
"details": log_entry.details,
|
|
"ip_address": log_entry.ip_address,
|
|
"created_at": log_entry.created_at.isoformat() if log_entry.created_at else None,
|
|
})
|
|
return json.dumps(entries, indent=2)
|