ppt-tool/backend/services/audit_service.py
Vadym Samoilenko cf21ba4516 Phase 1-2: Foundation + Admin Panel & Client Management
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>
2026-02-26 15:37:17 +00:00

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)