Initial commit: backend scaffold, auth, frontend login
- FastAPI + SQLAlchemy async + Alembic + Postgres backend - Auth: JWT access + rotating refresh tokens, argon2, roles, audit log - React 19 + Vite frontend: login page, protected route, auth context - Docker Compose: postgres, redis, migrate, api, worker (placeholder), frontend/nginx Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
"""Запись событий в журнал аудита."""
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user