All checks were successful
Deploy Trade-In / build-backend (push) Successful in 57s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 30s
Deploy Trade-In / test (push) Successful in 4m49s
Deploy Trade-In / deploy (push) Successful in 50s
Deploy Trade-In / changes (push) Successful in 10s
Admin-only read API: GET /admin/audit/accounts, /admin/audit/accounts/{username}, /admin/analytics over user_events. Read-only, empty-safe, RBAC via central gate.
146 lines
4.1 KiB
Python
146 lines
4.1 KiB
Python
"""Pydantic-схемы admin read API над `user_events` (Feature 2 audit + Feature 3 analytics).
|
||
|
||
`user_events` (migration `184_user_events.sql`) — unified append-only событийный лог
|
||
(login/IP audit + behavior analytics), admin-read-only. Эти схемы описывают ответы
|
||
`GET /api/v1/admin/audit/*` и `GET /api/v1/admin/analytics` (см. app/api/v1/audit.py).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import date, datetime
|
||
|
||
from pydantic import BaseModel, ConfigDict
|
||
|
||
|
||
class AccountSummary(BaseModel):
|
||
"""Одна строка списка `GET /audit/accounts` — сводка по одному username."""
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
username: str
|
||
first_seen_at: datetime
|
||
last_seen_at: datetime
|
||
distinct_ips: int
|
||
distinct_devices: int
|
||
login_count: int
|
||
request_count: int
|
||
search_count: int
|
||
|
||
|
||
class AccountIpEntry(BaseModel):
|
||
"""Одна строка `ips` в drilldown `GET /audit/accounts/{username}`."""
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
ip_address: str | None = None
|
||
event_count: int
|
||
first_seen: datetime
|
||
last_seen: datetime
|
||
|
||
|
||
class AccountDeviceEntry(BaseModel):
|
||
"""Одна строка `devices` в drilldown `GET /audit/accounts/{username}`."""
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
user_agent: str | None = None
|
||
event_count: int
|
||
first_seen: datetime
|
||
last_seen: datetime
|
||
|
||
|
||
class AccountSearchEntry(BaseModel):
|
||
"""Одна строка `searches` в drilldown — недавний estimate_request."""
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
address: str | None = None
|
||
area_m2: str | None = None
|
||
rooms: str | None = None
|
||
estimate_id: str | None = None
|
||
ip_address: str | None = None
|
||
created_at: datetime
|
||
|
||
|
||
class AccountActivityEntry(BaseModel):
|
||
"""Одна строка `recent_activity` в drilldown — сырое событие."""
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
event_type: str
|
||
path: str | None = None
|
||
method: str | None = None
|
||
ip_address: str | None = None
|
||
created_at: datetime
|
||
|
||
|
||
class AccountDrilldown(BaseModel):
|
||
"""Полный ответ `GET /audit/accounts/{username}` — 4 списка.
|
||
|
||
Неизвестный username НЕ 404 — это просто пустой отчёт (все списки == []).
|
||
"""
|
||
|
||
ips: list[AccountIpEntry]
|
||
devices: list[AccountDeviceEntry]
|
||
searches: list[AccountSearchEntry]
|
||
recent_activity: list[AccountActivityEntry]
|
||
|
||
|
||
class AnalyticsSummary(BaseModel):
|
||
"""Верхнеуровневые счётчики дашборда `GET /analytics`."""
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
total_events: int
|
||
distinct_users: int
|
||
events_last_24h: int
|
||
active_users_last_24h: int
|
||
|
||
|
||
class AnalyticsDailyPoint(BaseModel):
|
||
"""Одна точка time-series `daily` — события/пользователи за день."""
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
day: date
|
||
events: int
|
||
users: int
|
||
|
||
|
||
class AnalyticsTopSearch(BaseModel):
|
||
"""Одна строка `top_searches` — самый частый искомый адрес."""
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
address: str | None = None
|
||
n: int
|
||
|
||
|
||
class AnalyticsTopPath(BaseModel):
|
||
"""Одна строка `top_paths` — самый частый API-путь."""
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
path: str | None = None
|
||
n: int
|
||
|
||
|
||
class AnalyticsByAccount(BaseModel):
|
||
"""Одна строка `by_account` — сводка активности по username."""
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
username: str
|
||
events: int
|
||
searches: int
|
||
last_seen: datetime
|
||
|
||
|
||
class AnalyticsDashboard(BaseModel):
|
||
"""Полный ответ `GET /analytics` — бандл для дашборда Feature 3."""
|
||
|
||
summary: AnalyticsSummary
|
||
daily: list[AnalyticsDailyPoint]
|
||
top_searches: list[AnalyticsTopSearch]
|
||
top_paths: list[AnalyticsTopPath]
|
||
by_account: list[AnalyticsByAccount]
|