"""Push notifications about new mail: the server's public key, and the devices that want them (Settings → Accounts → Notifications).""" from __future__ import annotations from typing import Annotated from fastapi import APIRouter, Depends, Request from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession from app.api.errors import raise_api_error from app.db import get_session from app.identity.deps import CurrentSession, CurrentUser, require_csrf from app.push import service, webpush router = APIRouter(prefix="/push", tags=["push"], dependencies=[Depends(require_csrf)]) DbSession = Annotated[AsyncSession, Depends(get_session)] class PushKeyOut(BaseModel): public_key: str class PushKeys(BaseModel): p256dh: str = Field(min_length=1, max_length=200) auth: str = Field(min_length=1, max_length=100) class SubscriptionIn(BaseModel): """The browser's ``PushSubscription.toJSON()``.""" endpoint: str = Field(min_length=1, max_length=2000) keys: PushKeys class EndpointIn(BaseModel): endpoint: str = Field(min_length=1, max_length=2000) @router.get("/key", response_model=PushKeyOut) async def push_key(user: CurrentUser, db: DbSession) -> PushKeyOut: """The key a browser subscribes with (``applicationServerKey``).""" return PushKeyOut(public_key=(await service.vapid(db)).public_b64) @router.post("/subscriptions") async def add_subscription( body: SubscriptionIn, request: Request, user: CurrentUser, session: CurrentSession, db: DbSession, ) -> dict[str, str]: if not webpush.allowed_endpoint(body.endpoint): raise_api_error(400, "push_service", "not a known push service") try: p256dh = webpush.b64u_decode(body.keys.p256dh) auth = webpush.b64u_decode(body.keys.auth) if len(p256dh) != 65 or len(auth) != 16: raise ValueError except ValueError: raise_api_error(400, "push_keys", "invalid subscription keys") await service.subscribe( db, user_id=user.id, session_sid=session.sid, endpoint=body.endpoint, p256dh=body.keys.p256dh, auth=body.keys.auth, user_agent=request.headers.get("user-agent", ""), ) return {"status": "ok"} @router.post("/subscriptions/remove") async def remove_subscription(body: EndpointIn, user: CurrentUser, db: DbSession) -> dict[str, str]: await service.unsubscribe(db, user_id=user.id, endpoint=body.endpoint) return {"status": "ok"}