Files
chore/backend/api/tracking_api.py
Ryan Kegel 3dee8b80a2
All checks were successful
Gitea Actions Demo / build-and-push (push) Successful in 24s
feat: Implement task and reward tracking feature
- Added tracking events for tasks, penalties, and rewards with timestamps.
- Created new TinyDB table for tracking records to maintain audit history.
- Developed backend API for querying tracking events with filters and pagination.
- Implemented logging for tracking events with per-user rotating log files.
- Added unit tests for tracking event creation, querying, and anonymization.
- Deferred frontend changes for future implementation.
- Established acceptance criteria and documentation for the tracking feature.

feat: Introduce account deletion scheduler

- Implemented a scheduler to delete accounts marked for deletion after a configurable threshold.
- Added new fields to the User model to manage deletion status and attempts.
- Created admin API endpoints for managing deletion thresholds and viewing the deletion queue.
- Integrated error handling and logging for the deletion process.
- Developed unit tests for the deletion scheduler and related API endpoints.
- Documented the deletion process and acceptance criteria.
2026-02-09 15:39:43 -05:00

123 lines
4.1 KiB
Python

from flask import Blueprint, request, jsonify
from api.utils import get_validated_user_id
from db.tracking import get_tracking_events_by_child, get_tracking_events_by_user
from models.tracking_event import TrackingEvent
from functools import wraps
import jwt
from tinydb import Query
from db.db import users_db
from models.user import User
tracking_api = Blueprint('tracking_api', __name__)
def admin_required(f):
"""
Decorator to require admin role for endpoints.
"""
@wraps(f)
def decorated_function(*args, **kwargs):
# Get JWT token from cookie
token = request.cookies.get('token')
if not token:
return jsonify({'error': 'Authentication required', 'code': 'AUTH_REQUIRED'}), 401
try:
# Verify JWT token
payload = jwt.decode(token, 'supersecretkey', algorithms=['HS256'])
user_id = payload.get('user_id')
if not user_id:
return jsonify({'error': 'Invalid token', 'code': 'INVALID_TOKEN'}), 401
# Get user from database
Query_ = Query()
user_dict = users_db.get(Query_.id == user_id)
if not user_dict:
return jsonify({'error': 'User not found', 'code': 'USER_NOT_FOUND'}), 404
user = User.from_dict(user_dict)
# Check if user has admin role
if user.role != 'admin':
return jsonify({'error': 'Admin access required', 'code': 'ADMIN_REQUIRED'}), 403
# Store user_id in request context
request.admin_user_id = user_id
return f(*args, **kwargs)
except jwt.ExpiredSignatureError:
return jsonify({'error': 'Token expired', 'code': 'TOKEN_EXPIRED'}), 401
except jwt.InvalidTokenError:
return jsonify({'error': 'Invalid token', 'code': 'INVALID_TOKEN'}), 401
return decorated_function
@tracking_api.route('/admin/tracking', methods=['GET'])
@admin_required
def get_tracking():
"""
Admin endpoint to query tracking events with filters and pagination.
Query params:
- child_id: Filter by child ID (optional)
- user_id: Filter by user ID (optional, admin only)
- entity_type: Filter by entity type (task/reward/penalty) (optional)
- action: Filter by action type (activated/requested/redeemed/cancelled) (optional)
- limit: Max results (default 50, max 500)
- offset: Pagination offset (default 0)
"""
child_id = request.args.get('child_id')
filter_user_id = request.args.get('user_id')
entity_type = request.args.get('entity_type')
action = request.args.get('action')
limit = int(request.args.get('limit', 50))
offset = int(request.args.get('offset', 0))
# Validate limit
limit = min(max(limit, 1), 500)
offset = max(offset, 0)
# Validate filters
if entity_type and entity_type not in ['task', 'reward', 'penalty']:
return jsonify({'error': 'Invalid entity_type', 'code': 'INVALID_ENTITY_TYPE'}), 400
if action and action not in ['activated', 'requested', 'redeemed', 'cancelled']:
return jsonify({'error': 'Invalid action', 'code': 'INVALID_ACTION'}), 400
# Query tracking events
if child_id:
events, total = get_tracking_events_by_child(
child_id=child_id,
limit=limit,
offset=offset,
entity_type=entity_type,
action=action
)
elif filter_user_id:
events, total = get_tracking_events_by_user(
user_id=filter_user_id,
limit=limit,
offset=offset,
entity_type=entity_type
)
else:
return jsonify({
'error': 'Either child_id or user_id is required',
'code': 'MISSING_FILTER'
}), 400
# Convert to dict
events_data = [event.to_dict() for event in events]
return jsonify({
'tracking_events': events_data,
'total': total,
'limit': limit,
'offset': offset,
'count': len(events_data)
}), 200