Feat: Push-Notifications vollständig implementiert
- push.py: /vapid-key, /subscribe, /unsubscribe + send_push_to_user/all Helpers - VAPID Keys in docker-compose.yml (Public + Private) - Giftköder-Meldung löst automatisch Push an alle Subscriber aus - ON CONFLICT(endpoint) für idempotentes Re-Subscribe - Abgelaufene Subscriptions werden bei HTTP 410 auto-gelöscht
This commit is contained in:
parent
b8a5dc7a66
commit
c721d051c8
3 changed files with 142 additions and 3 deletions
|
|
@ -7,6 +7,7 @@ from pydantic import BaseModel
|
|||
from typing import Optional
|
||||
from database import db
|
||||
from auth import get_current_user
|
||||
from routes.push import send_push_to_all
|
||||
|
||||
router = APIRouter()
|
||||
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
|
||||
|
|
@ -84,7 +85,17 @@ async def report_poison(data: PoisonCreate, user=Depends(get_current_user)):
|
|||
"SELECT * FROM poison WHERE user_id=? ORDER BY id DESC LIMIT 1",
|
||||
(user["id"],)
|
||||
).fetchone()
|
||||
return dict(row)
|
||||
entry = dict(row)
|
||||
|
||||
# Push-Notification an alle User
|
||||
send_push_to_all({
|
||||
"type": "poison_alert",
|
||||
"title": "⚠️ Giftköder gemeldet!",
|
||||
"body": f"{data.typ or 'Verdächtiger Fund'} in deiner Nähe — bitte vorsichtig sein.",
|
||||
"data": {"page": "poison", "id": entry["id"]},
|
||||
})
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,3 +1,128 @@
|
|||
"""BAN YARO — push Routes (Stub, wird ausgebaut)"""
|
||||
from fastapi import APIRouter
|
||||
"""BAN YARO — Push-Notification Routes"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from pywebpush import webpush, WebPushException
|
||||
|
||||
from database import db
|
||||
from auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
VAPID_PUBLIC_KEY = os.getenv("VAPID_PUBLIC_KEY", "")
|
||||
VAPID_PRIVATE_KEY = os.getenv("VAPID_PRIVATE_KEY", "")
|
||||
VAPID_CONTACT = os.getenv("VAPID_CONTACT", "mailto:admin@banyaro.app")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/push/vapid-key — Public Key für Frontend
|
||||
# ------------------------------------------------------------------
|
||||
@router.get("/vapid-key")
|
||||
async def get_vapid_key():
|
||||
if not VAPID_PUBLIC_KEY:
|
||||
raise HTTPException(503, "Push-Notifications nicht konfiguriert.")
|
||||
return {"vapid_public_key": VAPID_PUBLIC_KEY}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /api/push/subscribe — Subscription speichern
|
||||
# ------------------------------------------------------------------
|
||||
class PushSubscription(BaseModel):
|
||||
endpoint: str
|
||||
keys: dict # { p256dh, auth }
|
||||
expirationTime: Optional[int] = None
|
||||
|
||||
|
||||
@router.post("/subscribe", status_code=201)
|
||||
async def subscribe(data: PushSubscription, user=Depends(get_current_user)):
|
||||
p256dh = data.keys.get("p256dh", "")
|
||||
auth = data.keys.get("auth", "")
|
||||
|
||||
with db() as conn:
|
||||
conn.execute("""
|
||||
INSERT INTO push_subscriptions (user_id, endpoint, p256dh, auth)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(endpoint) DO UPDATE SET
|
||||
p256dh=excluded.p256dh,
|
||||
auth=excluded.auth,
|
||||
user_id=excluded.user_id
|
||||
""", (user["id"], data.endpoint, p256dh, auth))
|
||||
|
||||
logger.info(f"Push-Subscription gespeichert für user {user['id']}")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# DELETE /api/push/subscribe — Subscription entfernen
|
||||
# ------------------------------------------------------------------
|
||||
@router.delete("/subscribe")
|
||||
async def unsubscribe(user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
conn.execute(
|
||||
"DELETE FROM push_subscriptions WHERE user_id=?", (user["id"],)
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Interne Hilfsfunktion: Push an einen oder alle User schicken
|
||||
# ------------------------------------------------------------------
|
||||
def send_push(subscription_row, payload: dict) -> bool:
|
||||
"""Schickt eine Push-Notification. Gibt True bei Erfolg zurück."""
|
||||
if not VAPID_PRIVATE_KEY:
|
||||
return False
|
||||
try:
|
||||
webpush(
|
||||
subscription_info={
|
||||
"endpoint": subscription_row["endpoint"],
|
||||
"keys": {
|
||||
"p256dh": subscription_row["p256dh"],
|
||||
"auth": subscription_row["auth"],
|
||||
},
|
||||
},
|
||||
data=json.dumps(payload),
|
||||
vapid_private_key=VAPID_PRIVATE_KEY,
|
||||
vapid_claims={"sub": VAPID_CONTACT},
|
||||
)
|
||||
return True
|
||||
except WebPushException as e:
|
||||
status = e.response.status_code if e.response else 0
|
||||
logger.warning(f"Push fehlgeschlagen (HTTP {status}): {e}")
|
||||
# 410 Gone = Subscription abgelaufen → aus DB löschen
|
||||
if status == 410:
|
||||
with db() as conn:
|
||||
conn.execute(
|
||||
"DELETE FROM push_subscriptions WHERE endpoint=?",
|
||||
(subscription_row["endpoint"],)
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def send_push_to_user(user_id: int, payload: dict):
|
||||
"""Schickt Push an alle Subscriptions eines Users."""
|
||||
with db() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM push_subscriptions WHERE user_id=?", (user_id,)
|
||||
).fetchall()
|
||||
sent = 0
|
||||
for row in rows:
|
||||
if send_push(row, payload):
|
||||
sent += 1
|
||||
return sent
|
||||
|
||||
|
||||
def send_push_to_all(payload: dict):
|
||||
"""Schickt Push an alle abonnierten User (z.B. Giftköder-Alarm)."""
|
||||
with db() as conn:
|
||||
rows = conn.execute("SELECT * FROM push_subscriptions").fetchall()
|
||||
sent = 0
|
||||
for row in rows:
|
||||
if send_push(row, payload):
|
||||
sent += 1
|
||||
logger.info(f"Push an {sent}/{len(rows)} Subscriptions gesendet.")
|
||||
return sent
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ services:
|
|||
environment:
|
||||
- DB_PATH=/data/banyaro.db
|
||||
- MEDIA_DIR=/data/media
|
||||
- VAPID_PUBLIC_KEY=BMKbFAmpsqJ-eFef_4XJcYpuxPWqBNAoy9buMNnMSa6ijcPzltboHi_YccPKJrUD0isBez-vJIzAgjnLTWkzcC0
|
||||
- VAPID_PRIVATE_KEY=8PWa9vvwMqtqsJEJGcwmiLhR0_Yl7duVX3wmWiKS878
|
||||
- VAPID_CONTACT=mailto:admin@banyaro.app
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/"]
|
||||
interval: 30s
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue