feat: add admin endpoint to send digest emails for users
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 1m37s
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 1m37s
- Implemented a new endpoint `/admin/test/send-digest` in `admin_api.py` to trigger digest emails for specific users. - Added a script `send_digest.py` to facilitate sending digest emails via the admin API. - Enhanced the digest action handling in `digest_action_api.py` to support token peeking without consuming it. - Updated the `send_digests` function in `digest_scheduler.py` to utilize the new `send_digest_for_user` function for sending emails. - Introduced a new utility function `peek_token` in `digest_token.py` to validate tokens without consuming them. - Modified the `ParentView.vue` component to handle digest actions upon receiving a digest token in the URL. - Updated `.gitignore` to exclude sensitive certificate files.
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import logging
|
||||
|
||||
from flask import Blueprint, redirect, make_response
|
||||
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
|
||||
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
|
||||
from api.utils import get_validated_user_id
|
||||
|
||||
digest_action_api = Blueprint('digest_action_api', __name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -34,45 +35,65 @@ _UNSUB_ERROR_HTML = """<!DOCTYPE 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.
|
||||
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 = validate_and_consume_token(token_id)
|
||||
token = peek_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}"
|
||||
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)
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user