- Fix missing await on FocusGroup.get_messages() (N-L1) - Replace time.sleep with asyncio.sleep in key_theme_service and focus_group_service (N-P10) - Replace flask import with quart in focus_groups.py (N-S3) - Add logger.error before all 500 returns in focus_groups.py (N-P6) - Add logging to silent except blocks across routes (N-M10, N-M11) - Add @rate_limit to 6 remaining AI endpoints (N-H4) - Add --confirm flag to populate scripts before delete_many (S-H2) - Remove hardcoded Azure ID fallbacks from msal_service.py and msalConfig.ts (A-M2, F-H4) - Centralize make_serializable() in utils.py, remove duplicates from 3 route files (N-P7) - Replace all datetime.utcnow() with datetime.now(timezone.utc) across entire backend (M-L2) - AuthContext.tsx: only mark token validated on 200 success, not on non-401 errors (F-H2) - Rename authType → auth_type in auth.py (N-S4) - Add security_report.md and security_report.pdf with full 92-finding status Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
267 lines
No EOL
10 KiB
Python
Executable file
267 lines
No EOL
10 KiB
Python
Executable file
from quart import Blueprint, request, jsonify
|
|
from app.auth.quart_jwt import jwt_required, get_jwt_identity
|
|
from app.models.folder import Folder
|
|
from bson import ObjectId
|
|
import datetime
|
|
|
|
from app.utils import make_serializable
|
|
|
|
folders_bp = Blueprint('folders', __name__)
|
|
|
|
@folders_bp.route('', methods=['GET'])
|
|
@folders_bp.route('/', methods=['GET'])
|
|
@jwt_required()
|
|
async def get_folders():
|
|
"""Get all folders in hierarchical tree structure - shared across all users."""
|
|
try:
|
|
# Return folders in hierarchical tree structure
|
|
folders = await Folder.get_folder_tree()
|
|
|
|
# Make folders serializable
|
|
serializable_folders = make_serializable(folders)
|
|
return jsonify(serializable_folders), 200
|
|
except Exception as e:
|
|
print(f"Error in get_folders: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@folders_bp.route('/<folder_id>', methods=['GET'])
|
|
@jwt_required()
|
|
async def get_folder(folder_id):
|
|
"""Get a specific folder by ID."""
|
|
try:
|
|
folder = await Folder.find_by_id(folder_id)
|
|
if not folder:
|
|
return jsonify({"message": "Folder not found"}), 404
|
|
|
|
# Make folder serializable
|
|
serializable_folder = make_serializable(folder)
|
|
return jsonify(serializable_folder), 200
|
|
except Exception as e:
|
|
print(f"Error in get_folder: {e}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@folders_bp.route('', methods=['POST'])
|
|
@folders_bp.route('/', methods=['POST'])
|
|
@jwt_required()
|
|
async def create_folder():
|
|
"""Create a new folder."""
|
|
user_id = get_jwt_identity()
|
|
data = await request.get_json()
|
|
|
|
if not data:
|
|
return jsonify({"message": "No data provided"}), 400
|
|
|
|
if not data.get('name'):
|
|
return jsonify({"message": "Folder name is required"}), 400
|
|
|
|
folder_id = await Folder.create(data, user_id)
|
|
|
|
return jsonify({
|
|
"message": "Folder created successfully",
|
|
"folder_id": folder_id
|
|
}), 201
|
|
|
|
@folders_bp.route('/<folder_id>', methods=['PUT'])
|
|
@jwt_required()
|
|
async def update_folder(folder_id):
|
|
"""Update a folder."""
|
|
try:
|
|
data = await request.get_json()
|
|
|
|
if not data:
|
|
return jsonify({"message": "No data provided"}), 400
|
|
|
|
folder = await Folder.find_by_id(folder_id)
|
|
if not folder:
|
|
return jsonify({"message": "Folder not found"}), 404
|
|
|
|
# Folder operations are shared across all users in this system
|
|
|
|
# Ensure _id is not being modified
|
|
if '_id' in data:
|
|
del data['_id']
|
|
|
|
# Ensure id is not being used for update
|
|
if 'id' in data:
|
|
del data['id']
|
|
|
|
success = await Folder.update(folder_id, data)
|
|
|
|
if success:
|
|
# Get the updated folder and return it
|
|
updated_folder = await Folder.find_by_id(folder_id)
|
|
return jsonify({
|
|
"message": "Folder updated successfully",
|
|
"folder": make_serializable(updated_folder)
|
|
}), 200
|
|
else:
|
|
return jsonify({"message": "No changes made to folder"}), 200
|
|
except Exception as e:
|
|
print(f"Error updating folder: {e}")
|
|
return jsonify({"message": f"Failed to update folder: {str(e)}"}), 500
|
|
|
|
@folders_bp.route('/<folder_id>', methods=['DELETE'])
|
|
@jwt_required()
|
|
async def delete_folder(folder_id):
|
|
"""Delete a folder and its entire hierarchy."""
|
|
user_id = get_jwt_identity()
|
|
|
|
# Folder operations are shared across all users in this system
|
|
|
|
try:
|
|
success, message = await Folder.delete_hierarchy(folder_id, user_id)
|
|
|
|
if success:
|
|
return jsonify({"message": message}), 200
|
|
else:
|
|
return jsonify({"message": message}), 400
|
|
except Exception as e:
|
|
print(f"Error deleting folder hierarchy: {e}")
|
|
return jsonify({"message": f"Failed to delete folder: {str(e)}"}), 500
|
|
|
|
@folders_bp.route('/<folder_id>/personas', methods=['POST'])
|
|
@jwt_required()
|
|
async def add_persona_to_folder(folder_id):
|
|
"""Add a persona to a folder (supports multiple folders per persona)."""
|
|
try:
|
|
data = await request.get_json()
|
|
|
|
if not data or not data.get('persona_id'):
|
|
return jsonify({"message": "Persona ID is required"}), 400
|
|
|
|
folder = await Folder.find_by_id(folder_id)
|
|
if not folder:
|
|
return jsonify({"message": "Folder not found"}), 404
|
|
|
|
# Folder operations are shared across all users in this system
|
|
|
|
persona_id = data['persona_id']
|
|
success = await Folder.add_persona(folder_id, persona_id)
|
|
|
|
if success:
|
|
return jsonify({"message": "Persona added to folder successfully"}), 200
|
|
else:
|
|
return jsonify({"message": "Persona was already in folder or update failed"}), 200
|
|
except Exception as e:
|
|
print(f"Error adding persona to folder: {e}")
|
|
return jsonify({"message": f"Failed to add persona to folder: {str(e)}"}), 500
|
|
|
|
@folders_bp.route('/<folder_id>/personas/<persona_id>', methods=['DELETE'])
|
|
@jwt_required()
|
|
async def remove_persona_from_folder(folder_id, persona_id):
|
|
"""Remove a persona from a folder (persona can remain in other folders)."""
|
|
try:
|
|
folder = await Folder.find_by_id(folder_id)
|
|
if not folder:
|
|
return jsonify({"message": "Folder not found"}), 404
|
|
|
|
# Folder operations are shared across all users in this system
|
|
|
|
success = await Folder.remove_persona(folder_id, persona_id)
|
|
|
|
if success:
|
|
return jsonify({"message": "Persona removed from folder successfully"}), 200
|
|
else:
|
|
return jsonify({"message": "Persona was not in folder or removal failed"}), 200
|
|
except Exception as e:
|
|
print(f"Error removing persona from folder: {e}")
|
|
return jsonify({"message": f"Failed to remove persona from folder: {str(e)}"}), 500
|
|
|
|
@folders_bp.route('/<folder_id>/personas/batch', methods=['POST'])
|
|
@jwt_required()
|
|
async def add_personas_to_folder_batch(folder_id):
|
|
"""Add multiple personas to a folder (personas can be in multiple folders)."""
|
|
try:
|
|
data = await request.get_json()
|
|
|
|
if not data or not data.get('persona_ids'):
|
|
return jsonify({"message": "Persona IDs are required"}), 400
|
|
|
|
folder = await Folder.find_by_id(folder_id)
|
|
if not folder:
|
|
return jsonify({"message": "Folder not found"}), 404
|
|
|
|
# Folder operations are shared across all users in this system
|
|
|
|
persona_ids = data['persona_ids']
|
|
if not isinstance(persona_ids, list):
|
|
return jsonify({"message": "persona_ids must be a list"}), 400
|
|
|
|
success = await Folder.add_personas_batch(folder_id, persona_ids)
|
|
|
|
if success:
|
|
return jsonify({"message": f"Successfully added {len(persona_ids)} personas to folder"}), 200
|
|
else:
|
|
return jsonify({"message": "Update failed or no changes made"}), 200
|
|
except Exception as e:
|
|
print(f"Error adding personas to folder: {e}")
|
|
return jsonify({"message": f"Failed to add personas to folder: {str(e)}"}), 500
|
|
|
|
@folders_bp.route('/<folder_id>/personas/remove-batch', methods=['POST'])
|
|
@jwt_required()
|
|
async def remove_personas_from_folder_batch(folder_id):
|
|
"""Remove multiple personas from a folder (personas remain in other folders)."""
|
|
print(f"🌐 BACKEND: POST /folders/{folder_id}/personas/remove-batch endpoint hit")
|
|
try:
|
|
data = await request.get_json()
|
|
print(f"🌐 BACKEND: Raw request data: {data}")
|
|
print(f"🌐 BACKEND: Request content type: {request.content_type}")
|
|
print(f"🌐 BACKEND: Request method: {request.method}")
|
|
|
|
if not data or not data.get('persona_ids'):
|
|
print(f"❌ BACKEND: Missing persona_ids in data: {data}")
|
|
return jsonify({"message": "Persona IDs are required"}), 400
|
|
|
|
folder = await Folder.find_by_id(folder_id)
|
|
if not folder:
|
|
return jsonify({"message": "Folder not found"}), 404
|
|
|
|
# Folder operations are shared across all users in this system
|
|
|
|
persona_ids = data['persona_ids']
|
|
if not isinstance(persona_ids, list):
|
|
return jsonify({"message": "persona_ids must be a list"}), 400
|
|
|
|
success = await Folder.remove_personas_batch(folder_id, persona_ids)
|
|
|
|
if success:
|
|
return jsonify({"message": f"Successfully removed {len(persona_ids)} personas from folder"}), 200
|
|
else:
|
|
return jsonify({"message": "Update failed or no changes made"}), 200
|
|
except Exception as e:
|
|
print(f"Error removing personas from folder: {e}")
|
|
return jsonify({"message": f"Failed to remove personas from folder: {str(e)}"}), 500
|
|
|
|
@folders_bp.route('/<folder_id>/move', methods=['PUT'])
|
|
@jwt_required()
|
|
async def move_folder(folder_id):
|
|
"""Move a folder to a new parent."""
|
|
try:
|
|
data = await request.get_json()
|
|
user_id = get_jwt_identity()
|
|
|
|
new_parent_id = data.get('parent_folder_id') # None for root level
|
|
|
|
# Folder operations are shared across all users in this system
|
|
|
|
success, message = await Folder.move_folder(folder_id, new_parent_id, user_id)
|
|
|
|
if success:
|
|
return jsonify({"message": message}), 200
|
|
else:
|
|
return jsonify({"message": message}), 400
|
|
except Exception as e:
|
|
print(f"Error moving folder: {e}")
|
|
return jsonify({"message": f"Failed to move folder: {str(e)}"}), 500
|
|
|
|
@folders_bp.route('/<folder_id>/descendants', methods=['GET'])
|
|
@jwt_required()
|
|
async def get_folder_descendants(folder_id):
|
|
"""Get all descendant folders of a given folder."""
|
|
try:
|
|
descendants = await Folder.get_descendants(folder_id)
|
|
serializable_descendants = make_serializable(descendants)
|
|
return jsonify(serializable_descendants), 200
|
|
except Exception as e:
|
|
print(f"Error getting folder descendants: {e}")
|
|
return jsonify({"error": str(e)}), 500 |