Files
lux_fiscal/backend/app/services/audit.py
lauadminandClaude Opus 5.5 8b3c2b6d63 Bind Nova Poshta API key to cash register (#3)
Each cash register stores its own encrypted NP API key. Status polling uses
register keys and binds an order to the register whose key sees the TTN as
its own (PhoneSender present); ETTN receipts are created from that register.
Migration 0008 moves the old NOVA_POSHTA_API_KEY into the default register.

Closes #3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 13:02:40 +03:00

77 lines
2.4 KiB
Python

"""Запись событий в журнал аудита."""
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",
"np_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