import json import logging from fastapi import APIRouter, Depends, HTTPException from pywebpush import WebPushException, webpush from sqlmodel import Session, select from app.auth import get_current_user from app.config import settings from app.db import get_session from app.models.models import PushSubscription, PushSubscriptionCreate logger = logging.getLogger(__name__) router = APIRouter(prefix="/notifications", tags=["notifications"]) @router.get("/vapid-public-key") def get_vapid_public_key(_user: str = Depends(get_current_user)): if not settings.VAPID_PUBLIC_KEY: raise HTTPException(status_code=503, detail="Push notifications not configured") return {"publicKey": settings.VAPID_PUBLIC_KEY} @router.post("/subscribe", status_code=201) def subscribe( data: PushSubscriptionCreate, session: Session = Depends(get_session), _user: str = Depends(get_current_user), ): existing = session.exec( select(PushSubscription).where(PushSubscription.endpoint == data.endpoint) ).first() if existing: existing.p256dh = data.keys["p256dh"] existing.auth = data.keys["auth"] session.add(existing) session.commit() return {"status": "updated"} sub = PushSubscription( endpoint=data.endpoint, p256dh=data.keys["p256dh"], auth=data.keys["auth"], ) session.add(sub) session.commit() return {"status": "subscribed"} @router.delete("/unsubscribe") def unsubscribe( data: PushSubscriptionCreate, session: Session = Depends(get_session), _user: str = Depends(get_current_user), ): existing = session.exec( select(PushSubscription).where(PushSubscription.endpoint == data.endpoint) ).first() if existing: session.delete(existing) session.commit() return {"status": "unsubscribed"} def send_push_to_all(session: Session, title: str, body: str, url: str = "/"): """Send a push notification to all registered subscriptions.""" if not settings.VAPID_PRIVATE_KEY or not settings.VAPID_PUBLIC_KEY: logger.debug("VAPID keys not configured, skipping push notification") return subscriptions = session.exec(select(PushSubscription)).all() payload = json.dumps({"title": title, "body": body, "url": url}) for sub in subscriptions: subscription_info = { "endpoint": sub.endpoint, "keys": {"p256dh": sub.p256dh, "auth": sub.auth}, } try: webpush( subscription_info=subscription_info, data=payload, vapid_private_key=settings.VAPID_PRIVATE_KEY, vapid_claims={"sub": settings.VAPID_CLAIM_EMAIL}, ) except WebPushException as e: logger.warning("Push failed for %s: %s", sub.endpoint[:50], e) if e.response and e.response.status_code in (404, 410): session.delete(sub) session.commit() except Exception: logger.exception("Unexpected push error for %s", sub.endpoint[:50])