Add push notification functionality with tests and digest scheduler
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m14s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m14s
- Implemented push subscription API with tests for subscribing and unsubscribing users. - Created web push notification tests triggered by child actions. - Added digest scheduler to send email digests to users at 9 PM local time. - Developed utility functions for creating and validating digest action tokens. - Integrated web push sender to handle sending notifications to users. - Added service worker for handling push notifications in the frontend. - Created a push opt-in component for user notification preferences. - Implemented tests for the push opt-in component to ensure correct behavior. - Updated frontend services to manage push subscriptions and permissions.
This commit is contained in:
68
backend/utils/push_sender.py
Normal file
68
backend/utils/push_sender.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from pywebpush import webpush, WebPushException
|
||||
|
||||
from db.push_subscriptions import get_subscriptions_by_user, delete_subscription_by_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_vapid_private_key() -> str | None:
|
||||
return os.environ.get('VAPID_PRIVATE_KEY')
|
||||
|
||||
|
||||
def _get_vapid_claims_email() -> str:
|
||||
return os.environ.get('VAPID_CLAIMS_EMAIL', 'admin@reward-app.local')
|
||||
|
||||
|
||||
def send_push_to_user(user_id: str, payload: dict) -> int:
|
||||
"""
|
||||
Send a web push notification to all stored subscriptions for the user.
|
||||
Returns the number of subscriptions successfully notified.
|
||||
|
||||
Stale/expired subscriptions (HTTP 404/410) are removed automatically.
|
||||
"""
|
||||
private_key = _get_vapid_private_key()
|
||||
if not private_key:
|
||||
logger.warning('VAPID_PRIVATE_KEY not configured; skipping push notification')
|
||||
return 0
|
||||
|
||||
subscriptions = get_subscriptions_by_user(user_id)
|
||||
if not subscriptions:
|
||||
return 0
|
||||
|
||||
claims_email = _get_vapid_claims_email()
|
||||
sent = 0
|
||||
|
||||
for sub in subscriptions:
|
||||
try:
|
||||
subscription_info = {
|
||||
'endpoint': sub.endpoint,
|
||||
'keys': sub.keys,
|
||||
}
|
||||
webpush(
|
||||
subscription_info=subscription_info,
|
||||
data=json.dumps(payload),
|
||||
vapid_private_key=private_key,
|
||||
vapid_claims={'sub': f'mailto:{claims_email}'},
|
||||
ttl=86400,
|
||||
)
|
||||
sent += 1
|
||||
except WebPushException as e:
|
||||
status_code = e.response.status_code if e.response is not None else None
|
||||
if status_code in (404, 410):
|
||||
# Subscription is gone — clean it up
|
||||
logger.info(
|
||||
f'Removing stale push subscription {sub.id} for user {user_id} (HTTP {status_code})'
|
||||
)
|
||||
delete_subscription_by_id(sub.id)
|
||||
else:
|
||||
logger.error(
|
||||
f'WebPushException for subscription {sub.id} (user {user_id}): {e}'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Unexpected error sending push to subscription {sub.id}: {e}')
|
||||
|
||||
return sent
|
||||
Reference in New Issue
Block a user