gendesign/tradein-mvp/backend/app/core/request_audit.py
bot-backend 82928be326
All checks were successful
CI Trade-In / backend-tests (pull_request) Successful in 59s
CI / changes (pull_request) Successful in 9s
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
feat(tradein/audit): request-audit middleware + estimate_request logging → user_events
Server-side capture layer for Feature 2 (login/IP audit) + base for Feature 3
(behavior analytics), on top of the already-deployed user_events table
(migration 184). record_event/schedule_event never raise into the caller —
audit logging must never break a real request.
2026-07-13 23:04:24 +03:00

66 lines
2.9 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-запрос.
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