161 lines
5.5 KiB
Python
161 lines
5.5 KiB
Python
"""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."
|
||
)
|