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:
88
backend/api/digest_action_api.py
Normal file
88
backend/api/digest_action_api.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import logging
|
||||
|
||||
from flask import Blueprint, redirect, make_response
|
||||
from tinydb import Query
|
||||
|
||||
from utils.digest_token import validate_and_consume_token, validate_unsubscribe_token
|
||||
from db.db import users_db
|
||||
from api.child_action_helpers import approve_chore, reject_chore, approve_reward_request, deny_reward
|
||||
|
||||
digest_action_api = Blueprint('digest_action_api', __name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_ERROR_HTML = """<!DOCTYPE html>
|
||||
<html><head><title>Link Error</title></head>
|
||||
<body style="font-family:sans-serif;text-align:center;margin-top:60px;">
|
||||
<h2>This link is invalid or has expired.</h2>
|
||||
<p>Action links expire after 24 hours and can only be used once.</p>
|
||||
</body></html>"""
|
||||
|
||||
_UNSUB_HTML = """<!DOCTYPE html>
|
||||
<html><head><title>Unsubscribed</title></head>
|
||||
<body style="font-family:sans-serif;text-align:center;margin-top:60px;">
|
||||
<h2>You have been unsubscribed from daily digest emails.</h2>
|
||||
<p>To re-enable, visit your profile in the app.</p>
|
||||
</body></html>"""
|
||||
|
||||
_UNSUB_ERROR_HTML = """<!DOCTYPE html>
|
||||
<html><head><title>Link Error</title></head>
|
||||
<body style="font-family:sans-serif;text-align:center;margin-top:60px;">
|
||||
<h2>This unsubscribe link is invalid or has expired.</h2>
|
||||
</body></html>"""
|
||||
|
||||
|
||||
@digest_action_api.route('/digest-action/<token_id>', methods=['GET'])
|
||||
def handle_digest_action(token_id: str):
|
||||
"""
|
||||
Validate a digest action token and execute the corresponding action.
|
||||
On success: 302 redirect to the ParentView deep link.
|
||||
On failure: 400 HTML error page.
|
||||
"""
|
||||
from flask import current_app
|
||||
frontend_url = current_app.config.get('FRONTEND_URL', 'https://localhost:5173')
|
||||
|
||||
token = validate_and_consume_token(token_id)
|
||||
if not token:
|
||||
return make_response(_ERROR_HTML, 400)
|
||||
|
||||
user_id = token.user_id
|
||||
child_id = token.child_id
|
||||
entity_id = token.entity_id
|
||||
entity_type = token.entity_type
|
||||
action = token.action
|
||||
|
||||
try:
|
||||
if entity_type == 'chore' and action == 'approve':
|
||||
approve_chore(user_id, child_id, entity_id)
|
||||
elif entity_type == 'chore' and action == 'deny':
|
||||
reject_chore(user_id, child_id, entity_id)
|
||||
elif entity_type == 'reward' and action == 'approve':
|
||||
approve_reward_request(user_id, child_id, entity_id)
|
||||
elif entity_type == 'reward' and action == 'deny':
|
||||
deny_reward(user_id, child_id, entity_id)
|
||||
else:
|
||||
return make_response(_ERROR_HTML, 400)
|
||||
except Exception as e:
|
||||
logger.error(f'Error executing digest action {action}/{entity_type}: {e}')
|
||||
return make_response(_ERROR_HTML, 400)
|
||||
|
||||
deep_link = (
|
||||
f"{frontend_url}/parent/{child_id}"
|
||||
f"?scrollTo={entity_id}&entityType={entity_type}"
|
||||
)
|
||||
return redirect(deep_link, 302)
|
||||
|
||||
|
||||
@digest_action_api.route('/digest-unsubscribe/<token>', methods=['GET'])
|
||||
def handle_digest_unsubscribe(token: str):
|
||||
user_id = validate_unsubscribe_token(token)
|
||||
if not user_id:
|
||||
return make_response(_UNSUB_ERROR_HTML, 400)
|
||||
|
||||
UserQ = Query()
|
||||
users_db.update({'email_digest_enabled': False}, UserQ.id == user_id)
|
||||
logger.info(f'User {user_id} unsubscribed from digest via email link')
|
||||
return make_response(_UNSUB_HTML, 200)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user