semblance-dev/backend/app/routes/folders.py
Vadym Samoilenko 3e1865edbd Apply Jintech security audit remediation (sprint 3) — 87/92 findings fixed
- 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>
2026-03-20 12:51:18 +00:00

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