"""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"}