suna/backend/credentials/api.py

327 lines
12 KiB
Python
Raw Normal View History

2025-06-07 14:24:59 +08:00
from fastapi import APIRouter, HTTPException, Depends
from typing import List, Optional, Dict, Any
from pydantic import BaseModel, validator
2025-06-09 14:05:17 +08:00
import urllib.parse
2025-06-07 14:24:59 +08:00
from utils.logger import logger
from utils.auth_utils import get_current_user_id_from_jwt
2025-07-29 00:36:07 +08:00
from services.supabase import DBConnection
from .credential_service import (
get_credential_service,
MCPCredential,
CredentialNotFoundError,
CredentialAccessDeniedError
)
from .profile_service import (
get_profile_service,
MCPCredentialProfile,
ProfileNotFoundError,
ProfileAccessDeniedError
2025-07-14 18:36:27 +08:00
)
2025-07-29 00:36:07 +08:00
from .utils import validate_config_not_empty, decode_mcp_qualified_name, extract_config_keys
2025-06-07 14:24:59 +08:00
router = APIRouter()
2025-07-29 00:36:07 +08:00
db: Optional[DBConnection] = None
2025-07-14 18:36:27 +08:00
2025-06-07 14:24:59 +08:00
class StoreCredentialRequest(BaseModel):
mcp_qualified_name: str
display_name: str
config: Dict[str, Any]
@validator('config')
2025-07-29 00:36:07 +08:00
def validate_config_not_empty_field(cls, v):
return validate_config_not_empty(v)
2025-06-07 14:24:59 +08:00
2025-07-14 18:36:27 +08:00
class StoreCredentialProfileRequest(BaseModel):
mcp_qualified_name: str
profile_name: str
display_name: str
config: Dict[str, Any]
is_default: bool = False
@validator('config')
2025-07-29 00:36:07 +08:00
def validate_config_not_empty_field(cls, v):
return validate_config_not_empty(v)
2025-07-14 18:36:27 +08:00
2025-06-07 14:24:59 +08:00
class CredentialResponse(BaseModel):
credential_id: str
mcp_qualified_name: str
display_name: str
config_keys: List[str]
2025-06-07 14:24:59 +08:00
is_active: bool
2025-07-29 00:36:07 +08:00
created_at: Optional[str] = None
updated_at: Optional[str] = None
2025-06-07 14:24:59 +08:00
2025-07-14 18:36:27 +08:00
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
2025-07-29 00:36:07 +08:00
created_at: Optional[str] = None
updated_at: Optional[str] = None
2025-07-14 18:36:27 +08:00
2025-07-29 00:36:07 +08:00
def initialize(database: DBConnection):
global db
db = database
2025-06-07 14:24:59 +08:00
@router.post("/credentials", response_model=CredentialResponse)
2025-07-29 00:36:07 +08:00
async def store_credential(
2025-06-07 14:24:59 +08:00
request: StoreCredentialRequest,
user_id: str = Depends(get_current_user_id_from_jwt)
):
try:
2025-07-29 00:36:07 +08:00
credential_service = get_credential_service(db)
credential_id = await credential_service.store_credential(
account_id=user_id,
mcp_qualified_name=request.mcp_qualified_name,
display_name=request.display_name,
config=request.config
2025-06-07 14:24:59 +08:00
)
2025-07-29 00:36:07 +08:00
credential = await credential_service.get_credential(user_id, request.mcp_qualified_name)
2025-06-07 14:24:59 +08:00
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,
2025-07-29 00:36:07 +08:00
config_keys=extract_config_keys(credential.config),
2025-06-07 14:24:59 +08:00
is_active=credential.is_active,
2025-07-29 00:36:07 +08:00
created_at=credential.created_at.isoformat() if credential.created_at else None,
updated_at=credential.updated_at.isoformat() if credential.updated_at else None
2025-06-07 14:24:59 +08:00
)
2025-07-29 00:36:07 +08:00
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
2025-06-07 14:24:59 +08:00
except Exception as e:
2025-07-29 00:36:07 +08:00
logger.error(f"Error storing credential: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
2025-06-07 14:24:59 +08:00
@router.get("/credentials", response_model=List[CredentialResponse])
async def get_user_credentials(
user_id: str = Depends(get_current_user_id_from_jwt)
):
try:
2025-07-29 00:36:07 +08:00
credential_service = get_credential_service(db)
credentials = await credential_service.get_user_credentials(user_id)
2025-06-09 14:05:17 +08:00
2025-06-07 14:24:59 +08:00
return [
CredentialResponse(
credential_id=cred.credential_id,
mcp_qualified_name=cred.mcp_qualified_name,
display_name=cred.display_name,
2025-07-29 00:36:07 +08:00
config_keys=extract_config_keys(cred.config),
2025-06-07 14:24:59 +08:00
is_active=cred.is_active,
2025-07-29 00:36:07 +08:00
created_at=cred.created_at.isoformat() if cred.created_at else None,
updated_at=cred.updated_at.isoformat() if cred.updated_at else None
2025-06-07 14:24:59 +08:00
)
for cred in credentials
]
2025-07-29 00:36:07 +08:00
2025-06-07 14:24:59 +08:00
except Exception as e:
2025-07-29 00:36:07 +08:00
logger.error(f"Error getting user credentials: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
2025-06-07 14:24:59 +08:00
2025-06-09 14:05:17 +08:00
@router.delete("/credentials/{mcp_qualified_name:path}")
2025-07-29 00:36:07 +08:00
async def delete_credential(
2025-06-07 14:24:59 +08:00
mcp_qualified_name: str,
user_id: str = Depends(get_current_user_id_from_jwt)
):
try:
2025-07-29 00:36:07 +08:00
decoded_name = decode_mcp_qualified_name(mcp_qualified_name)
2025-07-14 18:36:27 +08:00
2025-07-29 00:36:07 +08:00
credential_service = get_credential_service(db)
success = await credential_service.delete_credential(user_id, decoded_name)
2025-06-09 14:05:17 +08:00
2025-06-07 14:24:59 +08:00
if not success:
raise HTTPException(status_code=404, detail="Credential not found")
return {"message": "Credential deleted successfully"}
except HTTPException:
raise
except Exception as e:
2025-07-29 00:36:07 +08:00
logger.error(f"Error deleting credential: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
2025-06-07 14:24:59 +08:00
@router.post("/credential-profiles", response_model=CredentialProfileResponse)
async def store_credential_profile(
request: StoreCredentialProfileRequest,
user_id: str = Depends(get_current_user_id_from_jwt)
):
try:
2025-07-29 00:36:07 +08:00
profile_service = get_profile_service(db)
profile_id = await profile_service.store_profile(
account_id=user_id,
mcp_qualified_name=request.mcp_qualified_name,
profile_name=request.profile_name,
display_name=request.display_name,
config=request.config,
is_default=request.is_default
)
2025-07-29 00:36:07 +08:00
profile = await profile_service.get_profile(user_id, profile_id)
if not profile:
2025-07-14 18:36:27 +08:00
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,
2025-07-29 00:36:07 +08:00
config_keys=extract_config_keys(profile.config),
is_active=profile.is_active,
is_default=profile.is_default,
2025-07-29 00:36:07 +08:00
created_at=profile.created_at.isoformat() if profile.created_at else None,
updated_at=profile.updated_at.isoformat() if profile.updated_at else None
)
2025-07-29 00:36:07 +08:00
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
2025-07-29 00:36:07 +08:00
logger.error(f"Error storing credential profile: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/credential-profiles", response_model=List[CredentialProfileResponse])
2025-07-29 00:36:07 +08:00
async def get_user_credential_profiles(
user_id: str = Depends(get_current_user_id_from_jwt)
):
try:
2025-07-29 00:36:07 +08:00
profile_service = get_profile_service(db)
profiles = await profile_service.get_all_user_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,
2025-07-29 00:36:07 +08:00
config_keys=extract_config_keys(profile.config),
is_active=profile.is_active,
is_default=profile.is_default,
2025-07-29 00:36:07 +08:00
created_at=profile.created_at.isoformat() if profile.created_at else None,
updated_at=profile.updated_at.isoformat() if profile.updated_at else None
)
for profile in profiles
]
2025-07-29 00:36:07 +08:00
except Exception as e:
2025-07-29 00:36:07 +08:00
logger.error(f"Error getting user credential profiles: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@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:
2025-07-29 00:36:07 +08:00
decoded_name = decode_mcp_qualified_name(mcp_qualified_name)
2025-07-14 18:36:27 +08:00
2025-07-29 00:36:07 +08:00
profile_service = get_profile_service(db)
profiles = await profile_service.get_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,
2025-07-29 00:36:07 +08:00
config_keys=extract_config_keys(profile.config),
is_active=profile.is_active,
is_default=profile.is_default,
2025-07-29 00:36:07 +08:00
created_at=profile.created_at.isoformat() if profile.created_at else None,
updated_at=profile.updated_at.isoformat() if profile.updated_at else None
)
for profile in profiles
]
2025-07-29 00:36:07 +08:00
except Exception as e:
2025-07-29 00:36:07 +08:00
logger.error(f"Error getting credential profiles for MCP: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/credential-profiles/profile/{profile_id}", response_model=CredentialProfileResponse)
2025-07-29 00:36:07 +08:00
async def get_credential_profile(
profile_id: str,
user_id: str = Depends(get_current_user_id_from_jwt)
):
try:
2025-07-29 00:36:07 +08:00
profile_service = get_profile_service(db)
profile = await profile_service.get_profile(user_id, profile_id)
if not profile:
2025-07-29 00:36:07 +08:00
raise HTTPException(status_code=404, detail="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,
2025-07-29 00:36:07 +08:00
config_keys=extract_config_keys(profile.config),
is_active=profile.is_active,
is_default=profile.is_default,
2025-07-29 00:36:07 +08:00
created_at=profile.created_at.isoformat() if profile.created_at else None,
updated_at=profile.updated_at.isoformat() if profile.updated_at else None
)
2025-07-29 00:36:07 +08:00
except ProfileAccessDeniedError:
raise HTTPException(status_code=403, detail="Access denied to profile")
except Exception as e:
2025-07-29 00:36:07 +08:00
logger.error(f"Error getting credential profile: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@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)
):
try:
2025-07-29 00:36:07 +08:00
profile_service = get_profile_service(db)
success = await profile_service.set_default_profile(user_id, profile_id)
if not success:
2025-07-29 00:36:07 +08:00
raise HTTPException(status_code=404, detail="Profile not found")
return {"message": "Profile set as default successfully"}
except Exception as e:
2025-07-29 00:36:07 +08:00
logger.error(f"Error setting default profile: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.delete("/credential-profiles/{profile_id}")
async def delete_credential_profile(
profile_id: str,
user_id: str = Depends(get_current_user_id_from_jwt)
):
try:
2025-07-29 00:36:07 +08:00
profile_service = get_profile_service(db)
success = await profile_service.delete_profile(user_id, profile_id)
if not success:
2025-07-29 00:36:07 +08:00
raise HTTPException(status_code=404, detail="Profile not found")
return {"message": "Profile deleted successfully"}
except Exception as e:
2025-07-29 00:36:07 +08:00
logger.error(f"Error deleting profile: {e}")
raise HTTPException(status_code=500, detail="Internal server error")