Newer
Older
gn-synapse / app / api / me_routes.py
"""Личный кабинет любого аутентифицированного пользователя 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