"""Запись событий в журнал аудита.""" from __future__ import annotations import uuid from typing import Any from fastapi import Request from sqlalchemy.ext.asyncio import AsyncSession from app.db.models.audit import AuditLog from app.db.models.user import User # Ключи, значения которых не должны попасть в журнал даже случайно. _REDACTED_KEYS = { "password", "new_password", "current_password", "token", "access_token", "refresh_token", "secret", "license_key", "api_key", "pin", "pin_code", "cashier_pin", } def _sanitize(payload: dict[str, Any]) -> dict[str, Any]: """Журнал аудита читают люди и он живёт годами — секретам там не место.""" return { key: ("***" if key.lower() in _REDACTED_KEYS else value) for key, value in payload.items() } def client_ip(request: Request | None) -> str | None: if request is None: return None # За nginx реальный адрес приходит в X-Forwarded-For; первый элемент — клиент. forwarded = request.headers.get("x-forwarded-for") if forwarded: return forwarded.split(",")[0].strip() return request.client.host if request.client else None async def record( session: AsyncSession, *, action: str, user: User | None = None, actor_label: str | None = None, entity_type: str | None = None, entity_id: str | uuid.UUID | None = None, payload: dict[str, Any] | None = None, request: Request | None = None, ) -> AuditLog: """Добавляет запись в журнал. Без commit: вызывающий код сам решает границы транзакции, чтобы событие и изменение состояния фиксировались вместе либо не фиксировались вовсе. """ entry = AuditLog( user_id=user.id if user else None, actor_label=actor_label or (user.email if user else None), action=action, entity_type=entity_type, entity_id=str(entity_id) if entity_id is not None else None, payload=_sanitize(payload or {}), ip=client_ip(request), user_agent=(request.headers.get("user-agent") if request else None), ) session.add(entry) return entry