Skip to main content
Glama
users.py6.99 kB
""" User management routes with cascade deletion logic. This module implements Phase 8 of the Memory Sharing Implementation Plan: - User account deletion with complete cascade cleanup """ import logging from bson import ObjectId from fastapi import APIRouter, Depends, HTTPException from pymongo.database import Database from ..dependencies import AuthContext, authenticate_api_key, mongo_db from ..utils.datetime_helpers import utc_now from ..utils.permission_helpers import get_user_object_id_from_kratos_id logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/users", tags=["User Management"]) # Helper Functions def count_organization_owners(db: Database, organization_id: ObjectId) -> int: """Count number of owners in organization.""" org = db.organizations.find_one({"_id": organization_id}) return 1 if org else 0 def get_user_owned_organizations(db: Database, user_id: ObjectId) -> list: """Get all organizations where user is the owner.""" return list(db.organizations.find({"ownerId": user_id})) def get_user_admin_projects(db: Database, user_id: ObjectId) -> list: """Get all projects where user is the only admin.""" projects = list(db.project_members.find({"userId": user_id, "role": "admin"})) # Filter to only projects where user is the ONLY admin sole_admin_projects = [] for member in projects: admin_count = db.project_members.count_documents( {"projectId": member["projectId"], "role": "admin"} ) if admin_count == 1: sole_admin_projects.append(member["projectId"]) return sole_admin_projects # API Endpoints @router.delete("/me", summary="Delete user account with cascade cleanup") def delete_user_account( auth: AuthContext = Depends(authenticate_api_key), ): """ Delete user's account with complete cascade cleanup. **Cascade Actions:** 1. Set user.isActive = false 2. Deactivate ALL user's API keys 3. Remove from ALL organization_members 4. Remove from ALL project_members 5. Mark invitations as cancelled 6. Keep memories (project-owned, lose attribution) **Restrictions:** - User must NOT be the owner of any organization (must transfer first) - User must NOT be the sole admin of any project (must promote another) """ # Get MongoDB ObjectId from Kratos ID user_obj_id = get_user_object_id_from_kratos_id(mongo_db, auth.user_id) # Verify user exists user = mongo_db.users.find_one({"_id": user_obj_id}) if not user: raise HTTPException(status_code=404, detail="User not found") # Check if user owns any organizations owned_orgs = get_user_owned_organizations(mongo_db, user_obj_id) if owned_orgs: org_names = [org["name"] for org in owned_orgs] raise HTTPException( status_code=400, detail=f"Cannot delete account. You are the owner of {len(owned_orgs)} organization(s): {', '.join(org_names)}. Transfer ownership first.", ) # Check if user is sole admin of any projects sole_admin_projects = get_user_admin_projects(mongo_db, user_obj_id) if sole_admin_projects: raise HTTPException( status_code=400, detail=f"Cannot delete account. You are the sole admin of {len(sole_admin_projects)} project(s). Promote another admin first.", ) # CASCADE 1: Deactivate user account mongo_db.users.update_one( {"_id": user_obj_id}, { "$set": { "isActive": False, "deactivatedAt": utc_now(), "deactivatedReason": "user_requested_deletion", } }, ) # CASCADE 2: Deactivate ALL user's API keys key_deactivation_result = mongo_db.api_keys.update_many( {"userId": user_obj_id, "isActive": True}, { "$set": { "isActive": False, "deactivatedAt": utc_now(), "deactivatedBy": user_obj_id, "deactivatedReason": "user_account_deleted", } }, ) # CASCADE 3: Remove from ALL organization_members org_removal_result = mongo_db.organization_members.delete_many( {"userId": user_obj_id} ) # CASCADE 4: Remove from ALL project_members project_removal_result = mongo_db.project_members.delete_many( {"userId": user_obj_id} ) # CASCADE 5: Cancel ALL pending invitations sent by this user invitation_cancel_result = mongo_db.invitations.update_many( {"invitedBy": user_obj_id, "status": "pending"}, { "$set": { "status": "cancelled", "cancelledAt": utc_now(), "cancelledReason": "inviter_account_deleted", } }, ) # CASCADE 6: Cancel ALL pending invitations TO this user invitation_to_user_cancel_result = mongo_db.invitations.update_many( {"email": user.get("email"), "status": "pending"}, { "$set": { "status": "cancelled", "cancelledAt": utc_now(), "cancelledReason": "invitee_account_deleted", } }, ) # Note: Memories are NOT deleted - they remain in the project # Attribution (createdBy) will show the deactivated user ID logger.info( f"User account deleted: user={auth.user_id}, " f"keys_deactivated={key_deactivation_result.modified_count}, " f"orgs_removed={org_removal_result.deleted_count}, " f"projects_removed={project_removal_result.deleted_count}, " f"invitations_cancelled={invitation_cancel_result.modified_count + invitation_to_user_cancel_result.modified_count}" ) return { "message": "User account deleted successfully", "user_id": auth.user_id, "cascades": { "api_keys_deactivated": key_deactivation_result.modified_count, "organizations_removed": org_removal_result.deleted_count, "projects_removed": project_removal_result.deleted_count, "invitations_cancelled": invitation_cancel_result.modified_count + invitation_to_user_cancel_result.modified_count, }, "note": "Memories created by you remain in their respective projects but attribution is preserved.", } @router.get("/me", summary="Get current user details") def get_current_user( auth: AuthContext = Depends(authenticate_api_key), ): """Get details of the currently authenticated user.""" # Get MongoDB ObjectId from Kratos ID user_obj_id = get_user_object_id_from_kratos_id(mongo_db, auth.user_id) user = mongo_db.users.find_one({"_id": user_obj_id}) if not user: raise HTTPException(status_code=404, detail="User not found") # Remove sensitive fields user.pop("_id", None) user.pop("password", None) # Add user ID as string user["user_id"] = auth.user_id return user

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/shrijayan/SelfMemory'

If you have feedback or need assistance with the MCP directory API, please join our Discord server