mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-02 03:23:00 +02:00
133 lines
4.5 KiB
Python
133 lines
4.5 KiB
Python
"""Web Push subscription management endpoints."""
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app.push.send import send_push
|
|
from app.push.vapid import get_vapid_private_key, get_vapid_public_key
|
|
from app.repository.push_subscriptions import PushSubscriptionRepository
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/push", tags=["push"])
|
|
|
|
|
|
# ── Request/response models ─────────────────────────────────────────────
|
|
|
|
|
|
class VapidPublicKeyResponse(BaseModel):
|
|
public_key: str
|
|
|
|
|
|
class PushSubscribeRequest(BaseModel):
|
|
endpoint: str = Field(min_length=1)
|
|
p256dh: str = Field(min_length=1)
|
|
auth: str = Field(min_length=1)
|
|
label: str = ""
|
|
|
|
|
|
class PushSubscriptionUpdate(BaseModel):
|
|
label: str | None = None
|
|
filter_mode: str | None = None
|
|
filter_conversations: list[str] | None = None
|
|
|
|
|
|
# ── Endpoints ────────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.get("/vapid-public-key", response_model=VapidPublicKeyResponse)
|
|
async def vapid_public_key() -> VapidPublicKeyResponse:
|
|
"""Return the VAPID public key for browser PushManager.subscribe()."""
|
|
key = get_vapid_public_key()
|
|
if not key:
|
|
raise HTTPException(status_code=503, detail="VAPID keys not initialized")
|
|
return VapidPublicKeyResponse(public_key=key)
|
|
|
|
|
|
@router.post("/subscribe")
|
|
async def subscribe(body: PushSubscribeRequest) -> dict:
|
|
"""Register or update a push subscription. Upserts by endpoint."""
|
|
sub = await PushSubscriptionRepository.create(
|
|
endpoint=body.endpoint,
|
|
p256dh=body.p256dh,
|
|
auth=body.auth,
|
|
label=body.label,
|
|
)
|
|
return sub
|
|
|
|
|
|
@router.get("/subscriptions")
|
|
async def list_subscriptions() -> list[dict]:
|
|
"""List all push subscriptions."""
|
|
return await PushSubscriptionRepository.get_all()
|
|
|
|
|
|
@router.patch("/subscriptions/{subscription_id}")
|
|
async def update_subscription(subscription_id: str, body: PushSubscriptionUpdate) -> dict:
|
|
"""Update a subscription's label or filter preferences."""
|
|
existing = await PushSubscriptionRepository.get(subscription_id)
|
|
if not existing:
|
|
raise HTTPException(status_code=404, detail="Subscription not found")
|
|
|
|
updates = {}
|
|
if body.label is not None:
|
|
updates["label"] = body.label
|
|
if body.filter_mode is not None:
|
|
if body.filter_mode not in ("all_messages", "all_dms", "selected"):
|
|
raise HTTPException(status_code=400, detail="Invalid filter_mode")
|
|
updates["filter_mode"] = body.filter_mode
|
|
if body.filter_conversations is not None:
|
|
updates["filter_conversations"] = body.filter_conversations
|
|
|
|
result = await PushSubscriptionRepository.update(subscription_id, **updates)
|
|
return result or existing
|
|
|
|
|
|
@router.delete("/subscriptions/{subscription_id}")
|
|
async def unsubscribe(subscription_id: str) -> dict:
|
|
"""Delete a push subscription."""
|
|
deleted = await PushSubscriptionRepository.delete(subscription_id)
|
|
if not deleted:
|
|
raise HTTPException(status_code=404, detail="Subscription not found")
|
|
return {"deleted": True}
|
|
|
|
|
|
@router.post("/subscriptions/{subscription_id}/test")
|
|
async def test_push(subscription_id: str) -> dict:
|
|
"""Send a test notification to a subscription."""
|
|
sub = await PushSubscriptionRepository.get(subscription_id)
|
|
if not sub:
|
|
raise HTTPException(status_code=404, detail="Subscription not found")
|
|
|
|
vapid_key = get_vapid_private_key()
|
|
if not vapid_key:
|
|
raise HTTPException(status_code=503, detail="VAPID keys not initialized")
|
|
|
|
import json
|
|
|
|
payload = json.dumps(
|
|
{
|
|
"title": "RemoteTerm Test",
|
|
"body": "Push notifications are working!",
|
|
"tag": "meshcore-test",
|
|
"url_hash": "",
|
|
}
|
|
)
|
|
|
|
try:
|
|
await send_push(
|
|
subscription_info={
|
|
"endpoint": sub["endpoint"],
|
|
"keys": {"p256dh": sub["p256dh"], "auth": sub["auth"]},
|
|
},
|
|
payload=payload,
|
|
vapid_private_key=vapid_key,
|
|
vapid_claims={"sub": "mailto:noreply@meshcore.local"},
|
|
)
|
|
return {"status": "sent"}
|
|
except Exception as e:
|
|
logger.warning("Test push failed: %s", e)
|
|
raise HTTPException(status_code=502, detail=f"Push delivery failed: {e}") from None
|