# Utils package for Synthetic Society backend from functools import wraps from datetime import datetime from bson import ObjectId from quart import jsonify from app.auth.quart_jwt import get_jwt_identity from app.models.user import User def make_serializable(obj): """Recursively convert MongoDB documents to JSON-serializable types.""" if isinstance(obj, dict): return {k: make_serializable(v) for k, v in obj.items()} elif isinstance(obj, list): return [make_serializable(item) for item in obj] elif isinstance(obj, ObjectId): return str(obj) elif isinstance(obj, datetime): return obj.isoformat() else: return obj def admin_required(f): """Route decorator requiring admin role. Must be stacked BELOW @jwt_required().""" @wraps(f) async def decorated(*args, **kwargs): user_id = get_jwt_identity() if not user_id: return jsonify({"message": "Authentication required"}), 401 user_data = await User.find_by_id(user_id) if not user_data: return jsonify({"message": "User not found"}), 404 if user_data.get("role") != "admin": return jsonify({"message": "Admin privileges required"}), 403 if user_data.get("is_active") is False: return jsonify({"message": "Account disabled"}), 403 return await f(*args, **kwargs) return decorated def active_required(f): """Route decorator that rejects requests from disabled users. Guards LLM-invoking routes so that revoking a user's access takes effect immediately rather than waiting for their JWT to expire (24 h window). Must be stacked BELOW @jwt_required(). """ @wraps(f) async def decorated(*args, **kwargs): user_id = get_jwt_identity() if user_id: user_data = await User.find_by_id(user_id) if user_data and user_data.get("is_active") is False: return jsonify({"message": "Account disabled"}), 403 return await f(*args, **kwargs) return decorated def with_user_context(f): """Route decorator that injects the JWT user_id into the LLM usage ContextVar. Must be stacked BELOW @jwt_required() so the token is already validated. The context propagates to asyncio tasks and run_coroutine_threadsafe calls, so autonomous AI runner conversations pick up the user attribution automatically. """ @wraps(f) async def decorated(*args, **kwargs): from app.services.llm_usage_context import llm_context user_id = get_jwt_identity() if user_id: with llm_context(user_id=user_id): return await f(*args, **kwargs) return await f(*args, **kwargs) return decorated