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