gendesign/tradein-mvp/backend/app/core/request_audit.py
lekss361 e61c2debe5
Some checks failed
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Has been cancelled
fix(tradein/audit): exclude /api/v1/admin/* from api_request events (#2522)
Admin dashboard traffic to /api/v1/admin/* no longer logged as api_request (would pollute behavior analytics); login events kept for IP audit.
2026-07-13 20:55:16 +00:00

71 lines
3.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""RequestAuditMiddleware — пишет `api_request` (+ дедуплицированный `login`)
события в `user_events` для каждого аутентифицированного `/api/*` запроса.
Foundation для Feature 2 (login/IP audit) и базы Feature 3 (behavior analytics).
Логирование выполняется ПОСЛЕ `call_next` (не задерживает и не ветвит реальный
ответ клиенту) и через fire-and-forget `schedule_event` — сбой аудита никогда
не влияет на HTTP-ответ.
"""
from __future__ import annotations
import logging
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response
from app.core.ratelimit import _client_ip
from app.services.user_events import schedule_event, should_log_login
logger = logging.getLogger(__name__)
# Зеркалит app.main._PUBLIC_PATHS. Не импортируем напрямую из app.main — оно
# импортирует этот модуль (регистрирует middleware), обратный импорт дал бы
# циклическую зависимость.
_PUBLIC_PATHS = frozenset({"/health", "/docs", "/redoc", "/openapi.json"})
class RequestAuditMiddleware(BaseHTTPMiddleware):
"""Логирует активность аутентифицированных пользователей в `user_events`."""
async def dispatch(self, request: Request, call_next): # type: ignore[no-untyped-def]
response: Response = await call_next(request)
try:
username = request.headers.get("x-authenticated-user")
path = request.url.path
if username and path.startswith("/api/") and path not in _PUBLIC_PATHS:
ip = _client_ip(request)
ua = request.headers.get("user-agent")
# Общий behavior/activity-поток — каждый authenticated API-запрос.
# /api/v1/admin/* исключаем: это ops-действия (просмотр самих
# дашбордов аудита/аналитики), а не поведение пилота — иначе
# запросы дашборда зашумляют top_paths и счётчики активности.
# login ниже логируем всегда (вход админа с IP — валидный аудит).
if not path.startswith("/api/v1/admin/"):
schedule_event(
event_type="api_request",
username=username,
ip=ip,
user_agent=ua,
path=path,
method=request.method,
)
# Дедуплицированный login/IP-audit сигнал — максимум раз в день
# на (юзер, IP, устройство).
if should_log_login(username, ip, ua):
schedule_event(
event_type="login",
username=username,
ip=ip,
user_agent=ua,
path=path,
method=request.method,
)
except Exception:
logger.warning("RequestAuditMiddleware: failed to record event", exc_info=True)
return response