All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m8s
- Implemented routine child-mode flow tests to ensure proper functionality of routine assignment and task completion. - Created notification tests for parent view to verify routine completion notifications for children. - Developed ChildRoutineOverlay component for displaying routine tasks and handling user interactions. - Added RoutineApproveDialog component for approving or rejecting completed routines. - Created unit tests for ChildRoutineOverlay and RoutineEditView components to ensure correct behavior and rendering. - Enhanced RoutineEditView with proper handling of task addition and form submission. Co-authored-by: Copilot <copilot@github.com>
114 lines
4.5 KiB
Python
114 lines
4.5 KiB
Python
import logging
|
|
|
|
from flask import Blueprint, redirect, make_response, jsonify, request
|
|
from tinydb import Query
|
|
|
|
from utils.digest_token import validate_and_consume_token, validate_unsubscribe_token, peek_token
|
|
from db.db import users_db
|
|
from api.child_action_helpers import approve_chore, reject_chore, approve_reward_request, deny_reward, approve_routine, reject_routine
|
|
from api.utils import get_validated_user_id
|
|
|
|
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 (without consuming it) and redirect to the
|
|
frontend ParentView with the token embedded so the action executes only
|
|
after the user authenticates as a parent.
|
|
"""
|
|
from flask import current_app
|
|
frontend_url = current_app.config.get('FRONTEND_URL', 'https://localhost:5173')
|
|
|
|
token = peek_token(token_id)
|
|
if not token:
|
|
return make_response(_ERROR_HTML, 400)
|
|
|
|
deep_link = (
|
|
f"{frontend_url}/parent/{token.child_id}"
|
|
f"?digestToken={token_id}&scrollTo={token.entity_id}&entityType={token.entity_type}"
|
|
)
|
|
return redirect(deep_link, 302)
|
|
|
|
|
|
@digest_action_api.route('/digest-action/<token_id>', methods=['POST'])
|
|
def execute_digest_action(token_id: str):
|
|
"""
|
|
Execute a digest action. Requires the user to be authenticated (JWT cookie).
|
|
Validates and consumes the token, then performs the approve/deny action.
|
|
"""
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
|
|
|
|
token = validate_and_consume_token(token_id)
|
|
if not token:
|
|
return jsonify({'error': 'This link is invalid or has expired.', 'code': 'INVALID_TOKEN'}), 400
|
|
|
|
if token.user_id != user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 403
|
|
|
|
try:
|
|
if token.entity_type == 'chore' and token.action == 'approve':
|
|
approve_chore(user_id, token.child_id, token.entity_id)
|
|
elif token.entity_type == 'chore' and token.action == 'deny':
|
|
reject_chore(user_id, token.child_id, token.entity_id)
|
|
elif token.entity_type == 'reward' and token.action == 'approve':
|
|
approve_reward_request(user_id, token.child_id, token.entity_id)
|
|
elif token.entity_type == 'reward' and token.action == 'deny':
|
|
deny_reward(user_id, token.child_id, token.entity_id)
|
|
elif token.entity_type == 'routine' and token.action == 'approve':
|
|
approve_routine(user_id, token.child_id, token.entity_id)
|
|
elif token.entity_type == 'routine' and token.action == 'deny':
|
|
reject_routine(user_id, token.child_id, token.entity_id)
|
|
else:
|
|
return jsonify({'error': 'Unknown action', 'code': 'INVALID_ACTION'}), 400
|
|
except Exception as e:
|
|
logger.error(f'Error executing digest action {token.action}/{token.entity_type}: {e}')
|
|
return jsonify({'error': 'Failed to execute action', 'code': 'ACTION_FAILED'}), 400
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'child_id': token.child_id,
|
|
'entity_id': token.entity_id,
|
|
'entity_type': token.entity_type,
|
|
'action': token.action,
|
|
}), 200
|
|
|
|
|
|
@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)
|
|
|
|
|
|
|