"""An- und Abmeldung von Geräten für Push-Benachrichtigungen.""" from fastapi import APIRouter, BackgroundTasks, HTTPException, status from sqlalchemy import select from app.config import settings from app.deps import DbSession, VerifiedUser from app.models import PushSubscription from app.push import send_to_user from app.schemas import MessageOut from app.schemas_push import ( PushConfigOut, PushSubscribeIn, PushSubscriptionOut, PushUnsubscribeIn, ) from app.security import utcnow router = APIRouter(prefix="/api/push", tags=["push"]) MAX_DEVICES_PER_USER = 20 @router.get("/config", response_model=PushConfigOut) def push_config(): """Ohne Anmeldung erreichbar: Der öffentliche Schlüssel ist dafür da, veröffentlicht zu werden.""" return PushConfigOut( enabled=settings.push_enabled, public_key=settings.vapid_public_key or None, throttle_hours=settings.push_throttle_hours, ) @router.get("/subscriptions", response_model=list[PushSubscriptionOut]) def my_subscriptions(db: DbSession, user: VerifiedUser): rows = db.scalars( select(PushSubscription) .where(PushSubscription.user_id == user.id) .order_by(PushSubscription.created_at) ).all() return [ PushSubscriptionOut( id=s.id, label=s.label, created_at=s.created_at, last_success_at=s.last_success_at, ) for s in rows ] @router.post("/subscribe", response_model=PushSubscriptionOut, status_code=status.HTTP_201_CREATED) def subscribe(payload: PushSubscribeIn, db: DbSession, user: VerifiedUser): if not settings.push_enabled: raise HTTPException( status.HTTP_503_SERVICE_UNAVAILABLE, "Push-Benachrichtigungen sind auf diesem Server nicht eingerichtet.", ) existing = db.scalar( select(PushSubscription).where(PushSubscription.endpoint == payload.endpoint) ) if existing is not None: # Derselbe Endpunkt kann nach einem Kontowechsel auf demselben # Geraet auftauchen - dann uebernehmen statt abweisen. existing.user_id = user.id existing.p256dh = payload.keys.p256dh existing.auth = payload.keys.auth existing.label = payload.label or existing.label existing.failure_count = 0 db.commit() db.refresh(existing) return PushSubscriptionOut( id=existing.id, label=existing.label, created_at=existing.created_at, last_success_at=existing.last_success_at, ) count = len(db.scalars( select(PushSubscription).where(PushSubscription.user_id == user.id) ).all()) if count >= MAX_DEVICES_PER_USER: raise HTTPException( status.HTTP_409_CONFLICT, f"Höchstens {MAX_DEVICES_PER_USER} Geräte je Konto.", ) subscription = PushSubscription( user_id=user.id, endpoint=payload.endpoint, p256dh=payload.keys.p256dh, auth=payload.keys.auth, label=(payload.label or None), ) db.add(subscription) db.commit() db.refresh(subscription) return PushSubscriptionOut( id=subscription.id, label=subscription.label, created_at=subscription.created_at, last_success_at=None, ) @router.post("/unsubscribe", response_model=MessageOut) def unsubscribe(payload: PushUnsubscribeIn, db: DbSession, user: VerifiedUser): subscription = db.scalar( select(PushSubscription).where( PushSubscription.endpoint == payload.endpoint, PushSubscription.user_id == user.id, ) ) if subscription is not None: db.delete(subscription) db.commit() # Auch wenn nichts gefunden wurde: Fuer den Aufrufer ist das Ziel # erreicht - dieses Geraet bekommt keine Benachrichtigungen mehr. return MessageOut(detail="Benachrichtigungen für dieses Gerät abgeschaltet.") @router.delete("/subscriptions/{subscription_id}", response_model=MessageOut) def remove_subscription(subscription_id: str, db: DbSession, user: VerifiedUser): subscription = db.get(PushSubscription, subscription_id) if subscription is None or subscription.user_id != user.id: raise HTTPException(status.HTTP_404_NOT_FOUND, "Gerät nicht gefunden") db.delete(subscription) db.commit() return MessageOut(detail="Gerät entfernt.") @router.post("/test", response_model=MessageOut, status_code=status.HTTP_202_ACCEPTED) def send_test(background: BackgroundTasks, db: DbSession, user: VerifiedUser): """Testnachricht an die eigenen Geräte - umgeht die Drosselung.""" if not settings.push_enabled: raise HTTPException( status.HTTP_503_SERVICE_UNAVAILABLE, "Push-Benachrichtigungen sind auf diesem Server nicht eingerichtet.", ) count = len(db.scalars( select(PushSubscription).where(PushSubscription.user_id == user.id) ).all()) if count == 0: raise HTTPException( status.HTTP_409_CONFLICT, "Für dieses Konto ist kein Gerät angemeldet.", ) def run() -> None: from app.db import SessionLocal with SessionLocal() as session: send_to_user(session, user.id, { "title": settings.app_name, "body": "Testbenachrichtigung – die Zustellung funktioniert.", "tag": "test", }) background.add_task(run) return MessageOut( detail=f"Testnachricht an {count} Gerät(e) in Auftrag gegeben. " "Ergebnis steht im Log des api-Containers." )