Files
sapl/backend/models/push_subscription.py
T
shridhar.sabat fec6167037 feat(capture): web-push subscription store + API + delivery on job completion (PR-3, refs #524)
- 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.
2026-09-10 14:41:58 +05:30

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))