from fastapi import APIRouter, HTTPException, Depends from typing import List, Optional, Dict, Any from pydantic import BaseModel, validator import urllib.parse from utils.logger import logger from utils.auth_utils import get_current_user_id_from_jwt from .domain.entities import MCPCredential, MCPCredentialProfile from .domain.exceptions import ( CredentialNotFoundError, ProfileNotFoundError, CredentialAccessDeniedError, ProfileAccessDeniedError ) router = APIRouter() credential_manager = None class StoreCredentialRequest(BaseModel): mcp_qualified_name: str display_name: str config: Dict[str, Any] @validator('config') def validate_config_not_empty(cls, v): if not v: raise ValueError('Config cannot be empty') return v class StoreCredentialProfileRequest(BaseModel): mcp_qualified_name: str profile_name: str display_name: str config: Dict[str, Any] is_default: bool = False @validator('config') def validate_config_not_empty(cls, v): if not v: raise ValueError('Config cannot be empty') return v class CredentialResponse(BaseModel): credential_id: str mcp_qualified_name: str display_name: str config_keys: List[str] is_active: bool last_used_at: Optional[str] created_at: str updated_at: str class CredentialProfileResponse(BaseModel): profile_id: str mcp_qualified_name: str profile_name: str display_name: str config_keys: List[str] is_active: bool is_default: bool last_used_at: Optional[str] created_at: str updated_at: str class SetDefaultProfileRequest(BaseModel): profile_id: str class TestCredentialResponse(BaseModel): success: bool message: str error_details: Optional[str] = None @router.post("/credentials", response_model=CredentialResponse) async def store_mcp_credential( request: StoreCredentialRequest, user_id: str = Depends(get_current_user_id_from_jwt) ): logger.info(f"Storing credential for {request.mcp_qualified_name} for user {user_id}") try: credential_id = await credential_manager.store_credential( user_id, request.mcp_qualified_name, request.display_name, request.config ) credential = await credential_manager.get_credential(user_id, request.mcp_qualified_name) if not credential: raise HTTPException(status_code=500, detail="Failed to retrieve stored credential") return CredentialResponse( credential_id=credential.credential_id, mcp_qualified_name=credential.mcp_qualified_name, display_name=credential.display_name, config_keys=list(credential.config.keys()), is_active=credential.is_active, last_used_at=credential.last_used_at.isoformat() if credential.last_used_at and hasattr(credential.last_used_at, 'isoformat') else (str(credential.last_used_at) if credential.last_used_at else None), created_at=credential.created_at.isoformat() if credential.created_at and hasattr(credential.created_at, 'isoformat') else (str(credential.created_at) if credential.created_at else ""), updated_at=credential.updated_at.isoformat() if credential.updated_at and hasattr(credential.updated_at, 'isoformat') else (str(credential.updated_at) if credential.updated_at else "") ) except Exception as e: logger.error(f"Error storing credential: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to store credential: {str(e)}") @router.get("/credentials", response_model=List[CredentialResponse]) async def get_user_credentials( user_id: str = Depends(get_current_user_id_from_jwt) ): logger.info(f"Getting credentials for user {user_id}") try: credentials = await credential_manager.get_user_credentials(user_id) logger.debug(f"Found {len(credentials)} credentials for user {user_id}") for cred in credentials: logger.debug(f"Credential: '{cred.mcp_qualified_name}' (ID: {cred.credential_id})") return [ CredentialResponse( credential_id=cred.credential_id, mcp_qualified_name=cred.mcp_qualified_name, display_name=cred.display_name, config_keys=list(cred.config.keys()), is_active=cred.is_active, last_used_at=cred.last_used_at.isoformat() if cred.last_used_at and hasattr(cred.last_used_at, 'isoformat') else (str(cred.last_used_at) if cred.last_used_at else None), created_at=cred.created_at.isoformat() if cred.created_at and hasattr(cred.created_at, 'isoformat') else (str(cred.created_at) if cred.created_at else ""), updated_at=cred.updated_at.isoformat() if cred.updated_at and hasattr(cred.updated_at, 'isoformat') else (str(cred.updated_at) if cred.updated_at else "") ) for cred in credentials ] except Exception as e: logger.error(f"Error getting user credentials: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to get credentials: {str(e)}") @router.delete("/credentials/{mcp_qualified_name:path}") async def delete_mcp_credential( mcp_qualified_name: str, user_id: str = Depends(get_current_user_id_from_jwt) ): try: decoded_name = urllib.parse.unquote(mcp_qualified_name) logger.info(f"Deleting credential for '{decoded_name}' (raw: '{mcp_qualified_name}') for user {user_id}") existing_credential = await credential_manager.get_credential(user_id, decoded_name) if not existing_credential: logger.warning(f"Credential not found: '{decoded_name}' for user {user_id}") raise HTTPException(status_code=404, detail=f"Credential not found: {decoded_name}") success = await credential_manager.delete_credential(user_id, decoded_name) if not success: logger.error(f"Failed to delete credential: '{decoded_name}' for user {user_id}") raise HTTPException(status_code=404, detail="Credential not found") logger.info(f"Successfully deleted credential: '{decoded_name}' for user {user_id}") return {"message": "Credential deleted successfully"} except HTTPException: raise except Exception as e: logger.error(f"Error deleting credential '{decoded_name}': {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to delete credential: {str(e)}") @router.post("/credential-profiles", response_model=CredentialProfileResponse) async def store_credential_profile( request: StoreCredentialProfileRequest, user_id: str = Depends(get_current_user_id_from_jwt) ): logger.info(f"Storing credential profile '{request.profile_name}' for {request.mcp_qualified_name} for user {user_id}") try: profile_id = await credential_manager.store_credential_profile( user_id, request.mcp_qualified_name, request.profile_name, request.display_name, request.config, request.is_default ) profile = await credential_manager.get_credential_by_profile(user_id, profile_id) if not profile: raise HTTPException(status_code=500, detail="Failed to retrieve stored profile") return CredentialProfileResponse( profile_id=profile.profile_id, mcp_qualified_name=profile.mcp_qualified_name, profile_name=profile.profile_name, display_name=profile.display_name, config_keys=list(profile.config.keys()), is_active=profile.is_active, is_default=profile.is_default, last_used_at=profile.last_used_at.isoformat() if profile.last_used_at and hasattr(profile.last_used_at, 'isoformat') else (str(profile.last_used_at) if profile.last_used_at else None), created_at=profile.created_at.isoformat() if profile.created_at and hasattr(profile.created_at, 'isoformat') else (str(profile.created_at) if profile.created_at else ""), updated_at=profile.updated_at.isoformat() if profile.updated_at and hasattr(profile.updated_at, 'isoformat') else (str(profile.updated_at) if profile.updated_at else "") ) except Exception as e: logger.error(f"Error storing credential profile: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to store credential profile: {str(e)}") @router.get("/credential-profiles", response_model=List[CredentialProfileResponse]) async def get_all_user_credential_profiles( user_id: str = Depends(get_current_user_id_from_jwt) ): logger.info(f"Getting all credential profiles for user {user_id}") try: profiles = await credential_manager.get_all_user_credential_profiles(user_id) return [ CredentialProfileResponse( profile_id=profile.profile_id, mcp_qualified_name=profile.mcp_qualified_name, profile_name=profile.profile_name, display_name=profile.display_name, config_keys=list(profile.config.keys()), is_active=profile.is_active, is_default=profile.is_default, last_used_at=profile.last_used_at.isoformat() if profile.last_used_at and hasattr(profile.last_used_at, 'isoformat') else (str(profile.last_used_at) if profile.last_used_at else None), created_at=profile.created_at.isoformat() if profile.created_at and hasattr(profile.created_at, 'isoformat') else (str(profile.created_at) if profile.created_at else ""), updated_at=profile.updated_at.isoformat() if profile.updated_at and hasattr(profile.updated_at, 'isoformat') else (str(profile.updated_at) if profile.updated_at else "") ) for profile in profiles ] except Exception as e: logger.error(f"Error getting all user credential profiles: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to get credential profiles: {str(e)}") @router.get("/credential-profiles/{mcp_qualified_name:path}", response_model=List[CredentialProfileResponse]) async def get_credential_profiles_for_mcp( mcp_qualified_name: str, user_id: str = Depends(get_current_user_id_from_jwt) ): try: decoded_name = urllib.parse.unquote(mcp_qualified_name) logger.info(f"Getting credential profiles for '{decoded_name}' for user {user_id}") profiles = await credential_manager.get_credential_profiles(user_id, decoded_name) return [ CredentialProfileResponse( profile_id=profile.profile_id, mcp_qualified_name=profile.mcp_qualified_name, profile_name=profile.profile_name, display_name=profile.display_name, config_keys=list(profile.config.keys()), is_active=profile.is_active, is_default=profile.is_default, last_used_at=profile.last_used_at.isoformat() if profile.last_used_at and hasattr(profile.last_used_at, 'isoformat') else (str(profile.last_used_at) if profile.last_used_at else None), created_at=profile.created_at.isoformat() if profile.created_at and hasattr(profile.created_at, 'isoformat') else (str(profile.created_at) if profile.created_at else ""), updated_at=profile.updated_at.isoformat() if profile.updated_at and hasattr(profile.updated_at, 'isoformat') else (str(profile.updated_at) if profile.updated_at else "") ) for profile in profiles ] except Exception as e: logger.error(f"Error getting credential profiles for '{decoded_name}': {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to get credential profiles: {str(e)}") @router.get("/credential-profiles/profile/{profile_id}", response_model=CredentialProfileResponse) async def get_credential_profile_by_id( profile_id: str, user_id: str = Depends(get_current_user_id_from_jwt) ): logger.info(f"Getting credential profile {profile_id} for user {user_id}") try: profile = await credential_manager.get_credential_by_profile(user_id, profile_id) if not profile: raise HTTPException(status_code=404, detail="Credential profile not found") return CredentialProfileResponse( profile_id=profile.profile_id, mcp_qualified_name=profile.mcp_qualified_name, profile_name=profile.profile_name, display_name=profile.display_name, config_keys=list(profile.config.keys()), is_active=profile.is_active, is_default=profile.is_default, last_used_at=profile.last_used_at.isoformat() if profile.last_used_at and hasattr(profile.last_used_at, 'isoformat') else (str(profile.last_used_at) if profile.last_used_at else None), created_at=profile.created_at.isoformat() if profile.created_at and hasattr(profile.created_at, 'isoformat') else (str(profile.created_at) if profile.created_at else ""), updated_at=profile.updated_at.isoformat() if profile.updated_at and hasattr(profile.updated_at, 'isoformat') else (str(profile.updated_at) if profile.updated_at else "") ) except HTTPException: raise except Exception as e: logger.error(f"Error getting credential profile {profile_id}: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to get credential profile: {str(e)}") @router.put("/credential-profiles/{profile_id}/set-default") async def set_default_credential_profile( profile_id: str, user_id: str = Depends(get_current_user_id_from_jwt) ): logger.info(f"Setting credential profile {profile_id} as default for user {user_id}") try: success = await credential_manager.set_default_profile(user_id, profile_id) if not success: raise HTTPException(status_code=404, detail="Credential profile not found") return {"message": "Default profile set successfully"} except HTTPException: raise except Exception as e: logger.error(f"Error setting default profile {profile_id}: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to set default profile: {str(e)}") @router.delete("/credential-profiles/{profile_id}") async def delete_credential_profile( profile_id: str, user_id: str = Depends(get_current_user_id_from_jwt) ): logger.info(f"Deleting credential profile {profile_id} for user {user_id}") try: success = await credential_manager.delete_credential_profile(user_id, profile_id) if not success: raise HTTPException(status_code=404, detail="Credential profile not found") return {"message": "Credential profile deleted successfully"} except HTTPException: raise except Exception as e: logger.error(f"Error deleting credential profile {profile_id}: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to delete credential profile: {str(e)}")