feat: pwa notifications

This commit is contained in:
2026-07-14 20:49:30 +02:00
parent 2cf7b283e0
commit 5198826f83
21 changed files with 1563 additions and 8 deletions
+79
View File
@@ -0,0 +1,79 @@
from django.conf import settings
from django.http import HttpRequest
from ninja import Router, Schema
from ninja.errors import HttpError
from accounts.auth import AUTH
from accounts.models import PushSubscription
from tracker.push import notify_user, push_is_configured
router = Router(auth=AUTH)
class SubscriptionKeys(Schema):
p256dh: str
auth: str
class SubscribeIn(Schema):
"""Exactly the shape of a browser PushSubscription, so the client can post it as-is."""
endpoint: str
keys: SubscriptionKeys
class UnsubscribeIn(Schema):
endpoint: str
@router.post("/subscribe")
def subscribe(request: HttpRequest, payload: SubscribeIn):
"""Register this device for reminders. Re-subscribing the same endpoint is a no-op."""
if not push_is_configured():
raise HttpError(503, "Push is not configured on the server.")
PushSubscription.objects.update_or_create(
endpoint=payload.endpoint,
defaults={
"user": request.user,
"p256dh": payload.keys.p256dh,
"auth": payload.keys.auth,
"user_agent": request.headers.get("User-Agent", "")[:300],
},
)
return {"detail": "Reminders enabled on this device."}
@router.post("/unsubscribe")
def unsubscribe(request: HttpRequest, payload: UnsubscribeIn):
PushSubscription.objects.filter(
user=request.user, endpoint=payload.endpoint
).delete()
return {"detail": "Reminders disabled on this device."}
@router.post("/test")
def send_test(request: HttpRequest):
"""Push a notification right now, to prove the chain works end to end."""
if not request.user.push_subscriptions.exists():
raise HttpError(400, "No device has enabled reminders yet.")
delivered = notify_user(
request.user,
{
"title": "Osiris — test",
"body": "Reminders are working. This is what a dose reminder looks like.",
"url": "/admin/today/",
"tag": "test",
},
)
if not delivered:
raise HttpError(502, "The push service rejected the message.")
return {"detail": f"Sent to {delivered} device(s)."}
@router.get("/public-key", auth=None)
def public_key(request: HttpRequest):
"""The applicationServerKey the browser needs in order to subscribe."""
return {"public_key": settings.VAPID_PUBLIC_KEY}