Newer
Older
navi-1 / navi / api / routes / push.py
"""Web push (PWA) endpoints — VAPID key, subscribe / unsubscribe."""

from typing import Annotated

import structlog
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field

from navi.api.deps import get_push_service
from navi.auth.deps import User, require_user

log = structlog.get_logger()
router = APIRouter(prefix="/push", tags=["push"])

_MAX_ENDPOINT = 2048


class SubscribePayload(BaseModel):
    endpoint: str = Field(min_length=1, max_length=_MAX_ENDPOINT)
    keys: dict = Field(...)
    user_agent: str | None = Field(default=None, max_length=512)


def _service():
    service = get_push_service()
    if not service.enabled:
        raise HTTPException(status_code=503, detail="Web push is not configured")
    return service


@router.get("/vapid-key")
async def vapid_key() -> dict:
    service = _service()
    return {"public_key": service.public_key}


@router.post("/subscribe")
async def subscribe(payload: SubscribePayload, user: Annotated[User, Depends(require_user)]) -> dict:
    service = _service()
    keys = payload.keys or {}
    p256dh = keys.get("p256dh", "")
    auth = keys.get("auth", "")
    if not p256dh or not auth:
        raise HTTPException(status_code=422, detail="keys.p256dh and keys.auth are required")
    sub_id = await service.subscribe(
        user_id=user.id,
        endpoint=payload.endpoint,
        p256dh=p256dh,
        auth=auth,
        user_agent=payload.user_agent,
    )
    log.info("push.subscribed", user_id=user.id, endpoint=payload.endpoint[:120])
    return {"id": sub_id, "status": "subscribed"}


@router.delete("/subscribe")
async def unsubscribe(payload: SubscribePayload, user: Annotated[User, Depends(require_user)]) -> dict:
    service = _service()
    await service.unsubscribe(payload.endpoint)
    log.info("push.unsubscribed", user_id=user.id, endpoint=payload.endpoint[:120])
    return {"status": "unsubscribed"}