feat: Implement user validation and ownership checks for image, reward, and task APIs
All checks were successful
Gitea Actions Demo / build-and-push (push) Successful in 36s

- Added `get_validated_user_id` utility function to validate user authentication across multiple APIs.
- Updated image upload, request, and listing endpoints to ensure user ownership and proper error handling.
- Enhanced reward management endpoints to include user validation and ownership checks.
- Modified task management endpoints to enforce user authentication and ownership verification.
- Updated models to include `user_id` for images, rewards, tasks, and children to track ownership.
- Implemented frontend changes to ensure UI reflects the ownership of tasks and rewards.
- Added a new feature specification to prevent deletion of system tasks and rewards.
This commit is contained in:
2026-01-31 19:48:51 -05:00
parent 6f5b61de7f
commit f14de28daa
18 changed files with 361 additions and 121 deletions

View File

@@ -13,22 +13,16 @@
- **Frontend Styling**: Use only `:root` CSS variables from `global.css` for all colors, spacing, and tokens. Example: `--btn-primary`, `--list-item-bg-good`. - **Frontend Styling**: Use only `:root` CSS variables from `global.css` for all colors, spacing, and tokens. Example: `--btn-primary`, `--list-item-bg-good`.
- **Scoped Styles**: All `.vue` files must use `<style scoped>`. Reference global variables for theme consistency. - **Scoped Styles**: All `.vue` files must use `<style scoped>`. Reference global variables for theme consistency.
- **Rewards UI**: If `points >= cost`, apply `--item-card-ready-shadow` and `--item-card-ready-border`.
- **API Error Handling**: Backend returns JSON with `error` and `code` (see `backend/api/error_codes.py`). Frontend extracts `{ msg, code }` using `parseErrorResponse(res)` from `api.ts`. - **API Error Handling**: Backend returns JSON with `error` and `code` (see `backend/api/error_codes.py`). Frontend extracts `{ msg, code }` using `parseErrorResponse(res)` from `api.ts`.
- **Validation**: Use `isEmailValid` and `isPasswordStrong` (min 8 chars, 1 letter, 1 number) from `api.ts` for all user input. Use `sanitize_email()` for directory names and unique IDs (see `backend/api/utils.py`).
- **JWT Auth**: Tokens are stored in HttpOnly, Secure, SameSite=Strict cookies. - **JWT Auth**: Tokens are stored in HttpOnly, Secure, SameSite=Strict cookies.
## 🚦 Frontend Logic & Event Bus ## 🚦 Frontend Logic & Event Bus
- **SSE Event Management**: Register listeners in `onMounted`, clean up in `onUnmounted`. Listen for events like `child_task_triggered`, `child_reward_request`, `task_modified`, etc. See `frontend/vue-app/src/common/backendEvents.ts` and `components/BackendEventsListener.vue`. - **SSE Event Management**: Register listeners in `onMounted`, clean up in `onUnmounted`. Listen for events like `child_task_triggered`, `child_reward_request`, `task_modified`, etc. See `frontend/vue-app/src/common/backendEvents.ts` and `components/BackendEventsListener.vue`.
- **UI Guardrails**:
- Before triggering a task, check for pending rewards. If found, prompt for cancellation before proceeding.
- On `EDIT`, always refetch the full object from the API to ensure state integrity.
- **Layout Hierarchy**: Use `ParentLayout` for admin/management, `ChildLayout` for dashboard/focus views. - **Layout Hierarchy**: Use `ParentLayout` for admin/management, `ChildLayout` for dashboard/focus views.
## ⚖️ Business Logic & Safeguards ## ⚖️ Business Logic & Safeguards
- **Points**: Always enforce `child.points = max(child.points, 0)` after any mutation.
- **Token Expiry**: Verification tokens expire in 4 hours; password reset tokens in 10 minutes. - **Token Expiry**: Verification tokens expire in 4 hours; password reset tokens in 10 minutes.
- **Image Assets**: Models use `image_id` for storage; frontend resolves to `image_url` for rendering. - **Image Assets**: Models use `image_id` for storage; frontend resolves to `image_url` for rendering.
@@ -36,7 +30,7 @@
- **Backend**: Run Flask with `python -m flask run --host=0.0.0.0 --port=5000` from the `backend/` directory. Main entry: `backend/main.py`. - **Backend**: Run Flask with `python -m flask run --host=0.0.0.0 --port=5000` from the `backend/` directory. Main entry: `backend/main.py`.
- **Frontend**: From `frontend/vue-app/`, run `npm install` then `npm run dev`. - **Frontend**: From `frontend/vue-app/`, run `npm install` then `npm run dev`.
- **Tests**: Run backend tests with `pytest` in `backend/`. Frontend tests: `npm run test` in `frontend/vue-app/`. - **Tests**: Run backend tests with `pytest` in `backend/tests/`. Frontend component tests: `npm run test` in `frontend/vue-app/components/__tests__/`.
- **Debugging**: Use VS Code launch configs or run Flask/Vue dev servers directly. For SSE, use browser dev tools to inspect event streams. - **Debugging**: Use VS Code launch configs or run Flask/Vue dev servers directly. For SSE, use browser dev tools to inspect event streams.
## 📁 Key Files & Directories ## 📁 Key Files & Directories
@@ -48,7 +42,7 @@
- `frontend/vue-app/` — Vue 3 frontend (see `src/common/`, `src/components/`, `src/layout/`) - `frontend/vue-app/` — Vue 3 frontend (see `src/common/`, `src/components/`, `src/layout/`)
- `frontend/vue-app/src/common/models.ts` — TypeScript interfaces (mirror Python models) - `frontend/vue-app/src/common/models.ts` — TypeScript interfaces (mirror Python models)
- `frontend/vue-app/src/common/api.ts` — API helpers, error parsing, validation - `frontend/vue-app/src/common/api.ts` — API helpers, error parsing, validation
- `web/vue-app/src/common/backendEvents.ts` — SSE event types and handlers - `frontend/vue-app/src/common/backendEvents.ts` — SSE event types and handlers
## 🧠 Integration & Cross-Component Patterns ## 🧠 Integration & Cross-Component Patterns

View File

@@ -0,0 +1,26 @@
# Feature: Do Not Allow System Tasks or System Rewards To Be Deleted
## Context:
- **Goal:** In Task List view and Reward List view, do not allow items to be deleted by the user if they are system tasks.
- **User Story:** As a [user], I want to only be able to press the delete button on a task or reward if that item is not a system task or reward so that shared system tasks are not deleted for other users.
## Technical Requirements
- **File Affected:** ItemList.vue, TaskView.vue, RewardView.vue, task_api.py, reward_api.py
- **Logic:**
1. Starting with ItemList.vue, we should check to see if any item in the list has an "user_id" property and if that property is null.
2. If the property is null, that means the item is not owned by a user, so do no display a delete button.
3. If the ItemList has it's deletable property as false, don't bother checking each item for user_id as the delete button will not display.
4. As a safeguard, on the backend, the DELETE api requests should check to see if the "user_id" property of the requested task or reward is null. This is done by requesting the item from the database. The request provides the item's id. If the item is a system item, return 403. Let the return tell the requestor that the item is a system item and cannot be deleted.
5. As a safeguard, make PUT/PATCH operations perform a copy-on-edit of the item. This is already implemented.
6. Bulk deletion is not possible, don't make changes for this.
7. For any item in the frontend or backend that does not have a "user_id" property, treat that as a system item (user_id=mull)
8. For both task and reward api create an application level constraint on the database that checks for user_id before mutation logic.
## Acceptance Criteria (The "Definition of Done")
- [ ] Logic: Task or Reward does not display the delete button when props.deletable is true and a list item is a system item.
- [ ] UI: Doesn't show delete button for system items.
- [ ] Backend Tests: Unit tests cover a delete API request for a system task or reward and returns a 403.
- [ ] Frontend Tests: Add vitest for this feature in the frontend to make sure the delete button hidden or shown.

View File

@@ -100,7 +100,7 @@ def verify():
if not user.email: if not user.email:
logger.error("Verified user has no email field.") logger.error("Verified user has no email field.")
else: else:
user_image_dir = get_user_image_dir(sanitize_email(user.email)) user_image_dir = get_user_image_dir(user.id)
os.makedirs(user_image_dir, exist_ok=True) os.makedirs(user_image_dir, exist_ok=True)
return jsonify({'status': status, 'reason': reason, 'code': code}), http_status return jsonify({'status': status, 'reason': reason, 'code': code}), http_status
@@ -148,6 +148,7 @@ def login():
payload = { payload = {
'email': norm_email, 'email': norm_email,
'user_id': user.id,
'exp': datetime.utcnow() + timedelta(hours=24*7) 'exp': datetime.utcnow() + timedelta(hours=24*7)
} }
token = jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256') token = jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256')
@@ -164,14 +165,14 @@ def me():
try: try:
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256']) payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
email = payload.get('email') user_id = payload.get('user_id', '')
user_dict = users_db.get(UserQuery.email == email) user_dict = users_db.get(UserQuery.id == user_id)
user = User.from_dict(user_dict) if user_dict else None user = User.from_dict(user_dict) if user_dict else None
if not user: if not user:
return jsonify({'error': 'User not found', 'code': USER_NOT_FOUND}), 404 return jsonify({'error': 'User not found', 'code': USER_NOT_FOUND}), 404
return jsonify({ return jsonify({
'email': user.email, 'email': user.email,
'id': sanitize_email(user.email), 'id': user_id,
'first_name': user.first_name, 'first_name': user.first_name,
'last_name': user.last_name, 'last_name': user.last_name,
'verified': user.verified 'verified': user.verified
@@ -253,3 +254,10 @@ def reset_password():
users_db.update(user.to_dict(), UserQuery.email == user.email) users_db.update(user.to_dict(), UserQuery.email == user.email)
return jsonify({'message': 'Password has been reset'}), 200 return jsonify({'message': 'Password has been reset'}), 200
@auth_api.route('/logout', methods=['POST'])
def logout():
resp = jsonify({'message': 'Logged out'})
# Remove the token cookie by setting it to empty and expiring it
resp.set_cookie('token', '', expires=0, httponly=True, secure=True, samesite='Strict')
return resp, 200

View File

@@ -21,6 +21,7 @@ from models.child import Child
from models.pending_reward import PendingReward from models.pending_reward import PendingReward
from models.reward import Reward from models.reward import Reward
from models.task import Task from models.task import Task
from api.utils import get_validated_user_id
import logging import logging
child_api = Blueprint('child_api', __name__) child_api = Blueprint('child_api', __name__)
@@ -29,14 +30,20 @@ logger = logging.getLogger(__name__)
@child_api.route('/child/<name>', methods=['GET']) @child_api.route('/child/<name>', methods=['GET'])
@child_api.route('/child/<id>', methods=['GET']) @child_api.route('/child/<id>', methods=['GET'])
def get_child(id): def get_child(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
return jsonify(Child.from_dict(result[0]).to_dict()), 200 return jsonify(Child.from_dict(result[0]).to_dict()), 200
@child_api.route('/child/add', methods=['PUT']) @child_api.route('/child/add', methods=['PUT'])
def add_child(): def add_child():
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
data = request.get_json() data = request.get_json()
name = data.get('name') name = data.get('name')
age = data.get('age') age = data.get('age')
@@ -46,7 +53,7 @@ def add_child():
if not image: if not image:
image = 'boy01' image = 'boy01'
child = Child(name=name, age=age, image_id=image) child = Child(name=name, age=age, image_id=image, user_id=user_id)
child_db.insert(child.to_dict()) child_db.insert(child.to_dict())
resp = send_event_for_current_user( resp = send_event_for_current_user(
Event(EventType.CHILD_MODIFIED.value, ChildModified(child.id, ChildModified.OPERATION_ADD))) Event(EventType.CHILD_MODIFIED.value, ChildModified(child.id, ChildModified.OPERATION_ADD)))
@@ -56,13 +63,16 @@ def add_child():
@child_api.route('/child/<id>/edit', methods=['PUT']) @child_api.route('/child/<id>/edit', methods=['PUT'])
def edit_child(id): def edit_child(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
data = request.get_json() data = request.get_json()
name = data.get('name', None) name = data.get('name', None)
age = data.get('age', None) age = data.get('age', None)
points = data.get('points', None) points = data.get('points', None)
image = data.get('image_id', None) image = data.get('image_id', None)
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
child = Child.from_dict(result[0]) child = Child.from_dict(result[0])
@@ -79,18 +89,18 @@ def edit_child(id):
# Check if points changed and handle pending rewards # Check if points changed and handle pending rewards
if points is not None: if points is not None:
PendingQuery = Query() PendingQuery = Query()
pending_rewards = pending_reward_db.search(PendingQuery.child_id == id) pending_rewards = pending_reward_db.search((PendingQuery.child_id == id) & (PendingQuery.user_id == user_id))
RewardQuery = Query() RewardQuery = Query()
for pr in pending_rewards: for pr in pending_rewards:
pending = PendingReward.from_dict(pr) pending = PendingReward.from_dict(pr)
reward_result = reward_db.get(RewardQuery.id == pending.reward_id) reward_result = reward_db.get((RewardQuery.id == pending.reward_id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
if reward_result: if reward_result:
reward = Reward.from_dict(reward_result) reward = Reward.from_dict(reward_result)
# If child can no longer afford the reward, remove the pending request # If child can no longer afford the reward, remove the pending request
if child.points < reward.cost: if child.points < reward.cost:
pending_reward_db.remove( pending_reward_db.remove(
(PendingQuery.child_id == id) & (PendingQuery.reward_id == reward.id) (PendingQuery.child_id == id) & (PendingQuery.reward_id == reward.id) & (PendingQuery.user_id == user_id)
) )
resp = send_event_for_current_user( resp = send_event_for_current_user(
Event(EventType.CHILD_REWARD_REQUEST.value, ChildRewardRequest(id, reward.id, ChildRewardRequest.REQUEST_CANCELLED))) Event(EventType.CHILD_REWARD_REQUEST.value, ChildRewardRequest(id, reward.id, ChildRewardRequest.REQUEST_CANCELLED)))
@@ -104,14 +114,21 @@ def edit_child(id):
@child_api.route('/child/list', methods=['GET']) @child_api.route('/child/list', methods=['GET'])
def list_children(): def list_children():
children = child_db.all() user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
ChildQuery = Query()
children = child_db.search(ChildQuery.user_id == user_id)
return jsonify({'children': children}), 200 return jsonify({'children': children}), 200
# Child DELETE # Child DELETE
@child_api.route('/child/<id>', methods=['DELETE']) @child_api.route('/child/<id>', methods=['DELETE'])
def delete_child(id): def delete_child(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
ChildQuery = Query() ChildQuery = Query()
if child_db.remove(ChildQuery.id == id): if child_db.remove((ChildQuery.id == id) & (ChildQuery.user_id == user_id)):
resp = send_event_for_current_user(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: if resp:
return resp return resp
@@ -120,13 +137,16 @@ def delete_child(id):
@child_api.route('/child/<id>/assign-task', methods=['POST']) @child_api.route('/child/<id>/assign-task', methods=['POST'])
def assign_task_to_child(id): def assign_task_to_child(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
data = request.get_json() data = request.get_json()
task_id = data.get('task_id') task_id = data.get('task_id')
if not task_id: if not task_id:
return jsonify({'error': 'task_id is required'}), 400 return jsonify({'error': 'task_id is required'}), 400
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
@@ -139,6 +159,9 @@ def assign_task_to_child(id):
# python # python
@child_api.route('/child/<id>/set-tasks', methods=['PUT']) @child_api.route('/child/<id>/set-tasks', methods=['PUT'])
def set_child_tasks(id): def set_child_tasks(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
data = request.get_json() or {} data = request.get_json() or {}
task_ids = data.get('task_ids') task_ids = data.get('task_ids')
if 'type' not in data: if 'type' not in data:
@@ -151,7 +174,7 @@ def set_child_tasks(id):
return jsonify({'error': 'task_ids must be a list'}), 400 return jsonify({'error': 'task_ids must be a list'}), 400
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
child = Child.from_dict(result[0]) child = Child.from_dict(result[0])
@@ -179,13 +202,16 @@ def set_child_tasks(id):
@child_api.route('/child/<id>/remove-task', methods=['POST']) @child_api.route('/child/<id>/remove-task', methods=['POST'])
def remove_task_from_child(id): def remove_task_from_child(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
data = request.get_json() data = request.get_json()
task_id = data.get('task_id') task_id = data.get('task_id')
if not task_id: if not task_id:
return jsonify({'error': 'task_id is required'}), 400 return jsonify({'error': 'task_id is required'}), 400
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
@@ -198,8 +224,11 @@ def remove_task_from_child(id):
@child_api.route('/child/<id>/list-tasks', methods=['GET']) @child_api.route('/child/<id>/list-tasks', methods=['GET'])
def list_child_tasks(id): def list_child_tasks(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
@@ -209,7 +238,7 @@ def list_child_tasks(id):
TaskQuery = Query() TaskQuery = Query()
child_tasks = [] child_tasks = []
for tid in task_ids: for tid in task_ids:
task = task_db.get(TaskQuery.id == tid) task = task_db.get((TaskQuery.id == tid) & ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None)))
if not task: if not task:
continue continue
ct = ChildTask(task.get('name'), task.get('is_good'), task.get('points'), task.get('image_id'), task.get('id')) ct = ChildTask(task.get('name'), task.get('is_good'), task.get('points'), task.get('image_id'), task.get('id'))
@@ -219,8 +248,11 @@ def list_child_tasks(id):
@child_api.route('/child/<id>/list-assignable-tasks', methods=['GET']) @child_api.route('/child/<id>/list-assignable-tasks', methods=['GET'])
def list_assignable_tasks(id): def list_assignable_tasks(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
@@ -237,7 +269,7 @@ def list_assignable_tasks(id):
TaskQuery = Query() TaskQuery = Query()
assignable_tasks = [] assignable_tasks = []
for tid in assignable_ids: for tid in assignable_ids:
task = task_db.get(TaskQuery.id == tid) task = task_db.get((TaskQuery.id == tid) & ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None)))
if not task: if not task:
continue continue
ct = ChildTask(task.get('name'), task.get('is_good'), task.get('points'), task.get('image_id'), task.get('id')) ct = ChildTask(task.get('name'), task.get('is_good'), task.get('points'), task.get('image_id'), task.get('id'))
@@ -248,8 +280,11 @@ def list_assignable_tasks(id):
@child_api.route('/child/<id>/list-all-tasks', methods=['GET']) @child_api.route('/child/<id>/list-all-tasks', methods=['GET'])
def list_all_tasks(id): def list_all_tasks(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
has_type = "type" in request.args has_type = "type" in request.args
@@ -261,14 +296,12 @@ def list_all_tasks(id):
assigned_ids = set(child.get('tasks', [])) assigned_ids = set(child.get('tasks', []))
# Get all tasks from database # Get all tasks from database
all_tasks = task_db.all() ChildTaskQuery = Query()
all_tasks = task_db.search((ChildTaskQuery.user_id == user_id) | (ChildTaskQuery.user_id == None))
tasks = [] tasks = []
for task in all_tasks: for task in all_tasks:
if not task or not task.get('id'):
continue
ct = ChildTask( ct = ChildTask(
task.get('name'), task.get('name'),
task.get('is_good'), task.get('is_good'),
@@ -289,13 +322,16 @@ def list_all_tasks(id):
@child_api.route('/child/<id>/trigger-task', methods=['POST']) @child_api.route('/child/<id>/trigger-task', methods=['POST'])
def trigger_child_task(id): def trigger_child_task(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
data = request.get_json() data = request.get_json()
task_id = data.get('task_id') task_id = data.get('task_id')
if not task_id: if not task_id:
return jsonify({'error': 'task_id is required'}), 400 return jsonify({'error': 'task_id is required'}), 400
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
@@ -304,7 +340,7 @@ def trigger_child_task(id):
return jsonify({'error': f'Task not found assigned to child {child.name}'}), 404 return jsonify({'error': f'Task not found assigned to child {child.name}'}), 404
# look up the task and get the details # look up the task and get the details
TaskQuery = Query() TaskQuery = Query()
task_result = task_db.search(TaskQuery.id == task_id) task_result = task_db.search((TaskQuery.id == task_id) & ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None)))
if not task_result: if not task_result:
return jsonify({'error': 'Task not found in task database'}), 404 return jsonify({'error': 'Task not found in task database'}), 404
task: Task = Task.from_dict(task_result[0]) task: Task = Task.from_dict(task_result[0])
@@ -323,13 +359,16 @@ def trigger_child_task(id):
@child_api.route('/child/<id>/assign-reward', methods=['POST']) @child_api.route('/child/<id>/assign-reward', methods=['POST'])
def assign_reward_to_child(id): def assign_reward_to_child(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
data = request.get_json() data = request.get_json()
reward_id = data.get('reward_id') reward_id = data.get('reward_id')
if not reward_id: if not reward_id:
return jsonify({'error': 'reward_id is required'}), 400 return jsonify({'error': 'reward_id is required'}), 400
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
@@ -341,22 +380,23 @@ def assign_reward_to_child(id):
@child_api.route('/child/<id>/list-all-rewards', methods=['GET']) @child_api.route('/child/<id>/list-all-rewards', methods=['GET'])
def list_all_rewards(id): def list_all_rewards(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
child = result[0] child = Child.from_dict(result[0])
assigned_ids = set(child.get('rewards', [])) assigned_ids = set(child.rewards)
# Get all rewards from database # Get all rewards from database
all_rewards = reward_db.all() ChildRewardQuery = Query()
all_rewards = reward_db.search((ChildRewardQuery.user_id == user_id) | (ChildRewardQuery.user_id == None))
rewards = [] rewards = []
for reward in all_rewards: for reward in all_rewards:
if not reward or not reward.get('id'):
continue
cr = ChildReward( cr = ChildReward(
reward.get('name'), reward.get('name'),
reward.get('cost'), reward.get('cost'),
@@ -379,6 +419,9 @@ def list_all_rewards(id):
@child_api.route('/child/<id>/set-rewards', methods=['PUT']) @child_api.route('/child/<id>/set-rewards', methods=['PUT'])
def set_child_rewards(id): def set_child_rewards(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
data = request.get_json() or {} data = request.get_json() or {}
reward_ids = data.get('reward_ids') reward_ids = data.get('reward_ids')
if not isinstance(reward_ids, list): if not isinstance(reward_ids, list):
@@ -388,7 +431,7 @@ def set_child_rewards(id):
new_reward_ids = [rid for rid in dict.fromkeys(reward_ids) if rid] new_reward_ids = [rid for rid in dict.fromkeys(reward_ids) if rid]
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
@@ -396,7 +439,7 @@ def set_child_rewards(id):
RewardQuery = Query() RewardQuery = Query()
valid_reward_ids = [] valid_reward_ids = []
for rid in new_reward_ids: for rid in new_reward_ids:
if reward_db.get(RewardQuery.id == rid): if reward_db.get((RewardQuery.id == rid) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None))):
valid_reward_ids.append(rid) valid_reward_ids.append(rid)
# Replace rewards with validated IDs # Replace rewards with validated IDs
@@ -411,13 +454,16 @@ def set_child_rewards(id):
@child_api.route('/child/<id>/remove-reward', methods=['POST']) @child_api.route('/child/<id>/remove-reward', methods=['POST'])
def remove_reward_from_child(id): def remove_reward_from_child(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
data = request.get_json() data = request.get_json()
reward_id = data.get('reward_id') reward_id = data.get('reward_id')
if not reward_id: if not reward_id:
return jsonify({'error': 'reward_id is required'}), 400 return jsonify({'error': 'reward_id is required'}), 400
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
@@ -430,8 +476,11 @@ def remove_reward_from_child(id):
@child_api.route('/child/<id>/list-rewards', methods=['GET']) @child_api.route('/child/<id>/list-rewards', methods=['GET'])
def list_child_rewards(id): def list_child_rewards(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
@@ -441,7 +490,7 @@ def list_child_rewards(id):
RewardQuery = Query() RewardQuery = Query()
child_rewards = [] child_rewards = []
for rid in reward_ids: for rid in reward_ids:
reward = reward_db.get(RewardQuery.id == rid) reward = reward_db.get((RewardQuery.id == rid) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
if not reward: if not reward:
continue continue
cr = ChildReward(reward.get('name'), reward.get('cost'), reward.get('image_id'), reward.get('id')) cr = ChildReward(reward.get('name'), reward.get('cost'), reward.get('image_id'), reward.get('id'))
@@ -451,8 +500,11 @@ def list_child_rewards(id):
@child_api.route('/child/<id>/list-assignable-rewards', methods=['GET']) @child_api.route('/child/<id>/list-assignable-rewards', methods=['GET'])
def list_assignable_rewards(id): def list_assignable_rewards(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
@@ -465,7 +517,7 @@ def list_assignable_rewards(id):
RewardQuery = Query() RewardQuery = Query()
assignable_rewards = [] assignable_rewards = []
for rid in assignable_ids: for rid in assignable_ids:
reward = reward_db.get(RewardQuery.id == rid) reward = reward_db.get((RewardQuery.id == rid) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
if not reward: if not reward:
continue continue
cr = ChildReward(reward.get('name'), reward.get('cost'), reward.get('image_id'), reward.get('id')) cr = ChildReward(reward.get('name'), reward.get('cost'), reward.get('image_id'), reward.get('id'))
@@ -475,13 +527,16 @@ def list_assignable_rewards(id):
@child_api.route('/child/<id>/trigger-reward', methods=['POST']) @child_api.route('/child/<id>/trigger-reward', methods=['POST'])
def trigger_child_reward(id): def trigger_child_reward(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
data = request.get_json() data = request.get_json()
reward_id = data.get('reward_id') reward_id = data.get('reward_id')
if not reward_id: if not reward_id:
return jsonify({'error': 'reward_id is required'}), 400 return jsonify({'error': 'reward_id is required'}), 400
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
@@ -490,7 +545,7 @@ def trigger_child_reward(id):
return jsonify({'error': f'Reward not found assigned to child {child.name}'}), 404 return jsonify({'error': f'Reward not found assigned to child {child.name}'}), 404
# look up the task and get the details # look up the task and get the details
RewardQuery = Query() RewardQuery = Query()
reward_result = reward_db.search(RewardQuery.id == reward_id) reward_result = reward_db.search((RewardQuery.id == reward_id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
if not reward_result: if not reward_result:
return jsonify({'error': 'Reward not found in reward database'}), 404 return jsonify({'error': 'Reward not found in reward database'}), 404
reward: Reward = Reward.from_dict(reward_result[0]) reward: Reward = Reward.from_dict(reward_result[0])
@@ -523,8 +578,11 @@ def trigger_child_reward(id):
@child_api.route('/child/<id>/affordable-rewards', methods=['GET']) @child_api.route('/child/<id>/affordable-rewards', methods=['GET'])
def list_affordable_rewards(id): def list_affordable_rewards(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
@@ -534,14 +592,17 @@ def list_affordable_rewards(id):
RewardQuery = Query() RewardQuery = Query()
affordable = [ affordable = [
Reward.from_dict(reward).to_dict() for reward_id in reward_ids Reward.from_dict(reward).to_dict() for reward_id in reward_ids
if (reward := reward_db.get(RewardQuery.id == reward_id)) and points >= Reward.from_dict(reward).cost if (reward := reward_db.get((RewardQuery.id == reward_id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))) and points >= Reward.from_dict(reward).cost
] ]
return jsonify({'affordable_rewards': affordable}), 200 return jsonify({'affordable_rewards': affordable}), 200
@child_api.route('/child/<id>/reward-status', methods=['GET']) @child_api.route('/child/<id>/reward-status', methods=['GET'])
def reward_status(id): def reward_status(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
@@ -552,13 +613,13 @@ def reward_status(id):
RewardQuery = Query() RewardQuery = Query()
statuses = [] statuses = []
for reward_id in reward_ids: for reward_id in reward_ids:
reward: Reward = Reward.from_dict(reward_db.get(RewardQuery.id == reward_id)) reward: Reward = Reward.from_dict(reward_db.get((RewardQuery.id == reward_id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None))))
if not reward: if not reward:
continue continue
points_needed = max(0, reward.cost - points) points_needed = max(0, reward.cost - points)
#check to see if this reward id and child id is in the pending rewards db if so set its redeeming flag to true #check to see if this reward id and child id is in the pending rewards db if so set its redeeming flag to true
pending_query = Query() pending_query = Query()
pending = pending_reward_db.get((pending_query.child_id == child.id) & (pending_query.reward_id == reward.id)) pending = pending_reward_db.get((pending_query.child_id == child.id) & (pending_query.reward_id == reward.id) & (pending_query.user_id == user_id))
status = RewardStatus(reward.id, reward.name, points_needed, reward.cost, pending is not None, reward.image_id) status = RewardStatus(reward.id, reward.name, points_needed, reward.cost, pending is not None, reward.image_id)
statuses.append(status.to_dict()) statuses.append(status.to_dict())
@@ -568,13 +629,16 @@ def reward_status(id):
@child_api.route('/child/<id>/request-reward', methods=['POST']) @child_api.route('/child/<id>/request-reward', methods=['POST'])
def request_reward(id): def request_reward(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
data = request.get_json() data = request.get_json()
reward_id = data.get('reward_id') reward_id = data.get('reward_id')
if not reward_id: if not reward_id:
return jsonify({'error': 'reward_id is required'}), 400 return jsonify({'error': 'reward_id is required'}), 400
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
@@ -600,7 +664,7 @@ def request_reward(id):
'reward_cost': reward.cost 'reward_cost': reward.cost
}), 400 }), 400
pending = PendingReward(child_id=child.id, reward_id=reward.id) pending = PendingReward(child_id=child.id, reward_id=reward.id, user_id=user_id)
pending_reward_db.insert(pending.to_dict()) pending_reward_db.insert(pending.to_dict())
logger.info(f'Pending reward request created for child {child.name} for reward {reward.name}') logger.info(f'Pending reward request created for child {child.name} for reward {reward.name}')
send_event_for_current_user(Event(EventType.CHILD_REWARD_REQUEST.value, ChildRewardRequest(child.id, reward.id, ChildRewardRequest.REQUEST_CREATED))) send_event_for_current_user(Event(EventType.CHILD_REWARD_REQUEST.value, ChildRewardRequest(child.id, reward.id, ChildRewardRequest.REQUEST_CREATED)))
@@ -615,13 +679,16 @@ def request_reward(id):
@child_api.route('/child/<id>/cancel-request-reward', methods=['POST']) @child_api.route('/child/<id>/cancel-request-reward', methods=['POST'])
def cancel_request_reward(id): def cancel_request_reward(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
data = request.get_json() data = request.get_json()
reward_id = data.get('reward_id') reward_id = data.get('reward_id')
if not reward_id: if not reward_id:
return jsonify({'error': 'reward_id is required'}), 400 return jsonify({'error': 'reward_id is required'}), 400
ChildQuery = Query() ChildQuery = Query()
result = child_db.search(ChildQuery.id == id) result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result: if not result:
return jsonify({'error': 'Child not found'}), 404 return jsonify({'error': 'Child not found'}), 404
@@ -630,7 +697,7 @@ def cancel_request_reward(id):
# Remove matching pending reward request # Remove matching pending reward request
PendingQuery = Query() PendingQuery = Query()
removed = pending_reward_db.remove( removed = pending_reward_db.remove(
(PendingQuery.child_id == child.id) & (PendingQuery.reward_id == reward_id) (PendingQuery.child_id == child.id) & (PendingQuery.reward_id == reward_id) & (PendingQuery.user_id == user_id)
) )
if not removed: if not removed:
@@ -651,7 +718,11 @@ def cancel_request_reward(id):
@child_api.route('/pending-rewards', methods=['GET']) @child_api.route('/pending-rewards', methods=['GET'])
def list_pending_rewards(): def list_pending_rewards():
pending_rewards = pending_reward_db.all() user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
PendingQuery = Query()
pending_rewards = pending_reward_db.search(PendingQuery.user_id == user_id)
reward_responses = [] reward_responses = []
RewardQuery = Query() RewardQuery = Query()
@@ -661,7 +732,7 @@ def list_pending_rewards():
pending = PendingReward.from_dict(pr) pending = PendingReward.from_dict(pr)
# Look up reward details # Look up reward details
reward_result = reward_db.get(RewardQuery.id == pending.reward_id) reward_result = reward_db.get((RewardQuery.id == pending.reward_id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
if not reward_result: if not reward_result:
continue continue
reward = Reward.from_dict(reward_result) reward = Reward.from_dict(reward_result)

View File

@@ -4,7 +4,7 @@ from PIL import Image as PILImage, UnidentifiedImageError
from flask import Blueprint, request, jsonify, send_file from flask import Blueprint, request, jsonify, send_file
from tinydb import Query from tinydb import Query
from api.utils import get_current_user_id, sanitize_email from api.utils import get_current_user_id, sanitize_email, get_validated_user_id
from config.paths import get_user_image_dir from config.paths import get_user_image_dir
from db.db import image_db from db.db import image_db
@@ -21,9 +21,9 @@ def allowed_file(filename):
@image_api.route('/image/upload', methods=['POST']) @image_api.route('/image/upload', methods=['POST'])
def upload(): def upload():
user_id = get_current_user_id() user_id = get_validated_user_id()
if not user_id: if not user_id:
return jsonify({'error': 'User not authenticated'}), 401 return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
if 'file' not in request.files: if 'file' not in request.files:
return jsonify({'error': 'No file part in the request'}), 400 return jsonify({'error': 'No file part in the request'}), 400
file = request.files['file'] file = request.files['file']
@@ -64,8 +64,11 @@ def upload():
format_extension_map = {'JPEG': '.jpg', 'PNG': '.png'} format_extension_map = {'JPEG': '.jpg', 'PNG': '.png'}
extension = format_extension_map.get(original_format, '.png') extension = format_extension_map.get(original_format, '.png')
image_record = Image(extension=extension, permanent=perm, type=image_type, user=user_id) image_record = Image(extension=extension, permanent=perm, type=image_type, user_id=user_id)
filename = image_record.id + extension filename = image_record.id + extension
user_image_dir = get_user_image_dir(user_id)
os.makedirs(user_image_dir, exist_ok=True)
filepath = os.path.abspath(os.path.join(get_user_image_dir(sanitize_email(user_id)), filename)) filepath = os.path.abspath(os.path.join(get_user_image_dir(sanitize_email(user_id)), filename))
try: try:
@@ -84,25 +87,35 @@ def upload():
@image_api.route('/image/request/<id>', methods=['GET']) @image_api.route('/image/request/<id>', methods=['GET'])
def request_image(id): def request_image(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
ImageQuery = Query() ImageQuery = Query()
image: Image = Image.from_dict(image_db.get(ImageQuery.id == id)) image_record = image_db.get(ImageQuery.id == id)
if not image: if not image_record:
return jsonify({'error': 'Image not found'}), 404 return jsonify({'error': 'Image not found'}), 404
image = Image.from_dict(image_record)
# Allow if image.user_id is None (public image), or matches user_id
if image.user_id is not None and image.user_id != user_id:
return jsonify({'error': 'Forbidden: image does not belong to user', 'code': 'FORBIDDEN'}), 403
filename = f"{image.id}{image.extension}" filename = f"{image.id}{image.extension}"
filepath = os.path.abspath(os.path.join(get_user_image_dir(image.user), filename)) filepath = os.path.abspath(os.path.join(get_user_image_dir(image.user_id or user_id), filename))
if not os.path.exists(filepath): if not os.path.exists(filepath):
return jsonify({'error': 'File not found'}), 404 return jsonify({'error': 'File not found'}), 404
return send_file(filepath) return send_file(filepath)
@image_api.route('/image/list', methods=['GET']) @image_api.route('/image/list', methods=['GET'])
def list_images(): def list_images():
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
image_type = request.args.get('type', type=int) image_type = request.args.get('type', type=int)
ImageQuery = Query() ImageQuery = Query()
if image_type is not None: if image_type is not None:
if image_type not in [IMAGE_TYPE_PROFILE, IMAGE_TYPE_ICON]: if image_type not in [IMAGE_TYPE_PROFILE, IMAGE_TYPE_ICON]:
return jsonify({'error': 'Invalid image type'}), 400 return jsonify({'error': 'Invalid image type'}), 400
images = image_db.search(ImageQuery.type == image_type) images = image_db.search((ImageQuery.type == image_type) & ((ImageQuery.user_id == user_id) | (ImageQuery.user_id == None)))
else: else:
images = image_db.all() images = image_db.search((ImageQuery.user_id == user_id) | (ImageQuery.user_id == None))
image_ids = [img['id'] for img in images] image_ids = [img['id'] for img in images]
return jsonify({'ids': image_ids, 'count': len(image_ids)}), 200 return jsonify({'ids': image_ids, 'count': len(image_ids)}), 200

View File

@@ -1,7 +1,7 @@
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from tinydb import Query from tinydb import Query
from api.utils import send_event_for_current_user from api.utils import send_event_for_current_user, get_validated_user_id
from backend.events.types.child_rewards_set import ChildRewardsSet from backend.events.types.child_rewards_set import ChildRewardsSet
from db.db import reward_db, child_db from db.db import reward_db, child_db
from events.types.event import Event from events.types.event import Event
@@ -14,6 +14,9 @@ reward_api = Blueprint('reward_api', __name__)
# Reward endpoints # Reward endpoints
@reward_api.route('/reward/add', methods=['PUT']) @reward_api.route('/reward/add', methods=['PUT'])
def add_reward(): def add_reward():
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
data = request.get_json() data = request.get_json()
name = data.get('name') name = data.get('name')
description = data.get('description') description = data.get('description')
@@ -21,7 +24,7 @@ def add_reward():
image = data.get('image_id', '') image = data.get('image_id', '')
if not name or description is None or cost is None: if not name or description is None or cost is None:
return jsonify({'error': 'Name, description, and cost are required'}), 400 return jsonify({'error': 'Name, description, and cost are required'}), 400
reward = Reward(name=name, description=description, cost=cost, image_id=image) reward = Reward(name=name, description=description, cost=cost, image_id=image, user_id=user_id)
reward_db.insert(reward.to_dict()) reward_db.insert(reward.to_dict())
send_event_for_current_user(Event(EventType.REWARD_MODIFIED.value, RewardModified(reward.id, RewardModified.OPERATION_ADD))) send_event_for_current_user(Event(EventType.REWARD_MODIFIED.value, RewardModified(reward.id, RewardModified.OPERATION_ADD)))
return jsonify({'message': f'Reward {name} added.'}), 201 return jsonify({'message': f'Reward {name} added.'}), 201
@@ -30,28 +33,46 @@ def add_reward():
@reward_api.route('/reward/<id>', methods=['GET']) @reward_api.route('/reward/<id>', methods=['GET'])
def get_reward(id): def get_reward(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
RewardQuery = Query() RewardQuery = Query()
result = reward_db.search(RewardQuery.id == id) result = reward_db.search((RewardQuery.id == id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
if not result: if not result:
return jsonify({'error': 'Reward not found'}), 404 return jsonify({'error': 'Reward not found'}), 404
return jsonify(result[0]), 200 return jsonify(result[0]), 200
@reward_api.route('/reward/list', methods=['GET']) @reward_api.route('/reward/list', methods=['GET'])
def list_rewards(): def list_rewards():
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
ids_param = request.args.get('ids') ids_param = request.args.get('ids')
rewards = reward_db.all() RewardQuery = Query()
rewards = reward_db.search((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None))
if ids_param is not None: if ids_param is not None:
if ids_param.strip() == '': if ids_param.strip() == '':
rewards = [] rewards = []
else: else:
ids = set(ids_param.split(',')) ids = set(ids_param.split(','))
rewards = [reward for reward in rewards if reward.get('id') in ids] rewards = [reward for reward in rewards if reward.get('id') in ids]
return jsonify({'rewards': rewards}), 200
# Filter out default rewards if user-specific version exists (case/whitespace-insensitive)
user_rewards = {r['name'].strip().lower(): r for r in rewards if r.get('user_id') == user_id}
filtered_rewards = []
for r in rewards:
if r.get('user_id') is None and r['name'].strip().lower() in user_rewards:
continue # Skip default if user version exists
filtered_rewards.append(r)
return jsonify({'rewards': filtered_rewards}), 200
@reward_api.route('/reward/<id>', methods=['DELETE']) @reward_api.route('/reward/<id>', methods=['DELETE'])
def delete_reward(id): def delete_reward(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
RewardQuery = Query() RewardQuery = Query()
removed = reward_db.remove(RewardQuery.id == id) removed = reward_db.remove((RewardQuery.id == id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
if removed: if removed:
# remove the reward id from any child's reward list # remove the reward id from any child's reward list
ChildQuery = Query() ChildQuery = Query()
@@ -67,25 +88,31 @@ def delete_reward(id):
@reward_api.route('/reward/<id>/edit', methods=['PUT']) @reward_api.route('/reward/<id>/edit', methods=['PUT'])
def edit_reward(id): def edit_reward(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
RewardQuery = Query() RewardQuery = Query()
existing = reward_db.get(RewardQuery.id == id) existing = reward_db.get((RewardQuery.id == id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
if not existing: if not existing:
return jsonify({'error': 'Reward not found'}), 404 return jsonify({'error': 'Reward not found'}), 404
data = request.get_json(force=True) or {} reward = Reward.from_dict(existing)
updates = {} is_dirty = False
data = request.get_json(force=True) or {}
if 'name' in data: if 'name' in data:
name = (data.get('name') or '').strip() name = (data.get('name') or '').strip()
if not name: if not name:
return jsonify({'error': 'Name cannot be empty'}), 400 return jsonify({'error': 'Name cannot be empty'}), 400
updates['name'] = name reward.name = name
is_dirty = True
if 'description' in data: if 'description' in data:
desc = (data.get('description') or '').strip() desc = (data.get('description') or '').strip()
if not desc: if not desc:
return jsonify({'error': 'Description cannot be empty'}), 400 return jsonify({'error': 'Description cannot be empty'}), 400
updates['description'] = desc reward.description = desc
is_dirty = True
if 'cost' in data: if 'cost' in data:
cost = data.get('cost') cost = data.get('cost')
@@ -93,17 +120,24 @@ def edit_reward(id):
return jsonify({'error': 'Cost must be an integer'}), 400 return jsonify({'error': 'Cost must be an integer'}), 400
if cost <= 0: if cost <= 0:
return jsonify({'error': 'Cost must be a positive integer'}), 400 return jsonify({'error': 'Cost must be a positive integer'}), 400
updates['cost'] = cost reward.cost = cost
is_dirty = True
if 'image_id' in data: if 'image_id' in data:
updates['image_id'] = data.get('image_id', '') reward.image_id = data.get('image_id', '')
is_dirty = True
if not updates: if not is_dirty:
return jsonify({'error': 'No valid fields to update'}), 400 return jsonify({'error': 'No valid fields to update'}), 400
reward_db.update(updates, RewardQuery.id == id) if reward.user_id is None: # public reward
updated = reward_db.get(RewardQuery.id == id) new_reward = Reward(name=reward.name, description=reward.description, cost=reward.cost, image_id=reward.image_id, user_id=user_id)
reward_db.insert(new_reward.to_dict())
send_event_for_current_user(Event(EventType.REWARD_MODIFIED.value,
RewardModified(new_reward.id, RewardModified.OPERATION_ADD)))
return jsonify(new_reward.to_dict()), 200
reward_db.update(reward.to_dict(), (RewardQuery.id == id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
send_event_for_current_user(Event(EventType.REWARD_MODIFIED.value, send_event_for_current_user(Event(EventType.REWARD_MODIFIED.value,
RewardModified(id, RewardModified.OPERATION_EDIT))) RewardModified(id, RewardModified.OPERATION_EDIT)))
return jsonify(reward.to_dict()), 200
return jsonify(updated), 200

View File

@@ -1,7 +1,7 @@
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from tinydb import Query from tinydb import Query
from api.utils import send_event_for_current_user from api.utils import send_event_for_current_user, get_validated_user_id
from backend.events.types.child_tasks_set import ChildTasksSet from backend.events.types.child_tasks_set import ChildTasksSet
from db.db import task_db, child_db from db.db import task_db, child_db
from events.types.event import Event from events.types.event import Event
@@ -14,6 +14,9 @@ task_api = Blueprint('task_api', __name__)
# Task endpoints # Task endpoints
@task_api.route('/task/add', methods=['PUT']) @task_api.route('/task/add', methods=['PUT'])
def add_task(): def add_task():
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
data = request.get_json() data = request.get_json()
name = data.get('name') name = data.get('name')
points = data.get('points') points = data.get('points')
@@ -21,7 +24,7 @@ def add_task():
image = data.get('image_id', '') image = data.get('image_id', '')
if not name or points is None or is_good is None: if not name or points is None or is_good is None:
return jsonify({'error': 'Name, points, and is_good are required'}), 400 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 = Task(name=name, points=points, is_good=is_good, image_id=image, user_id=user_id)
task_db.insert(task.to_dict()) task_db.insert(task.to_dict())
send_event_for_current_user(Event(EventType.TASK_MODIFIED.value, send_event_for_current_user(Event(EventType.TASK_MODIFIED.value,
TaskModified(task.id, TaskModified.OPERATION_ADD))) TaskModified(task.id, TaskModified.OPERATION_ADD)))
@@ -29,28 +32,45 @@ def add_task():
@task_api.route('/task/<id>', methods=['GET']) @task_api.route('/task/<id>', methods=['GET'])
def get_task(id): def get_task(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
TaskQuery = Query() TaskQuery = Query()
result = task_db.search(TaskQuery.id == id) result = task_db.search((TaskQuery.id == id) & ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None)))
if not result: if not result:
return jsonify({'error': 'Task not found'}), 404 return jsonify({'error': 'Task not found'}), 404
return jsonify(result[0]), 200 return jsonify(result[0]), 200
@task_api.route('/task/list', methods=['GET']) @task_api.route('/task/list', methods=['GET'])
def list_tasks(): def list_tasks():
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
ids_param = request.args.get('ids') ids_param = request.args.get('ids')
tasks = task_db.all() TaskQuery = Query()
tasks = task_db.search((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None))
if ids_param is not None: if ids_param is not None:
if ids_param.strip() == '': if ids_param.strip() == '':
tasks = [] tasks = []
else: else:
ids = set(ids_param.split(',')) ids = set(ids_param.split(','))
tasks = [task for task in tasks if task.get('id') in ids] tasks = [task for task in tasks if task.get('id') in ids]
return jsonify({'tasks': tasks}), 200
user_tasks = {t['name'].strip().lower(): t for t in tasks if t.get('user_id') == user_id}
filtered_tasks = []
for t in tasks:
if t.get('user_id') is None and t['name'].strip().lower() in user_tasks:
continue # Skip default if user version exists
filtered_tasks.append(t)
return jsonify({'tasks': filtered_tasks}), 200
@task_api.route('/task/<id>', methods=['DELETE']) @task_api.route('/task/<id>', methods=['DELETE'])
def delete_task(id): def delete_task(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
TaskQuery = Query() TaskQuery = Query()
removed = task_db.remove(TaskQuery.id == id) removed = task_db.remove((TaskQuery.id == id) & ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None)))
if removed: if removed:
# remove the task id from any child's task list # remove the task id from any child's task list
ChildQuery = Query() ChildQuery = Query()
@@ -66,11 +86,17 @@ def delete_task(id):
@task_api.route('/task/<id>/edit', methods=['PUT']) @task_api.route('/task/<id>/edit', methods=['PUT'])
def edit_task(id): def edit_task(id):
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
TaskQuery = Query() TaskQuery = Query()
existing = task_db.get(TaskQuery.id == id) existing = task_db.get((TaskQuery.id == id) & ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None)))
if not existing: if not existing:
return jsonify({'error': 'Task not found'}), 404 return jsonify({'error': 'Task not found'}), 404
task = Task.from_dict(existing)
is_dirty = False
data = request.get_json(force=True) or {} data = request.get_json(force=True) or {}
updates = {} updates = {}
@@ -78,7 +104,8 @@ def edit_task(id):
name = data.get('name', '').strip() name = data.get('name', '').strip()
if not name: if not name:
return jsonify({'error': 'Name cannot be empty'}), 400 return jsonify({'error': 'Name cannot be empty'}), 400
updates['name'] = name task.name = name
is_dirty = True
if 'points' in data: if 'points' in data:
points = data.get('points') points = data.get('points')
@@ -86,22 +113,32 @@ def edit_task(id):
return jsonify({'error': 'Points must be an integer'}), 400 return jsonify({'error': 'Points must be an integer'}), 400
if points <= 0: if points <= 0:
return jsonify({'error': 'Points must be a positive integer'}), 400 return jsonify({'error': 'Points must be a positive integer'}), 400
updates['points'] = points task.points = points
is_dirty = True
if 'is_good' in data: if 'is_good' in data:
is_good = data.get('is_good') is_good = data.get('is_good')
if not isinstance(is_good, bool): if not isinstance(is_good, bool):
return jsonify({'error': 'is_good must be a boolean'}), 400 return jsonify({'error': 'is_good must be a boolean'}), 400
updates['is_good'] = is_good task.is_good = is_good
is_dirty = True
if 'image_id' in data: if 'image_id' in data:
updates['image_id'] = data.get('image_id', '') task.image_id = data.get('image_id', '')
is_dirty = True
if not updates: if not is_dirty:
return jsonify({'error': 'No valid fields to update'}), 400 return jsonify({'error': 'No valid fields to update'}), 400
task_db.update(updates, TaskQuery.id == id) if task.user_id is None: # public task
updated = task_db.get(TaskQuery.id == id) new_task = Task(name=task.name, points=task.points, is_good=task.is_good, image_id=task.image_id, user_id=user_id)
task_db.insert(new_task.to_dict())
send_event_for_current_user(Event(EventType.TASK_MODIFIED.value,
TaskModified(new_task.id, TaskModified.OPERATION_ADD)))
return jsonify(new_task.to_dict()), 200
task_db.update(task.to_dict(), (TaskQuery.id == id) & ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None)))
send_event_for_current_user(Event(EventType.TASK_MODIFIED.value, send_event_for_current_user(Event(EventType.TASK_MODIFIED.value,
TaskModified(id, TaskModified.OPERATION_EDIT))) TaskModified(id, TaskModified.OPERATION_EDIT)))
return jsonify(updated), 200 return jsonify(task.to_dict()), 200

View File

@@ -8,6 +8,7 @@ import string
import smtplib import smtplib
from backend.utils.email_instance import email_sender from backend.utils.email_instance import email_sender
from datetime import datetime, timedelta from datetime import datetime, timedelta
from api.utils import get_validated_user_id
user_api = Blueprint('user_api', __name__) user_api = Blueprint('user_api', __name__)
UserQuery = Query() UserQuery = Query()
@@ -18,14 +19,17 @@ def get_current_user():
return None return None
try: try:
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256']) payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
email = payload.get('email') user_id = payload.get('user_id')
user_dict = users_db.get(UserQuery.email == email) user_dict = users_db.get(UserQuery.id == user_id)
return User.from_dict(user_dict) if user_dict else None return User.from_dict(user_dict) if user_dict else None
except Exception: except Exception:
return None return None
@user_api.route('/user/profile', methods=['GET']) @user_api.route('/user/profile', methods=['GET'])
def get_profile(): def get_profile():
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
user = get_current_user() user = get_current_user()
if not user: if not user:
return jsonify({'error': 'Unauthorized'}), 401 return jsonify({'error': 'Unauthorized'}), 401
@@ -38,6 +42,9 @@ def get_profile():
@user_api.route('/user/profile', methods=['PUT']) @user_api.route('/user/profile', methods=['PUT'])
def update_profile(): def update_profile():
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
user = get_current_user() user = get_current_user()
if not user: if not user:
return jsonify({'error': 'Unauthorized'}), 401 return jsonify({'error': 'Unauthorized'}), 401
@@ -57,6 +64,9 @@ def update_profile():
@user_api.route('/user/image', methods=['PUT']) @user_api.route('/user/image', methods=['PUT'])
def update_image(): def update_image():
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
user = get_current_user() user = get_current_user()
if not user: if not user:
return jsonify({'error': 'Unauthorized'}), 401 return jsonify({'error': 'Unauthorized'}), 401
@@ -70,6 +80,9 @@ def update_image():
@user_api.route('/user/check-pin', methods=['POST']) @user_api.route('/user/check-pin', methods=['POST'])
def check_pin(): def check_pin():
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
user = get_current_user() user = get_current_user()
if not user: if not user:
return jsonify({'error': 'Unauthorized'}), 401 return jsonify({'error': 'Unauthorized'}), 401
@@ -83,6 +96,9 @@ def check_pin():
@user_api.route('/user/has-pin', methods=['GET']) @user_api.route('/user/has-pin', methods=['GET'])
def has_pin(): def has_pin():
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
user = get_current_user() user = get_current_user()
if not user: if not user:
return jsonify({'error': 'Unauthorized'}), 401 return jsonify({'error': 'Unauthorized'}), 401
@@ -90,6 +106,9 @@ def has_pin():
@user_api.route('/user/request-pin-setup', methods=['POST']) @user_api.route('/user/request-pin-setup', methods=['POST'])
def request_pin_setup(): def request_pin_setup():
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
user = get_current_user() user = get_current_user()
if not user or not user.verified: if not user or not user.verified:
return jsonify({'error': 'Unauthorized'}), 401 return jsonify({'error': 'Unauthorized'}), 401
@@ -108,6 +127,9 @@ def send_pin_setup_email(email, code):
@user_api.route('/user/verify-pin-setup', methods=['POST']) @user_api.route('/user/verify-pin-setup', methods=['POST'])
def verify_pin_setup(): def verify_pin_setup():
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
user = get_current_user() user = get_current_user()
if not user or not user.verified: if not user or not user.verified:
return jsonify({'error': 'Unauthorized'}), 401 return jsonify({'error': 'Unauthorized'}), 401
@@ -127,6 +149,9 @@ def verify_pin_setup():
@user_api.route('/user/set-pin', methods=['POST']) @user_api.route('/user/set-pin', methods=['POST'])
def set_pin(): def set_pin():
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
user = get_current_user() user = get_current_user()
if not user or not user.verified: if not user or not user.verified:
return jsonify({'error': 'Unauthorized'}), 401 return jsonify({'error': 'Unauthorized'}), 401

View File

@@ -1,5 +1,7 @@
import jwt import jwt
import re import re
from db.db import users_db
from tinydb import Query
from flask import request, current_app, jsonify from flask import request, current_app, jsonify
from events.sse import send_event_to_user from events.sse import send_event_to_user
@@ -24,13 +26,19 @@ def get_current_user_id():
return None return None
try: try:
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256']) payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
email = payload.get('email') user_id = payload.get('user_id')
if not email: if not user_id:
return None return None
return sanitize_email(email) return user_id
except jwt.InvalidTokenError: except jwt.InvalidTokenError:
return None return None
def get_validated_user_id():
user_id = get_current_user_id()
if not user_id or not users_db.get(Query().id == user_id):
return None
return user_id
def send_event_for_current_user(event): def send_event_for_current_user(event):
user_id = get_current_user_id() user_id = get_current_user_id()
if not user_id: if not user_id:

View File

@@ -9,6 +9,7 @@ class Child(BaseModel):
rewards: list[str] = field(default_factory=list) rewards: list[str] = field(default_factory=list)
points: int = 0 points: int = 0
image_id: str | None = None image_id: str | None = None
user_id: str | None = None
@classmethod @classmethod
def from_dict(cls, d: dict): def from_dict(cls, d: dict):
@@ -19,10 +20,10 @@ class Child(BaseModel):
rewards=d.get('rewards', []), rewards=d.get('rewards', []),
points=d.get('points', 0), points=d.get('points', 0),
image_id=d.get('image_id'), image_id=d.get('image_id'),
user_id=d.get('user_id'),
id=d.get('id'), id=d.get('id'),
created_at=d.get('created_at'), created_at=d.get('created_at'),
updated_at=d.get('updated_at') updated_at=d.get('updated_at')
) )
def to_dict(self): def to_dict(self):
@@ -33,6 +34,7 @@ class Child(BaseModel):
'tasks': self.tasks, 'tasks': self.tasks,
'rewards': self.rewards, 'rewards': self.rewards,
'points': self.points, 'points': self.points,
'image_id': self.image_id 'image_id': self.image_id,
'user_id': self.user_id
}) })
return base return base

View File

@@ -2,12 +2,14 @@
from dataclasses import dataclass from dataclasses import dataclass
from models.base import BaseModel from models.base import BaseModel
@dataclass @dataclass
class Image(BaseModel): class Image(BaseModel):
type: int type: int
extension: str extension: str
permanent: bool = False permanent: bool = False
user: str | None = None user_id: str | None = None
@classmethod @classmethod
def from_dict(cls, d: dict): def from_dict(cls, d: dict):
@@ -19,7 +21,7 @@ class Image(BaseModel):
id=d.get('id'), id=d.get('id'),
created_at=d.get('created_at'), created_at=d.get('created_at'),
updated_at=d.get('updated_at'), updated_at=d.get('updated_at'),
user=d.get('user') user_id=d.get('user_id') if 'user_id' in d else d.get('user')
) )
def to_dict(self): def to_dict(self):
@@ -28,6 +30,6 @@ class Image(BaseModel):
'type': self.type, 'type': self.type,
'permanent': self.permanent, 'permanent': self.permanent,
'extension': self.extension, 'extension': self.extension,
'user': self.user 'user_id': self.user_id
}) })
return base return base

View File

@@ -5,6 +5,7 @@ from models.base import BaseModel
class PendingReward(BaseModel): class PendingReward(BaseModel):
child_id: str child_id: str
reward_id: str reward_id: str
user_id: str
status: str = "pending" # pending, approved, rejected status: str = "pending" # pending, approved, rejected
@classmethod @classmethod
@@ -13,6 +14,7 @@ class PendingReward(BaseModel):
child_id=d.get('child_id'), child_id=d.get('child_id'),
reward_id=d.get('reward_id'), reward_id=d.get('reward_id'),
status=d.get('status', 'pending'), status=d.get('status', 'pending'),
user_id=d.get('user_id'),
id=d.get('id'), id=d.get('id'),
created_at=d.get('created_at'), created_at=d.get('created_at'),
updated_at=d.get('updated_at') updated_at=d.get('updated_at')
@@ -23,6 +25,7 @@ class PendingReward(BaseModel):
base.update({ base.update({
'child_id': self.child_id, 'child_id': self.child_id,
'reward_id': self.reward_id, 'reward_id': self.reward_id,
'status': self.status 'status': self.status,
'user_id': self.user_id
}) })
return base return base

View File

@@ -8,6 +8,7 @@ class Reward(BaseModel):
description: str description: str
cost: int cost: int
image_id: str | None = None image_id: str | None = None
user_id: str | None = None
@classmethod @classmethod
def from_dict(cls, d: dict): def from_dict(cls, d: dict):
@@ -17,6 +18,7 @@ class Reward(BaseModel):
description=d.get('description'), description=d.get('description'),
cost=d.get('cost', 0), cost=d.get('cost', 0),
image_id=d.get('image_id'), image_id=d.get('image_id'),
user_id=d.get('user_id'),
id=d.get('id'), id=d.get('id'),
created_at=d.get('created_at'), created_at=d.get('created_at'),
updated_at=d.get('updated_at') updated_at=d.get('updated_at')
@@ -28,6 +30,7 @@ class Reward(BaseModel):
'name': self.name, 'name': self.name,
'description': self.description, 'description': self.description,
'cost': self.cost, 'cost': self.cost,
'image_id': self.image_id 'image_id': self.image_id,
'user_id': self.user_id
}) })
return base return base

View File

@@ -7,6 +7,7 @@ class Task(BaseModel):
points: int points: int
is_good: bool is_good: bool
image_id: str | None = None image_id: str | None = None
user_id: str | None = None
@classmethod @classmethod
def from_dict(cls, d: dict): def from_dict(cls, d: dict):
@@ -15,6 +16,7 @@ class Task(BaseModel):
points=d.get('points', 0), points=d.get('points', 0),
is_good=d.get('is_good', True), is_good=d.get('is_good', True),
image_id=d.get('image_id'), image_id=d.get('image_id'),
user_id=d.get('user_id'),
id=d.get('id'), id=d.get('id'),
created_at=d.get('created_at'), created_at=d.get('created_at'),
updated_at=d.get('updated_at') updated_at=d.get('updated_at')
@@ -26,6 +28,7 @@ class Task(BaseModel):
'name': self.name, 'name': self.name,
'points': self.points, 'points': self.points,
'is_good': self.is_good, 'is_good': self.is_good,
'image_id': self.image_id 'image_id': self.image_id,
'user_id': self.user_id
}) })
return base return base

View File

@@ -48,6 +48,7 @@ function handleRewardTriggered(event: Event) {
const payload = event.payload as ChildRewardTriggeredEventPayload const payload = event.payload as ChildRewardTriggeredEventPayload
if (child.value && payload.child_id == child.value.id) { if (child.value && payload.child_id == child.value.id) {
child.value.points = payload.points child.value.points = payload.points
childRewardListRef.value?.refresh()
} }
} }

View File

@@ -113,6 +113,7 @@ function onAddImage({ id, file }: { id: string; file: File }) {
async function updateAvatar(imageId: string) { async function updateAvatar(imageId: string) {
errorMsg.value = '' errorMsg.value = ''
successMsg.value = '' successMsg.value = ''
//todo update avatar loading state
try { try {
const res = await fetch('/api/user/avatar', { const res = await fetch('/api/user/avatar', {
method: 'PUT', method: 'PUT',
@@ -123,7 +124,9 @@ async function updateAvatar(imageId: string) {
initialData.value.image_id = imageId initialData.value.image_id = imageId
successMsg.value = 'Avatar updated!' successMsg.value = 'Avatar updated!'
} catch { } catch {
errorMsg.value = 'Failed to update avatar.' //errorMsg.value = 'Failed to update avatar.'
//todo update avatar error handling
errorMsg.value = ''
} }
} }

View File

@@ -2,7 +2,12 @@
import { ref, nextTick, onMounted, onUnmounted } from 'vue' import { ref, nextTick, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { eventBus } from '@/common/eventBus' import { eventBus } from '@/common/eventBus'
import { authenticateParent, isParentAuthenticated, logoutParent } from '../../stores/auth' import {
authenticateParent,
isParentAuthenticated,
logoutParent,
logoutUser,
} from '../../stores/auth'
import '@/assets/styles.css' import '@/assets/styles.css'
import '@/assets/colors.css' import '@/assets/colors.css'
import ModalDialog from './ModalDialog.vue' import ModalDialog from './ModalDialog.vue'
@@ -91,7 +96,7 @@ function closeDropdown() {
async function signOut() { async function signOut() {
try { try {
await fetch('/api/logout', { method: 'POST' }) await fetch('/api/logout', { method: 'POST' })
logoutParent() logoutUser()
router.push('/auth') router.push('/auth')
} catch { } catch {
// Optionally show error // Optionally show error

View File

@@ -24,6 +24,8 @@ export function loginUser() {
export function logoutUser() { export function logoutUser() {
isUserLoggedIn.value = false isUserLoggedIn.value = false
currentUserId.value = ''
logoutParent()
} }
export async function checkAuth() { export async function checkAuth() {