Private
Public Access
Phase 4: Web Push notifications (pywebpush + VAPID)
Backend: PushSubscription model/migration, VAPID config + `cli.py generate-vapid-keys`, push_service.send_push_to_user (upsert-by-endpoint subscribe/unsubscribe, auto-cleanup of expired 404/410 subscriptions), /api/push/* router, and ConnectionManager now tracks connected user IDs per room so chat.py can push only to offline members after broadcasting to online ones. Two test-infra bugs found and fixed along the way: send_push_to_user takes the caller's AsyncSession and is awaited inline rather than fired via asyncio.create_task with its own session (background tasks were outliving the test event loop); and the ws_client fixture now uses NullPool to eliminate a connection-pool checkout race that was failing WS tests intermittently. Frontend: service worker rebuilt with vite-plugin-pwa's injectManifest strategy (custom src/sw.ts) so it can add push/notificationclick handlers alongside the existing precaching and StaleWhileRevalidate routes ported over from generateSW. New subscribe/unsubscribe flow (lib/push.ts, api/push.ts) with a toggle in the account menu. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ created by an operator running this script directly on the app server.
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import base64
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
@@ -33,6 +34,34 @@ async def _create_user(username: str, email: str, password: str, is_admin: bool)
|
||||
print(f"Created user {username!r} (id={user.id}, admin={is_admin})")
|
||||
|
||||
|
||||
def _generate_vapid_keys() -> None:
|
||||
# py_vapid works in DER/PEM internally, but both pywebpush's
|
||||
# vapid_private_key argument and the browser's PushManager
|
||||
# applicationServerKey expect base64url-encoded *raw* key bytes -- the
|
||||
# format used in every Web Push tutorial/example. Encode explicitly
|
||||
# rather than relying on py_vapid's own (PEM-oriented) save helpers.
|
||||
from py_vapid import Vapid02
|
||||
|
||||
vapid = Vapid02()
|
||||
vapid.generate_keys()
|
||||
|
||||
private_raw = vapid.private_key.private_numbers().private_value.to_bytes(32, "big")
|
||||
private_b64 = base64.urlsafe_b64encode(private_raw).decode().rstrip("=")
|
||||
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
||||
public_raw = vapid.public_key.public_bytes(
|
||||
encoding=serialization.Encoding.X962,
|
||||
format=serialization.PublicFormat.UncompressedPoint,
|
||||
)
|
||||
public_b64 = base64.urlsafe_b64encode(public_raw).decode().rstrip("=")
|
||||
|
||||
print("Add these to backend/.env:")
|
||||
print(f"VAPID_PUBLIC_KEY={public_b64}")
|
||||
print(f"VAPID_PRIVATE_KEY={private_b64}")
|
||||
print("VAPID_SUBJECT=mailto:you@example.com")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(prog="python -m app.cli")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
@@ -43,10 +72,14 @@ def main() -> None:
|
||||
create_user.add_argument("password")
|
||||
create_user.add_argument("--admin", action="store_true", help="Grant is_site_admin")
|
||||
|
||||
subparsers.add_parser("generate-vapid-keys", help="Generate a VAPID key pair for push notifications")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "create-user":
|
||||
asyncio.run(_create_user(args.username, args.email, args.password, args.admin))
|
||||
elif args.command == "generate-vapid-keys":
|
||||
_generate_vapid_keys()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user