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>
77 lines
2.4 KiB
Python
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
|