diff --git a/api/credentials_service.py b/api/credentials_service.py index 9ad77f3..8f288eb 100644 --- a/api/credentials_service.py +++ b/api/credentials_service.py @@ -223,6 +223,7 @@ def credential_to_response(cred: Credential, model_count: int = 0) -> Credential created=str(cred.created) if cred.created else "", updated=str(cred.updated) if cred.updated else "", model_count=model_count, + decryption_error=cred.decryption_error, ) diff --git a/api/models.py b/api/models.py index 7ea8967..4be0049 100644 --- a/api/models.py +++ b/api/models.py @@ -617,6 +617,7 @@ class CredentialResponse(BaseModel): created: str updated: str model_count: int = 0 + decryption_error: Optional[str] = None class CredentialDeleteResponse(BaseModel): diff --git a/api/routers/credentials.py b/api/routers/credentials.py index b3cf86f..38e3a2d 100644 --- a/api/routers/credentials.py +++ b/api/routers/credentials.py @@ -27,16 +27,22 @@ from pydantic import SecretStr from api.credentials_service import ( credential_to_response, discover_with_config, - migrate_from_env as svc_migrate_from_env, - migrate_from_provider_config as svc_migrate_from_provider_config, + get_provider_status, register_models, require_encryption_key, - test_credential as svc_test_credential, validate_url, ) from api.credentials_service import ( get_env_status as svc_get_env_status, - get_provider_status, +) +from api.credentials_service import ( + migrate_from_env as svc_migrate_from_env, +) +from api.credentials_service import ( + migrate_from_provider_config as svc_migrate_from_provider_config, +) +from api.credentials_service import ( + test_credential as svc_test_credential, ) from api.models import ( CreateCredentialRequest, @@ -48,6 +54,7 @@ from api.models import ( RegisterModelsResponse, UpdateCredentialRequest, ) +from open_notebook.database.repository import ensure_record_id, repo_delete, repo_query from open_notebook.domain.credential import Credential router = APIRouter(prefix="/credentials", tags=["credentials"]) @@ -260,7 +267,36 @@ async def delete_credential( - Otherwise, linked models are cascade-deleted automatically """ try: - cred = await Credential.get(credential_id) + try: + cred = await Credential.get(credential_id) + except Exception as decrypt_err: + # Credential exists but can't be decrypted (wrong encryption key). + # Fall back to direct DB operations for deletion. + logger.warning( + f"Cannot decrypt credential {credential_id}, " + f"falling back to direct delete: {decrypt_err}" + ) + + # Delete linked models directly + linked = await repo_query( + "SELECT * FROM model WHERE credential = $cred_id", + {"cred_id": ensure_record_id(credential_id)}, + ) + deleted_models = 0 + for model_row in linked: + model_id = str(model_row.get("id", "")) + if model_id: + await repo_delete(model_id) + deleted_models += 1 + + # Delete the credential itself + await repo_delete(credential_id) + + return CredentialDeleteResponse( + message="Credential deleted successfully", + deleted_models=deleted_models, + ) + linked_models = await cred.get_linked_models() deleted_models = 0 diff --git a/frontend/src/app/(dashboard)/settings/api-keys/page.tsx b/frontend/src/app/(dashboard)/settings/api-keys/page.tsx index 27bde0e..086fb1a 100644 --- a/frontend/src/app/(dashboard)/settings/api-keys/page.tsx +++ b/frontend/src/app/(dashboard)/settings/api-keys/page.tsx @@ -22,6 +22,7 @@ import { RefreshCw, Key, ShieldAlert, + AlertTriangle, Plus, Edit, Trash2, @@ -822,7 +823,7 @@ function CredentialItem({ -