- models/push_subscription.py: push_subscriptions table (id uuid pk, user_email indexed, endpoint Text, p256dh/auth Text, user_agent nullable, created_at). Registered in main.py AND sync_worker.py so both containers create_all the table. - routers/push.py (wired at /api/push): GET /vapid-public, GET /status, POST /subscribe (upsert by user_email+endpoint), DELETE /subscribe (query-param or body endpoint; TestClient-safe). - services/push_service.py: send_push_to_user() — VAPID-signed pywebpush delivery, deletes 404/410-gone subs, never raises; logs once and skips when VAPID keys are missing. - capture_job_processor: fires a 'Capture complete' push next to the bell Notification at terminal status (target /library). - config: vapid_private_key/public_key/subject (subject defaults to mailto: from Mail_From). requirements.txt + .env.dev/uat.example. - tests/test_push.py: subscribe upsert-not-duplicate, status, unsubscribe idempotent, vapid-public, silent no-op without keys, mocked delivery asserting title/body/target_path + gone-sub cleanup.
30 lines
1.1 KiB
Python
30 lines
1.1 KiB
Python
"""Web-Push subscription store (issue #524, PR-3).
|
|
|
|
One row per browser/device a user has opted into push notifications on. The
|
|
``endpoint`` is the browser's unique push URL; a user may legitimately have
|
|
several (desktop + phone + different browsers), so uniqueness is scoped per
|
|
``user_email`` — the API upserts by (user_email, endpoint) and the worker can
|
|
never push to a stale/unknown user.
|
|
|
|
Sent at job completion by ``services.push_service.send_push_to_user``.
|
|
"""
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
|
|
from sqlalchemy import Column, DateTime, String, Text
|
|
|
|
from database import Base
|
|
|
|
|
|
class PushSubscription(Base):
|
|
__tablename__ = "push_subscriptions"
|
|
|
|
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
user_email = Column(String(255), nullable=False, index=True)
|
|
endpoint = Column(Text, nullable=False)
|
|
p256dh = Column(Text, nullable=False)
|
|
auth = Column(Text, nullable=False)
|
|
user_agent = Column(Text, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), nullable=False,
|
|
default=lambda: datetime.now(timezone.utc))
|