From fd1057662f0994f3c52c2bc0aa5bcc0e99234c83 Mon Sep 17 00:00:00 2001 From: Ryan Kegel Date: Tue, 6 Jan 2026 16:25:09 -0500 Subject: [PATCH] added seperate users for backend events --- api/auth_api.py | 21 ++++- api/child_api.py | 79 ++++++++++--------- api/image_api.py | 12 +-- api/reward_api.py | 18 +++-- api/task_api.py | 16 ++-- api/utils.py | 28 +++++++ main.py | 4 +- web/vue-app/src/App.vue | 15 ++-- web/vue-app/src/common/backendEvents.ts | 30 ++++--- .../src/components/BackendEventsListener.vue | 18 +++++ web/vue-app/src/stores/auth.ts | 11 ++- 11 files changed, 172 insertions(+), 80 deletions(-) create mode 100644 api/utils.py create mode 100644 web/vue-app/src/components/BackendEventsListener.vue diff --git a/api/auth_api.py b/api/auth_api.py index 619f8d9..ec2affc 100644 --- a/api/auth_api.py +++ b/api/auth_api.py @@ -1,22 +1,26 @@ +import logging import secrets, jwt from datetime import datetime, timedelta, timezone from flask import Blueprint, request, jsonify, current_app from flask_mail import Mail, Message from tinydb import Query +import os + +from api.utils import sanitize_email +from config.paths import get_user_image_dir from api.error_codes import MISSING_FIELDS, EMAIL_EXISTS, MISSING_TOKEN, INVALID_TOKEN, TOKEN_TIMESTAMP_MISSING, \ TOKEN_EXPIRED, ALREADY_VERIFIED, MISSING_EMAIL, USER_NOT_FOUND, MISSING_EMAIL_OR_PASSWORD, INVALID_CREDENTIALS, \ NOT_VERIFIED from db.db import users_db +logger = logging.getLogger(__name__) auth_api = Blueprint('auth_api', __name__) UserQuery = Query() mail = Mail() TOKEN_EXPIRY_MINUTES = 60*4 -SECRET_KEY = "your-secret-key" # Use a secure key in production -#SECRET_KEY = os.environ.get('SECRET_KEY') def send_verification_email(to_email, token): verify_url = f"{current_app.config['FRONTEND_URL']}/auth/verify?token={token}" @@ -58,6 +62,7 @@ def verify(): status = 'success' reason = '' code = '' + user = None if not token: status = 'error' @@ -85,6 +90,13 @@ def verify(): users_db.update({'verified': True, 'verify_token': None, 'verify_token_created': None}, Query().verify_token == token) http_status = 200 if status == 'success' else 400 + if http_status == 200 and user is not None: ##user is verified, create the user's image directory + if 'email' not in user: + logger.error("Verified user has no email field.") + else: + user_image_dir = get_user_image_dir(sanitize_email(user['email'])) + os.makedirs(user_image_dir, exist_ok=True) + return jsonify({'status': status, 'reason': reason, 'code': code}), http_status @auth_api.route('/resend-verify', methods=['POST']) @@ -130,7 +142,7 @@ def login(): 'email': email, 'exp': datetime.utcnow() + timedelta(hours=24*7) } - token = jwt.encode(payload, SECRET_KEY, algorithm='HS256') + token = jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256') resp = jsonify({'message': 'Login successful'}) resp.set_cookie('token', token, httponly=True, secure=True, samesite='Strict') @@ -143,13 +155,14 @@ def me(): return jsonify({'error': 'Missing token', 'code': MISSING_TOKEN}), 401 try: - payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256']) + payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256']) email = payload.get('email') user = users_db.get(UserQuery.email == email) if not user: return jsonify({'error': 'User not found', 'code': USER_NOT_FOUND}), 404 return jsonify({ 'email': user['email'], + 'id': sanitize_email(user['email']), 'first_name': user['first_name'], 'last_name': user['last_name'], 'verified': user['verified'] diff --git a/api/child_api.py b/api/child_api.py index 862720c..d54f15b 100644 --- a/api/child_api.py +++ b/api/child_api.py @@ -1,10 +1,12 @@ from flask import Blueprint, request, jsonify from tinydb import Query -from db.db import child_db, task_db, reward_db, pending_reward_db -from api.reward_status import RewardStatus -from api.child_tasks import ChildTask + from api.child_rewards import ChildReward -from events.sse import send_event_to_user +from api.child_tasks import ChildTask +from api.pending_reward import PendingReward as PendingRewardResponse +from api.reward_status import RewardStatus +from api.utils import send_event_for_current_user +from db.db import child_db, task_db, reward_db, pending_reward_db from events.types.child_modified import ChildModified from events.types.child_reward_request import ChildRewardRequest from events.types.child_reward_triggered import ChildRewardTriggered @@ -13,12 +15,10 @@ from events.types.child_task_triggered import ChildTaskTriggered from events.types.child_tasks_set import ChildTasksSet from events.types.event import Event from events.types.event_types import EventType -from api.pending_reward import PendingReward as PendingRewardResponse - from models.child import Child from models.pending_reward import PendingReward -from models.task import Task from models.reward import Reward +from models.task import Task child_api = Blueprint('child_api', __name__) @@ -44,7 +44,10 @@ def add_child(): child = Child(name=name, age=age, image_id=image) child_db.insert(child.to_dict()) - send_event_to_user("user123", Event(EventType.CHILD_MODIFIED.value, ChildModified(child.id, ChildModified.OPERATION_ADD))) + resp = send_event_for_current_user( + Event(EventType.CHILD_MODIFIED.value, ChildModified(child.id, ChildModified.OPERATION_ADD))) + if resp: + return resp return jsonify({'message': f'Child {name} added.'}), 201 @child_api.route('/child//edit', methods=['PUT']) @@ -85,16 +88,14 @@ def edit_child(id): pending_reward_db.remove( (PendingQuery.child_id == id) & (PendingQuery.reward_id == reward.id) ) - send_event_to_user( - "user123", - Event( - EventType.CHILD_REWARD_REQUEST.value, - ChildRewardRequest(id, reward.id, ChildRewardRequest.REQUEST_CANCELLED) - ) - ) - + resp = send_event_for_current_user( + Event(EventType.CHILD_REWARD_REQUEST.value, ChildRewardRequest(id, reward.id, ChildRewardRequest.REQUEST_CANCELLED))) + if resp: + return resp child_db.update(child.to_dict(), ChildQuery.id == id) - send_event_to_user("user123", Event(EventType.CHILD_MODIFIED.value, ChildModified(id, ChildModified.OPERATION_EDIT))) + resp = send_event_for_current_user(Event(EventType.CHILD_MODIFIED.value, ChildModified(id, ChildModified.OPERATION_EDIT))) + if resp: + return resp return jsonify({'message': f'Child {id} updated.'}), 200 @child_api.route('/child/list', methods=['GET']) @@ -107,8 +108,9 @@ def list_children(): def delete_child(id): ChildQuery = Query() if child_db.remove(ChildQuery.id == id): - send_event_to_user("user123", - Event(EventType.CHILD_MODIFIED.value, ChildModified(id, ChildModified.OPERATION_DELETE))) + resp = send_event_for_current_user(Event(EventType.CHILD_MODIFIED.value, ChildModified(id, ChildModified.OPERATION_DELETE))) + if resp: + return resp return jsonify({'message': f'Child {id} deleted.'}), 200 return jsonify({'error': 'Child not found'}), 404 @@ -154,7 +156,9 @@ def set_child_tasks(id): valid_task_ids.append(tid) # Replace tasks with validated IDs child_db.update({'tasks': valid_task_ids}, ChildQuery.id == id) - send_event_to_user("user123", Event(EventType.CHILD_TASKS_SET.value, ChildTasksSet(id, valid_task_ids))) + resp = send_event_for_current_user(Event(EventType.CHILD_TASKS_SET.value, ChildTasksSet(id, valid_task_ids))) + if resp: + return resp return jsonify({ 'message': f'Tasks set for child {id}.', 'task_ids': valid_task_ids, @@ -302,8 +306,9 @@ def trigger_child_task(id): child.points = max(child.points, 0) # update the child in the database child_db.update({'points': child.points}, ChildQuery.id == id) - send_event_to_user("user123", Event(EventType.CHILD_TASK_TRIGGERED.value, ChildTaskTriggered(task.id, child.id, child.points))) - + resp = send_event_for_current_user(Event(EventType.CHILD_TASK_TRIGGERED.value, ChildTaskTriggered(task.id, child.id, child.points))) + if resp: + return resp return jsonify({'message': f'{task.name} points assigned to {child.name}.', 'points': child.points, 'id': child.id}), 200 @child_api.route('/child//assign-reward', methods=['POST']) @@ -388,7 +393,9 @@ def set_child_rewards(id): # Replace rewards with validated IDs child_db.update({'rewards': valid_reward_ids}, ChildQuery.id == id) - send_event_to_user("user123", Event(EventType.CHILD_REWARDS_SET.value, ChildRewardsSet(id, valid_reward_ids))) + resp = send_event_for_current_user(Event(EventType.CHILD_REWARDS_SET.value, ChildRewardsSet(id, valid_reward_ids))) + if resp: + return resp return jsonify({ 'message': f'Rewards set for child {id}.', 'reward_ids': valid_reward_ids, @@ -488,16 +495,18 @@ def trigger_child_reward(id): (PendingQuery.child_id == child.id) & (PendingQuery.reward_id == reward.id) ) if removed: - send_event_to_user("user123", Event(EventType.CHILD_REWARD_REQUEST.value, ChildRewardRequest(reward.id, child.id, ChildRewardRequest.REQUEST_GRANTED))) + resp = send_event_for_current_user(Event(EventType.CHILD_REWARD_REQUEST.value, ChildRewardRequest(reward.id, child.id, ChildRewardRequest.REQUEST_GRANTED))) + if resp: + return resp # update the child's points based on reward cost child.points -= reward.cost # update the child in the database child_db.update({'points': child.points}, ChildQuery.id == id) - send_event_to_user("user123", Event(EventType.CHILD_REWARD_TRIGGERED.value, ChildRewardTriggered(reward.id, child.id, child.points))) - - + resp = send_event_for_current_user(Event(EventType.CHILD_REWARD_TRIGGERED.value, ChildRewardTriggered(reward.id, child.id, child.points))) + if resp: + return resp return jsonify({'message': f'{reward.name} assigned to {child.name}.', 'points': child.points, 'id': child.id}), 200 @child_api.route('/child//affordable-rewards', methods=['GET']) @@ -580,8 +589,9 @@ def request_reward(id): pending = PendingReward(child_id=child.id, reward_id=reward.id) pending_reward_db.insert(pending.to_dict()) - send_event_to_user("user123", Event(EventType.CHILD_REWARD_REQUEST.value, ChildRewardRequest(child.id, reward.id, ChildRewardRequest.REQUEST_CREATED))) - + resp = send_event_for_current_user(Event(EventType.CHILD_REWARD_REQUEST.value, ChildRewardRequest(child.id, reward.id, ChildRewardRequest.REQUEST_CREATED))) + if resp: + return resp return jsonify({ 'message': f'Reward request for {reward.name} submitted for {child.name}.', 'reward_id': reward.id, @@ -615,14 +625,9 @@ def cancel_request_reward(id): return jsonify({'error': 'No pending request found for this reward'}), 404 # Notify user that the request was cancelled - send_event_to_user( - "user123", - Event( - EventType.CHILD_REWARD_REQUEST.value, - ChildRewardRequest(child.id, reward_id, ChildRewardRequest.REQUEST_CANCELLED) - ) - ) - + resp = send_event_for_current_user(Event(EventType.CHILD_REWARD_REQUEST.value, ChildRewardRequest(child.id, reward_id, ChildRewardRequest.REQUEST_CANCELLED))) + if resp: + return resp return jsonify({ 'message': f'Reward request cancelled for {child.name}.', 'child_id': child.id, diff --git a/api/image_api.py b/api/image_api.py index 579e02d..2d418f2 100644 --- a/api/image_api.py +++ b/api/image_api.py @@ -3,13 +3,14 @@ import os from PIL import Image as PILImage, UnidentifiedImageError from flask import Blueprint, request, jsonify, send_file from tinydb import Query + +from api.utils import get_current_user_id, sanitize_email from config.paths import get_user_image_dir from db.db import image_db from models.image import Image image_api = Blueprint('image_api', __name__) -UPLOAD_FOLDER = get_user_image_dir("user123") ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png'} IMAGE_TYPE_PROFILE = 1 IMAGE_TYPE_ICON = 2 @@ -20,6 +21,9 @@ def allowed_file(filename): @image_api.route('/image/upload', methods=['POST']) def upload(): + user_id = get_current_user_id() + if not user_id: + return jsonify({'error': 'User not authenticated'}), 401 if 'file' not in request.files: return jsonify({'error': 'No file part in the request'}), 400 file = request.files['file'] @@ -60,13 +64,11 @@ def upload(): format_extension_map = {'JPEG': '.jpg', 'PNG': '.png'} extension = format_extension_map.get(original_format, '.png') - - image_record = Image(extension=extension, permanent=perm, type=image_type, user="user123") + image_record = Image(extension=extension, permanent=perm, type=image_type, user=user_id) filename = image_record.id + extension - filepath = os.path.abspath(os.path.join(UPLOAD_FOLDER, filename)) + filepath = os.path.abspath(os.path.join(get_user_image_dir(sanitize_email(user_id)), filename)) try: - os.makedirs(UPLOAD_FOLDER, exist_ok=True) # Save with appropriate format save_params = {} if pil_image.format == 'JPEG': diff --git a/api/reward_api.py b/api/reward_api.py index 6b79eb5..259b41e 100644 --- a/api/reward_api.py +++ b/api/reward_api.py @@ -1,12 +1,12 @@ from flask import Blueprint, request, jsonify from tinydb import Query -from events.sse import send_event_to_user +from api.utils import send_event_for_current_user +from db.db import reward_db, child_db from events.types.event import Event from events.types.event_types import EventType from events.types.reward_modified import RewardModified from models.reward import Reward -from db.db import reward_db, child_db reward_api = Blueprint('reward_api', __name__) @@ -22,9 +22,10 @@ def add_reward(): return jsonify({'error': 'Name, description, and cost are required'}), 400 reward = Reward(name=name, description=description, cost=cost, image_id=image) reward_db.insert(reward.to_dict()) - send_event_to_user("user123", Event(EventType.REWARD_MODIFIED.value, + resp = send_event_for_current_user(Event(EventType.REWARD_MODIFIED.value, RewardModified(reward.id, RewardModified.OPERATION_ADD))) - + if resp: + return resp return jsonify({'message': f'Reward {name} added.'}), 201 @@ -54,9 +55,10 @@ def delete_reward(id): if id in rewards: rewards.remove(id) child_db.update({'rewards': rewards}, ChildQuery.id == child.get('id')) - send_event_to_user("user123", Event(EventType.REWARD_MODIFIED.value, + resp = send_event_for_current_user(Event(EventType.REWARD_MODIFIED.value, RewardModified(id, RewardModified.OPERATION_DELETE))) - + if resp: + return resp return jsonify({'message': f'Reward {id} deleted.'}), 200 return jsonify({'error': 'Reward not found'}), 404 @@ -98,7 +100,9 @@ def edit_reward(id): reward_db.update(updates, RewardQuery.id == id) updated = reward_db.get(RewardQuery.id == id) - send_event_to_user("user123", Event(EventType.REWARD_MODIFIED.value, + resp = send_event_for_current_user(Event(EventType.REWARD_MODIFIED.value, RewardModified(id, RewardModified.OPERATION_EDIT))) + if resp: + return resp return jsonify(updated), 200 diff --git a/api/task_api.py b/api/task_api.py index b3a90df..44aa5d9 100644 --- a/api/task_api.py +++ b/api/task_api.py @@ -1,12 +1,12 @@ from flask import Blueprint, request, jsonify from tinydb import Query -from events.sse import send_event_to_user +from api.utils import send_event_for_current_user +from db.db import task_db, child_db from events.types.event import Event from events.types.event_types import EventType from events.types.task_modified import TaskModified from models.task import Task -from db.db import task_db, child_db task_api = Blueprint('task_api', __name__) @@ -22,8 +22,10 @@ def add_task(): return jsonify({'error': 'Name, points, and is_good are required'}), 400 task = Task(name=name, points=points, is_good=is_good, image_id=image) task_db.insert(task.to_dict()) - send_event_to_user("user123", Event(EventType.TASK_MODIFIED.value, + resp = send_event_for_current_user(Event(EventType.TASK_MODIFIED.value, TaskModified(task.id, TaskModified.OPERATION_ADD))) + if resp: + return resp return jsonify({'message': f'Task {name} added.'}), 201 @task_api.route('/task/', methods=['GET']) @@ -51,8 +53,10 @@ def delete_task(id): if id in tasks: tasks.remove(id) child_db.update({'tasks': tasks}, ChildQuery.id == child.get('id')) - send_event_to_user("user123", Event(EventType.TASK_MODIFIED.value, + resp = send_event_for_current_user(Event(EventType.TASK_MODIFIED.value, TaskModified(id, TaskModified.OPERATION_DELETE))) + if resp: + return resp return jsonify({'message': f'Task {id} deleted.'}), 200 return jsonify({'error': 'Task not found'}), 404 @@ -95,6 +99,8 @@ def edit_task(id): task_db.update(updates, TaskQuery.id == id) updated = task_db.get(TaskQuery.id == id) - send_event_to_user("user123", Event(EventType.TASK_MODIFIED.value, + resp = send_event_for_current_user(Event(EventType.TASK_MODIFIED.value, TaskModified(id, TaskModified.OPERATION_EDIT))) + if resp: + return resp return jsonify(updated), 200 diff --git a/api/utils.py b/api/utils.py new file mode 100644 index 0000000..dfe9c95 --- /dev/null +++ b/api/utils.py @@ -0,0 +1,28 @@ +import jwt +from flask import request, current_app, jsonify + +from events.sse import send_event_to_user + + +def sanitize_email(email): + return email.replace('@', '_at_').replace('.', '_dot_') + +def get_current_user_id(): + token = request.cookies.get('token') + if not token: + return None + try: + payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256']) + email = payload.get('email') + if not email: + return None + return sanitize_email(email) + except jwt.InvalidTokenError: + return None + +def send_event_for_current_user(event): + user_id = get_current_user_id() + if not user_id: + return jsonify({'error': 'Unauthorized'}), 401 + send_event_to_user(user_id, event) + return None \ No newline at end of file diff --git a/main.py b/main.py index 96a0a4d..cef9f61 100644 --- a/main.py +++ b/main.py @@ -38,7 +38,8 @@ app.config.update( MAIL_USERNAME='ryan.kegel@gmail.com', MAIL_PASSWORD='ruyj hxjf nmrz buar', MAIL_DEFAULT_SENDER='ryan.kegel@gmail.com', - FRONTEND_URL='https://localhost:5173' # Adjust as needed + FRONTEND_URL='https://localhost:5173', # Adjust as needed + SECRET_KEY='supersecretkey' # Replace with a secure key in production ) mail.init_app(app) @@ -75,7 +76,6 @@ def start_background_threads(): broadcaster.start() # TODO: implement users -os.makedirs(get_user_image_dir("user123"), exist_ok=True) initializeImages() createDefaultTasks() createDefaultRewards() diff --git a/web/vue-app/src/App.vue b/web/vue-app/src/App.vue index a4e6753..8628eaa 100644 --- a/web/vue-app/src/App.vue +++ b/web/vue-app/src/App.vue @@ -1,14 +1,15 @@ - - + +