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
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:
10
.github/copilot-instructions.md
vendored
10
.github/copilot-instructions.md
vendored
@@ -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
|
||||||
|
|
||||||
|
|||||||
26
.github/specs/active/no-delete-system-tasks-and-rewards.md
vendored
Normal file
26
.github/specs/active/no-delete-system-tasks-and-rewards.md
vendored
Normal 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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user