"""Личный кабинет любого аутентифицированного пользователя gnexus-auth.
Роли user (не admin) в админ-часть не пускаются (require_admin), но личное
им доступно: /me — профиль; /me/events — события, которые правила
маршрутизации отправили в канал user (личный лог) с адресатом этим
пользователем (payload.user_id == sub, конвенция docs/05-ingestion-api.md).
Без ролевого гейта: get_current_user — только аутентификация call-home.
"""
import uuid
from datetime import datetime
from fastapi import Depends, Query
from fastapi import APIRouter
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.auth.deps import get_current_user
from app.config import get_settings
from app.database import get_db
from app.api.routes import AdminMe
from app.models import Delivery, Event, Source
router = APIRouter(prefix="/api/v1", tags=["me"])
class MeEventOut(BaseModel):
id: uuid.UUID
source_name: str
subject: str
action: str
priority: str
payload: dict
created_at: datetime
delivered_at: datetime | None
rendered_message: str | None
@router.get("/me", response_model=AdminMe)
def me(user=Depends(get_current_user)) -> AdminMe:
"""Профиль по токену gnexus-auth (любая роль). SPA-гейт решает по system_role."""
return AdminMe(
sub=str(user.user_id),
email=user.email,
system_role=user.system_role or "unknown",
username=user.profile.get("username"),
display_name=user.profile.get("display_name"),
avatar_url=user.profile.get("avatar_url"),
account_url=f"{get_settings().gauth_base_url.rstrip('/')}/account/profile",
)
@router.get("/me/events", response_model=list[MeEventOut])
def my_events(
user=Depends(get_current_user),
db: Session = Depends(get_db),
limit: int = Query(50, ge=1, le=200),
) -> list[MeEventOut]:
"""События, направленные лично этому пользователю.
Только доставки канала user его адресата: событие попадает сюда
исключительно через правило маршрутизации. Строгий фильтр по sub —
чужое не видно никогда. Дедуп по событию (одно событие могут направить
два правила — показываем один раз).
"""
sub = str(user.user_id)
rows = (
db.execute(
select(Delivery, Event, Source.name)
.join(Event, Event.id == Delivery.event_id)
.join(Source, Source.id == Event.source_id)
.where(
Delivery.channel == "user",
Delivery.recipient_user_id == sub,
Delivery.status == "delivered",
)
.order_by(Delivery.created_at.desc(), Delivery.id.desc())
.limit(limit * 2) # запас на дубли двух правил
)
.all()
)
seen: set[uuid.UUID] = set()
out: list[MeEventOut] = []
for delivery, event, source_name in rows:
if event.id in seen:
continue
seen.add(event.id)
out.append(
MeEventOut(
id=event.id,
source_name=source_name,
subject=event.subject,
action=event.action,
priority=event.priority,
payload=dict(event.payload or {}),
created_at=event.created_at,
delivered_at=delivery.delivered_at,
rendered_message=delivery.rendered_message,
)
)
if len(out) >= limit:
break
return out