Source code for mdvtools.dbutils.routes

import io
from mdvtools.logging_config import get_logger
[docs] logger = get_logger(__name__)
[docs] def register_routes(app, ENABLE_AUTH): from flask import abort, request, jsonify, session, redirect, url_for, render_template, send_file from mdvtools.auth.authutils import update_cache, active_projects_cache, user_project_cache,user_cache, all_users_cache from mdvtools.dbutils.mdv_server_app import serve_projects_from_filesystem, is_valid_mdv_project import os import shutil from mdvtools.mdvproject import MDVProject from mdvtools.project_router import ProjectBlueprint from mdvtools.dbutils.dbmodels import User from mdvtools.dbutils.dbservice import ProjectService, UserProjectService import tempfile import zipfile def get_project_thumbnail(project_path): """Extract the first available viewImage from a project's views.""" try: mdv_project = MDVProject(project_path) return next((v["viewImage"] for v in mdv_project.views.values() if "viewImage" in v), None) except Exception as e: logger.exception(f"Error extracting thumbnail for project at {project_path}: {e}") return None REQUIRED_FILES = {"views.json", "state.json", "datasources.json"} def find_root_prefix(names): # Check if all required files are in the root of archive if REQUIRED_FILES.issubset(set(os.path.basename(n) for n in names if "/" not in n)): return "" # Check one level below if required files exist dirs = {n.split("/", 1)[0] for n in names if "/" in n} for d in dirs: files_in_d = {os.path.basename(n) for n in names if n.startswith(f"{d}/")} if REQUIRED_FILES.issubset(files_in_d): return d + "/" return None """Register routes with the Flask app.""" logger.info("Registering routes...") # if len(active_projects_cache) == 0: # logger.info("active_projects_cache is empty, calling cache_user_projects()") # with app.app_context(): # cache_user_projects() # this won't work in localhost # logger.info(f"active_projects_cache populated with {len(active_projects_cache)} projects") try: @app.route('/') def index(): try: return render_template('index.html') except Exception as e: logger.exception(f"Error rendering index: {e}") return jsonify({"error": str(e)}), 500 logger.info("Route registered: /") @app.route('/login_dev') def login_dev(): return render_template('login.html') logger.info("Route registered: /login_dev") @app.route('/rescan_projects') def rescan_projects(): if ENABLE_AUTH: user = session.get('user') if not user: # If user is not found in session, raise an error or redirect. raise ValueError("User not found in session.") is_admin = user.get("is_admin", False) if not is_admin: # If user isn't admin, abort with 403 (Forbidden). abort(403) # Forbidden try: # Serve the projects after checking authentication and admin privileges serve_projects_from_filesystem(app, app.config["projects_base_dir"]) except Exception as e: # Handle potential errors while serving the projects logger.exception(f"Error while serving the projects: {e}") abort(500, description="Error while serving the projects.") # If everything goes well, redirect to the 'index' page return redirect(url_for('index')) def get_project_owners(project_id): owners = [] for user_id, projects in user_project_cache.items(): perms = projects.get(project_id) if perms and perms.get("is_owner"): user_data = next((u for u in all_users_cache if u["id"] == user_id), None) if user_data: owners.append(user_data["email"]) return sorted(owners) @app.route('/projects') def get_projects(): """ Fetches the list of active projects from cache or database depending on the authentication. """ logger.info(" /projects queried...") try: # Step 1: Get projects directly from cache if ENABLE_AUTH: active_projects = active_projects_cache else: active_projects = ProjectService.get_active_projects() # logger.info(f"Active projects from cache: {active_projects}") if ENABLE_AUTH: user = session.get('user') if not user: raise ValueError("User not found in session.") # Filter by user permissions if authentication is enabled user_id = user["id"] user_projects = user_project_cache.get(user_id) # If user has no projects assigned, return an empty list instead of error if not user_projects: return jsonify([]) allowed_project_ids = { pid for pid, perms in user_projects.items() if perms["can_read"] or perms["can_write"] or perms["is_owner"] } filtered_projects = [ { "id": p["id"], "name": p["name"], "lastModified": p["lastModified"], "thumbnail": p["thumbnail"], "permissions": user_projects[p["id"]], "owner": get_project_owners(p["id"]), } for p in active_projects if p["id"] in allowed_project_ids ] else: # No auth, return all filtered_projects = [ { "id": p["id"], "name": p["name"], "lastModified": p["lastModified"], "thumbnail": p["thumbnail"], "permissions": None, "owner": [], } for p in active_projects ] return jsonify(filtered_projects) except Exception as e: logger.exception(f"Error retrieving projects: {e}") return jsonify({"error": str(e)}), 500 logger.info("Route registered: /projects") @app.route("/create_project", methods=["POST"]) def create_project(): """ Creates a new project and updates the caches and database accordingly. """ project_path = None next_id = None try: logger.info("Creating project") # Get the next available ID next_id = ProjectService.get_next_project_id() if next_id is None: logger.error("In register_routes: Error- Failed to determine next project ID from db") return jsonify({"error": "Failed to determine next project ID from db"}), 500 # Create the project directory path project_path = os.path.join(app.config['projects_base_dir'], str(next_id)) # Create and serve the MDVProject try: logger.info("Creating and serving the new project") p = MDVProject(project_path, backend_db= True) p.set_editable(True) p.serve(app=app, open_browser=False, backend_db=True) except Exception as e: logger.exception(f"In register_routes: Error serving MDVProject: {e}") return jsonify({"error": "Failed to serve MDVProject"}), 500 # Create a new Project record in the database with the path logger.info("Adding new project to the database") new_project = ProjectService.add_new_project(path=project_path) if new_project: # Step 5: Associate the admin user with the new project and grant all permissions if ENABLE_AUTH: user_data = session.get('user') if not user_data: raise ValueError("User not found in session.") current_user_id = user_data['id'] UserProjectService.add_or_update_user_project( user_id=current_user_id, project_id=new_project.id, is_owner=True ) auth_id = user_data["auth_id"] owner_email = user_cache.get(auth_id, {}).get("email", "unknown") # Generate thumbnail thumbnail = get_project_thumbnail(project_path) # Step 6: Update caches for the admin user and new project update_cache( user_id=current_user_id, project_id=new_project.id, permissions={ "can_read": True, "can_write": True, "is_owner": True }, project_data={ "id": new_project.id, "name": new_project.name, "lastModified": new_project.update_timestamp.strftime("%Y-%m-%d %H:%M:%S"), "thumbnail": thumbnail, "owner": [owner_email] } ) # Return the new project info return jsonify({ "id": new_project.id, "name": new_project.name, "status": "success" }) except Exception as e: logger.error(f"In register_routes - /create_project : Error creating project: {e}") logger.info("started rollabck") # Rollback: Clean up the projects filesystem directory if it was created if project_path and os.path.exists(project_path): try: shutil.rmtree(project_path) logger.info("In register_routes -/create_project : Rolled back project directory creation as db entry is not added") except Exception as cleanup_error: logger.exception(f"In register_routes -/create_project : Error during rollback cleanup: {cleanup_error}") # Optional: Remove project routes from Flask app if needed if next_id is not None and str(next_id) in ProjectBlueprint.blueprints: del ProjectBlueprint.blueprints[str(next_id)] logger.info("In register_routes -/create_project : Rolled back ProjectBlueprint.blueprints as db entry is not added") return jsonify({"error": str(e)}), 500 logger.info("Route registered: /create_project") @app.route("/import_project", methods=["POST"]) def import_project(): try: # Check if the request contains a file if 'file' not in request.files: logger.error("In register_routes /import_project: Error - No project archive provided") return jsonify({"error": "No project archive provided"}), 400 project_file = request.files['file'] project_name = request.form.get('name') # Get next available project ID next_id = ProjectService.get_next_project_id() project_path = os.path.join(app.config['projects_base_dir'], str(next_id)) os.makedirs(project_path, exist_ok=True) file_stream = io.BytesIO(project_file.read()) with zipfile.ZipFile(file_stream) as zf: names = zf.namelist() # Reject entries with absolute paths or “..” bad = [n for n in names if n.startswith(("/", "\\")) or ".." in n] if bad: logger.error("In register_routes /import_project: Error - Unsafe entries in ZIP") return jsonify({"error": "Invalid ZIP file: unsafe paths detected"}), 400 # Find the root directory of the mdv project root = find_root_prefix(names) if root is None: logger.error("In register_routes /import_project: Error - Not a valid MDV project") return jsonify({"error": "Not a valid MDV project"}), 400 # Select the files based in the mdv project members = [n for n in names if n.startswith(root)] # Extract them to the project path zf.extractall(path=project_path, members=members) # If everything was under a single top-level folder, flatten it if root: subdir = os.path.join(project_path, root.rstrip("/")) for item in os.listdir(subdir): shutil.move(os.path.join(subdir, item), project_path) os.rmdir(subdir) # # Create a new MDV project out of the new path and files copied p = MDVProject(project_path, backend_db=True) p.set_editable(True) p.serve(app=app, open_browser=False, backend_db=True) # Initialize the project and register it using project name if valid if project_name is not None: new_project = ProjectService.add_new_project(path=project_path, name=project_name) else: new_project = ProjectService.add_new_project(path=project_path) if new_project: if ENABLE_AUTH: user_data = session.get("user") if not user_data: raise ValueError("User not found in session.") current_user_id = user_data['id'] # Create a new entry with the new project and the current user UserProjectService.add_or_update_user_project( user_id=current_user_id, project_id=new_project.id, is_owner=True ) auth_id = user_data["auth_id"] owner_email = user_cache.get(auth_id, {}).get("email", "unknown") thumbnail = get_project_thumbnail(project_path) # Update cache with the new project data and user id update_cache( user_id=current_user_id, project_id=new_project.id, permissions={ "can_read": True, "can_write": True, "is_owner": True }, project_data={ "id": new_project.id, "name": new_project.name, "lastModified": new_project.update_timestamp.strftime("%Y-%m-%d %H:%M:%S"), "thumbnail": thumbnail, "owners": [owner_email] } ) # Return the new project id and name logger.info("Import successfull. Returning the success response.") return jsonify({ "id": new_project.id, "name": new_project.name, "status": "success" }) except Exception as e: logger.exception(f"In register_routes - /import_project : Error importing project: {e}") logger.info("started rollabck") # Clean up on error project_path = locals().get('project_path') next_id = locals().get('next_id') # Clean up project directory if it was created if project_path and os.path.exists(project_path): try: shutil.rmtree(project_path) logger.info("In register_routes -/import_project : Rolled back project directory creation as db entry is not added") except Exception as cleanup_error: logger.exception(f"In register_routes -/import_project : Error during cleanup: {cleanup_error}") # Remove from blueprints if registered if next_id is not None and str(next_id) in ProjectBlueprint.blueprints: try: del ProjectBlueprint.blueprints[str(next_id)] logger.info("In register_routes -/import_project : Rolled back ProjectBlueprint.blueprints as db entry is not added") except Exception as blueprint_error: logger.exception(f"In register_routes -/import_project : Error removing blueprint: {blueprint_error}") return jsonify({"error": str(e)}), 500 logger.info("Route registered: /import_project") @app.route("/export_project/<int:project_id>", methods=["GET"]) def export_project(project_id: int): try: # Fetch the project using the provided project id project = ProjectService.get_project_by_id(project_id) # No project with the given project id found if project is None: logger.error(f"In register_routes - /export_project Error: Project with ID {project_id} not found in database") return jsonify({"error": f"Project with ID {project_id} not found in database"}) if ENABLE_AUTH: user = session.get('user') if not user: raise ValueError("User not found in session.") user_id = user["id"] user_projects = user_project_cache.get(user_id) if not user_projects or not user_projects.get(int(project_id), {}).get("is_owner", False): logger.error(f"User does not have ownership of project {project_id}") return jsonify({"error": "Only the project owner can export the project."}), 403 if project.access_level != 'editable': logger.error(f"Project with ID {project_id} is not editable.") return jsonify({"error": "This project is not editable and cannot be exported."}), 403 if project.path is None: logger.error(f"In register_routes - /export_project Error: Project with ID {project_id} has no path in database") return jsonify({"error": f"Project with ID {project_id} has no path in the database"}) if project.name is None: project_name = "unnamed_project" else: project_name = project.name # Create a temporary directory with tempfile.TemporaryDirectory() as temp_dir: file_name = f"{project_name}" file_path = os.path.join(temp_dir, file_name) # Create an archive from the project path logger.info(f"Creating archive for project {project_name} at path: {project.path}...") zip_path = shutil.make_archive( file_path, "zip", project.path ) logger.info(f"Archive created at: {zip_path}") # Return the zip file return send_file( path_or_file=zip_path, mimetype="application/zip", as_attachment=True, download_name=f"{project_name}.mdv.zip" ) except Exception as e: logger.exception(f"In register_routes - /export_project : Unexpected error while exporting project with project id - '{project_id}': {e}") return jsonify({"error": str(e)}), 500 print("Route registered: /export_project/<project_id>") @app.route("/delete_project/<int:project_id>", methods=["DELETE"]) def delete_project(project_id: int): #project_removed_from_blueprints = False nonlocal active_projects_cache try: logger.info(f"Deleting project '{project_id}'") # Step 2: Check if the user is owner using in-memory cache if ENABLE_AUTH: user = session.get('user') if not user: raise ValueError("User not found in session.") user_id = user["id"] user_projects = user_project_cache.get(user_id) if not user_projects or not user_projects.get(int(project_id), {}).get("is_owner", False): logger.error(f"User does not have ownership of project {project_id}") return jsonify({"error": "Only the project owner can delete the project."}), 403 # Step 3: Get full project object for access_level check project_obj = ProjectService.get_project_by_id(project_id) if not project_obj: logger.error(f"Project with ID {project_id} not found in database") return jsonify({"error": f"Project with ID {project_id} not found in database"}), 404 # Step 4: Check if project is editable if project_obj.access_level != 'editable': logger.error(f"Project with ID {project_id} is not editable.") return jsonify({"error": "This project is not editable and cannot be deleted."}), 403 # Remove the project from the ProjectBlueprint.blueprints dictionary if str(project_id) in ProjectBlueprint.blueprints: del ProjectBlueprint.blueprints[str(project_id)] #project_removed_from_blueprints = True # Mark as removed logger.info(f"In register_routes - /delete_project : Removed project '{project_id}' from ProjectBlueprint.blueprints") # Soft delete the project delete_status = ProjectService.soft_delete_project(project_id) if not delete_status: logger.error(f"In delete_project: Error - Failed to soft delete project {project_id} in the database") return jsonify({"error": "Failed to soft delete project in db"}), 500 # Step 7: Remove from cache if ENABLE_AUTH: active_projects_cache = [p for p in active_projects_cache if p["id"] != project_id] logger.info(f"Removed project '{project_id}' from in-memory cache") # Step 8: Return success response return jsonify({"message": f"Project '{project_id}' deleted successfully."}) except Exception as e: logger.exception(f"In register_routes - /delete_project: Error deleting project '{project_id}': {e}") return jsonify({"error": str(e)}), 500 logger.info("Route registered: /delete_project/<project_id>") @app.route("/projects/<int:project_id>/rename", methods=["PUT"]) def rename_project(project_id: int): # Retrieve the new project name from the multipart/form-data payload new_name = request.form.get("name") if not new_name: return jsonify({"error": "New name not provided"}), 400 try: # Step 1: Check if project exists in active cache project = ProjectService.get_project_by_id(project_id) if not project: logger.error(f"Project with ID {project_id} not found in db") return jsonify({"error": f"Project with ID {project_id} not found"}), 404 # Step 2: Check ownership using cache if ENABLE_AUTH: user = session.get('user') if not user: raise ValueError("User not found in session.") user_id = user["id"] user_projects = user_project_cache.get(user_id) if not user_projects or not user_projects.get(int(project_id), {}).get("is_owner", False): logger.error(f"User does not have ownership of project {project_id}") return jsonify({"error": "Only the project owner can rename the project."}), 403 # Step 4: Check if project is editable if project.access_level != 'editable': logger.error(f"Project with ID {project_id} is not editable.") return jsonify({"error": "This project is not editable and cannot be renamed."}), 403 # Attempt to rename the project rename_status = ProjectService.update_project_name(project_id, new_name) if not rename_status: logger.error(f"In register_routes - /rename_project Error: The project with ID '{project_id}' not found in db") return jsonify({"error": f"Failed to rename project '{project_id}' in db"}), 500 # Step 6: Update the name in active_projects_cache if ENABLE_AUTH: for proj in active_projects_cache: if proj["id"] == project_id: proj["name"] = new_name break logger.info(f"In rename_project: Updated cache for active projects, renamed project '{project_id}'.") # Step 7: Return success response return jsonify({"status": "success", "id": project_id, "new_name": new_name}), 200 except Exception as e: logger.exception(f"In register_routes - /rename_project : Error renaming project '{project_id}': {e}") return jsonify({"error": str(e)}), 500 logger.info("Route registered: /projects/<int:project_id>/rename") @app.route("/projects/<int:project_id>/access", methods=["PUT"]) def change_project_access(project_id: int): """API endpoint to change the access level of a project (editable or read-only).""" try: # Get the new access level from the request new_access_level = request.form.get("type") # Validate the new access level if new_access_level not in ["read-only", "editable"]: return jsonify({"error": "Invalid access level. Must be 'read-only' or 'editable'."}), 400 # Step 3: Check ownership from the user_project_cache if ENABLE_AUTH: user = session.get('user') if not user: raise ValueError("User not found in session.") user_id = user["id"] user_projects = user_project_cache.get(user_id) if not user_projects or not user_projects.get(int(project_id), {}).get("is_owner", False): logger.error(f"User does not have ownership of project {project_id}") return jsonify({"error": "Only the project owner can change the access level."}), 403 # Call the service method to change the access level access_level, message, status_code = ProjectService.change_project_access(project_id, new_access_level) if access_level is None: return jsonify({"error": message}), status_code return jsonify({"status": "success", "access_level": access_level}), 200 except Exception as e: logger.exception(f"In register_routes - /access : Unexpected error while changing access level for project '{project_id}': {e}") return jsonify({"error": "An unexpected error occurred."}), 500 logger.info("Route registered: /projects/<int:project_id>/access") @app.route("/projects/<int:project_id>/share", methods=["GET"]) def share_project(project_id: int): """Fetch users with whom the project is shared along with their permissions.""" try: logger.info(f"Sharing project '{project_id}'") # Step 1: Skip authentication if not enabled if not ENABLE_AUTH: logger.info("Authentication is disabled, skipping authentication check.") return jsonify({"error": "Authentication is disabled, no action taken."}) user = session.get('user') if not user: raise ValueError("User not found in session.") user_id = user["id"] user_permissions = user_project_cache.get(user_id, {}).get(int(project_id)) if not user_permissions or not user_permissions.get("is_owner"): return jsonify({"error": "Only the project owner can share the project"}), 403 # Step 3: Compile shared users list shared_users_list = [] for uid, permissions in user_project_cache.items(): proj_perm = permissions.get(project_id) if proj_perm: user_data = user_cache.get(uid) if not user_data: # Fallback: Try fetching from the DB if not in cache user_obj = User.query.get(uid) if not user_obj: continue # Skip if user doesn't exist in DB user_data = { "id": user_obj.id, "email": user_obj.email, "auth_id": user_obj.auth_id, "is_admin": user_obj.is_admin } # Optionally update cache to avoid this hit next time user_cache[uid] = user_data if user_data not in all_users_cache: all_users_cache.append(user_data) shared_users_list.append({ "id": user_data["id"], "email": user_data["email"], "permission": ( "Owner" if proj_perm["is_owner"] else "Edit" if proj_perm["can_write"] else "View" ) }) # Step 4: Get unshared users for dropdown shared_user_ids = {u["id"] for u in shared_users_list} all_users = [ {"id": u["id"], "email": u["email"]} for u in all_users_cache if u["id"] not in shared_user_ids ] # Return the list of users with permissions, and all users for the dropdown return jsonify({ "shared_users": shared_users_list, "all_users": all_users # List of all users to populate the dropdown }) except Exception as e: logger.exception(f"Error in share_project: {e}") return jsonify({"error": str(e)}), 500 logger.info("Route registered: /projects/<int:project_id>/share- GET") @app.route("/projects/<int:project_id>/share", methods=["POST"]) def add_user_to_project(project_id: int): """Add a user to the project with specified permissions.""" try: logger.info(f"Adding user to project '{project_id}'") # Step 1: Skip authentication if not enabled if not ENABLE_AUTH: logger.info("Authentication is disabled, skipping authentication check.") return jsonify({"error": "Authentication is disabled, no action taken."}) user = session.get('user') if not user: raise ValueError("User not found in session.") user_id = user["id"] # Step 2: Check if current user is owner of the project user_permissions = user_project_cache.get(user_id, {}).get(int(project_id)) if not user_permissions or not user_permissions.get("is_owner"): return jsonify({"error": "Only the project owner can share the project"}), 403 # Step 5: Get data from the POST request data = request.get_json() target_user_id = data.get('user_id') # User to be added permission = data.get('permission', 'view') # "view", "edit", or "owner" if not target_user_id or permission not in ["view", "edit", "owner"]: return jsonify({"error": "Invalid user or permission"}), 400 # Step 4: Determine permission flags is_owner = permission == "owner" can_write = permission in ["edit", "owner"] #can_read = True # Always true if added to a project # Step 5: Update the DB via service UserProjectService.add_or_update_user_project( user_id=target_user_id, project_id=project_id, is_owner=is_owner, can_write=can_write ) # Update user_project_cache if target_user_id not in user_project_cache: user_project_cache[target_user_id] = {} user_project_cache[target_user_id][project_id] = { "can_read": (permission in ["view", "edit", "owner"]), "can_write": (permission in ["edit", "owner"]), "is_owner": (permission == "owner") } logger.info(f"User {target_user_id} added to project {project_id} with '{permission}' permission.") return jsonify({"message": f"User {target_user_id} added to project {project_id} with {permission} permission."}), 200 except Exception as e: logger.exception(f"Error in add_user_to_project: {e}") return jsonify({"error": str(e)}), 500 logger.info("Route registered: /projects/<int:project_id>/share- POST") @app.route("/projects/<int:project_id>/share/<int:user_id>/edit", methods=["POST"]) def edit_user_permission(project_id: int, user_id: int): """Edit user permissions for a project.""" try: logger.info(f"Editing permissions for user '{user_id}' in project '{project_id}'") # Step 1: Ensure authentication is enabled if not ENABLE_AUTH: logger.info("Authentication is disabled, skipping authentication check.") # If authentication is disabled, simply return and stop execution return jsonify({"Error": "Authentication is disabled, no action taken."}) user = session.get('user') if not user: raise ValueError("User not found in session.") current_user_id = user["id"] # Step 2: Validate if current user is the owner user_permissions = user_project_cache.get(current_user_id, {}).get(int(project_id)) if not user_permissions or not user_permissions.get("is_owner"): return jsonify({"error": "Only the project owner can edit permissions"}), 403 # Step 3: Extract new permission from request assert request.json, "Request JSON is empty" new_permission = request.json.get("permission", "").lower() if new_permission not in ["view", "edit", "owner"]: return jsonify({"error": "Invalid permission value"}), 400 is_owner = new_permission == "owner" can_write = new_permission in ["edit", "owner"] can_read = True # Always true for project access # Step 4: Update DB via service UserProjectService.add_or_update_user_project( user_id=user_id, project_id=project_id, is_owner=is_owner, can_write=can_write ) # Step 5: Update cache if user_id not in user_project_cache: user_project_cache[user_id] = {} user_project_cache[user_id][project_id] = { "can_read": can_read, "can_write": can_write, "is_owner": is_owner } logger.info(f"Updated permissions for user {user_id} in project {project_id}: {new_permission}") return jsonify({"message": "Permissions updated successfully"}), 200 except Exception as e: logger.exception(f"Error in edit_user_permission: {e}") return jsonify({"error": str(e)}), 500 logger.info("Route registered: /projects/<int:project_id>/share/<int:user_id>/edit") @app.route("/projects/<int:project_id>/share/<int:user_id>/delete", methods=["POST"]) def delete_user_from_project(project_id: int, user_id: int): """Remove a user from the project.""" try: logger.info(f"Removing user '{user_id}' from project '{project_id}'") # Step 1: Ensure authentication is enabled if not ENABLE_AUTH: logger.info("Authentication is disabled, skipping authentication check.") # If authentication is disabled, simply return and stop execution return jsonify({"error": "Authentication is disabled, no action taken."}) user = session.get('user') if not user: raise ValueError("User not found in session.") current_user_id = user["id"] # Step 2: Validate ownership user_permissions = user_project_cache.get(current_user_id, {}).get(int(project_id)) if not user_permissions or not user_permissions.get("is_owner"): return jsonify({"error": "Only the project owner can remove users"}), 403 # Step 4: Remove the user from the project using the service method UserProjectService.remove_user_from_project(user_id, project_id) # Step 5: Check if the user is part of the project # Step 5: Check if the user is part of the project user_permissions_cache = user_project_cache.get(user_id) if user_permissions_cache: if project_id in user_project_cache[user_id]: del user_project_cache[user_id][project_id] else: logger.warning(f"Project {project_id} not found in cache for user {user_id}") return jsonify({"message": "User removed successfully"}), 200 except Exception as e: logger.exception(f"Error in delete_user_from_project: {e}") return jsonify({"error": str(e)}), 500 logger.info("Route registered: /projects/<int:project_id>/share/<int:user_id>/delete") except Exception as e: logger.exception(f"Error registering routes: {e}") raise # Re-raise to be handled by the parent function