diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx index cc7e925499..a3c5be94ec 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx @@ -142,7 +142,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { strokeWidth={2} /> setSearchTerm(e.target.value)} className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' @@ -195,7 +195,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { {workspaceKeys.length === 0 ? (
- No workspace API keys yet + No workspace Sim keys yet
) : ( workspaceKeys.map((key) => ( @@ -301,7 +301,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { {isConflict && (
- Workspace API key with the same name overrides this. Rename your + Workspace Sim key with the same name overrides this. Rename your personal key to use it.
)} @@ -317,7 +317,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { filteredWorkspaceKeys.length === 0 && (personalKeys.length > 0 || workspaceKeys.length > 0) && (
- No API keys found matching "{searchTerm}" + No Sim keys found matching "{searchTerm}"
)} @@ -331,7 +331,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) {
- Allow personal API keys + Allow personal Sim keys @@ -383,7 +383,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { {/* Delete Confirmation Dialog */} - Delete API key + Delete Sim key

Deleting{' '} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/components/create-api-key-modal/create-api-key-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/components/create-api-key-modal/create-api-key-modal.tsx index 439280bf25..12b0692159 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/components/create-api-key-modal/create-api-key-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/components/create-api-key-modal/create-api-key-modal.tsx @@ -62,8 +62,8 @@ export function CreateApiKeyModal({ if (isDuplicate) { setCreateError( keyType === 'workspace' - ? `A workspace API key named "${trimmedName}" already exists. Please choose a different name.` - : `A personal API key named "${trimmedName}" already exists. Please choose a different name.` + ? `A workspace Sim key named "${trimmedName}" already exists. Please choose a different name.` + : `A personal Sim key named "${trimmedName}" already exists. Please choose a different name.` ) return } @@ -86,11 +86,11 @@ export function CreateApiKeyModal({ } catch (error: unknown) { logger.error('API key creation failed:', { error }) const errorMessage = - error instanceof Error ? error.message : 'Failed to create API key. Please try again.' + error instanceof Error ? error.message : 'Failed to create Sim key. Please try again.' if (errorMessage.toLowerCase().includes('already exists')) { setCreateError(errorMessage) } else { - setCreateError('Failed to create API key. Please check your connection and try again.') + setCreateError('Failed to create Sim key. Please check your connection and try again.') } } } @@ -113,7 +113,7 @@ export function CreateApiKeyModal({ {/* Create API Key Dialog */} - Create new API key + Create new Sim key

{keyType === 'workspace' @@ -125,7 +125,7 @@ export function CreateApiKeyModal({ {canManageWorkspaceKeys && (

- API Key Type + Sim Key Type

- Enter a name for your API key to help you identify it later. + Enter a name for your Sim key to help you identify it later.

{/* Hidden decoy fields to prevent browser autofill */} - Your API key has been created + Your Sim key has been created

- This is the only time you will see your API key.{' '} + This is the only time you will see your Sim key.{' '} Copy it now and store it securely. diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials-manager.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials-manager.tsx index c2e6960f34..036f280675 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials-manager.tsx @@ -2,7 +2,7 @@ import { createElement, useCallback, useEffect, useMemo, useState } from 'react' import { createLogger } from '@sim/logger' -import { AlertTriangle, Check, Copy, Plus, RefreshCw, Search, Share2, Trash2 } from 'lucide-react' +import { AlertTriangle, Check, Copy, Plus, RefreshCw, Search, Share2, X } from 'lucide-react' import { useParams } from 'next/navigation' import { Badge, @@ -20,9 +20,8 @@ import { Textarea, Tooltip, } from '@/components/emcn' -import { Skeleton } from '@/components/ui' +import { Skeleton, Input as UiInput } from '@/components/ui' import { useSession } from '@/lib/auth/auth-client' -import { cn } from '@/lib/core/utils/cn' import { clearPendingCredentialCreateRequest, PENDING_CREDENTIAL_CREATE_REQUEST_EVENT, @@ -72,8 +71,8 @@ type SecretScope = 'workspace' | 'personal' type SecretInputMode = 'single' | 'bulk' const createTypeOptions = [ - { value: 'oauth', label: 'OAuth Account' }, { value: 'secret', label: 'Secret' }, + { value: 'oauth', label: 'OAuth Account' }, ] as const interface ParsedEnvEntry { @@ -151,9 +150,9 @@ function typeBadgeVariant(type: WorkspaceCredential['type']): 'blue' | 'amber' | } function typeLabel(type: WorkspaceCredential['type']): string { - if (type === 'oauth') return 'OAuth' - if (type === 'env_workspace') return 'Workspace Secret' - return 'Personal Secret' + if (type === 'oauth') return 'oauth' + if (type === 'env_workspace') return 'workspace secret' + return 'personal secret' } function normalizeEnvKeyInput(raw: string): string { @@ -162,6 +161,21 @@ function normalizeEnvKeyInput(raw: string): string { return wrappedMatch ? wrappedMatch[1] : trimmed } +function CredentialSkeleton() { + return ( +

+
+ + +
+
+ + +
+
+ ) +} + export function CredentialsManager() { const params = useParams() const workspaceId = (params?.workspaceId as string) || '' @@ -171,7 +185,7 @@ export function CredentialsManager() { const [memberRole, setMemberRole] = useState('admin') const [memberUserId, setMemberUserId] = useState('') const [showCreateModal, setShowCreateModal] = useState(false) - const [createType, setCreateType] = useState('oauth') + const [createType, setCreateType] = useState('secret') const [createSecretScope, setCreateSecretScope] = useState('workspace') const [createDisplayName, setCreateDisplayName] = useState('') const [createDescription, setCreateDescription] = useState('') @@ -179,7 +193,7 @@ export function CredentialsManager() { const [createEnvValue, setCreateEnvValue] = useState('') const [createOAuthProviderId, setCreateOAuthProviderId] = useState('') const [createSecretInputMode, setCreateSecretInputMode] = useState('single') - const [createBulkText, setCreateBulkText] = useState('') + const [createBulkEntries, setCreateBulkEntries] = useState([]) const [createError, setCreateError] = useState(null) const [detailsError, setDetailsError] = useState(null) const [selectedEnvValueDraft, setSelectedEnvValueDraft] = useState('') @@ -188,6 +202,9 @@ export function CredentialsManager() { const [selectedDisplayNameDraft, setSelectedDisplayNameDraft] = useState('') const [showCreateOAuthRequiredModal, setShowCreateOAuthRequiredModal] = useState(false) const [copyIdSuccess, setCopyIdSuccess] = useState(false) + const [credentialToDelete, setCredentialToDelete] = useState(null) + const [showDeleteConfirmDialog, setShowDeleteConfirmDialog] = useState(false) + const [deleteError, setDeleteError] = useState(null) const { data: session } = useSession() const currentUserId = session?.user?.id || '' @@ -261,6 +278,7 @@ export function CredentialsManager() { oauthConnections.map((service) => ({ value: service.providerId, label: service.name, + icon: getServiceConfigByProviderId(service.providerId)?.icon, })), [oauthConnections] ) @@ -533,14 +551,14 @@ export function CredentialsManager() { }, [selectedCredential]) const resetCreateForm = () => { - setCreateType('oauth') + setCreateType('secret') setCreateSecretScope('workspace') setCreateSecretInputMode('single') setCreateDisplayName('') setCreateDescription('') setCreateEnvKey('') setCreateEnvValue('') - setCreateBulkText('') + setCreateBulkEntries([]) setCreateOAuthProviderId('') setCreateError(null) setShowCreateOAuthRequiredModal(false) @@ -639,7 +657,7 @@ export function CredentialsManager() { setShowCreateModal(false) resetCreateForm() } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Failed to create credential' + const message = error instanceof Error ? error.message : 'Failed to create secret' setCreateError(message) logger.error('Failed to create credential', error) } @@ -649,14 +667,40 @@ export function CredentialsManager() { if (!workspaceId) return setCreateError(null) - const { entries, errors } = parseEnvText(createBulkText) - if (errors.length > 0) { - setCreateError(errors.join('\n')) + const entries = createBulkEntries + .map((e) => ({ key: e.key.trim(), value: e.value.trim() })) + .filter((e) => e.key || e.value) + + if (entries.length === 0) { + setCreateError('Add at least one secret.') return } - if (entries.length === 0) { - setCreateError('No valid KEY=VALUE pairs found. Add one per line, e.g. API_KEY=sk-abc123') + const errors: string[] = [] + const seenKeys = new Set() + for (let i = 0; i < entries.length; i++) { + const { key, value } = entries[i] + if (!key) { + errors.push(`Row ${i + 1}: empty key`) + continue + } + if (!isValidEnvVarName(key)) { + errors.push(`Row ${i + 1}: "${key}" must contain only letters, numbers, and underscores`) + continue + } + if (!value) { + errors.push(`Row ${i + 1}: "${key}" has an empty value`) + continue + } + if (seenKeys.has(key.toUpperCase())) { + errors.push(`Row ${i + 1}: duplicate key "${key}"`) + continue + } + seenKeys.add(key.toUpperCase()) + } + + if (errors.length > 0) { + setCreateError(errors.join('\n')) return } @@ -759,16 +803,48 @@ export function CredentialsManager() { } } - const handleDeleteCredential = async () => { - if (!selectedCredential) return - if (selectedCredential.type === 'oauth') { - await handleDisconnectSelectedCredential() - return - } + const handleDeleteClick = (credential: WorkspaceCredential) => { + setCredentialToDelete(credential) + setDeleteError(null) + setShowDeleteConfirmDialog(true) + } + + const handleConfirmDelete = async () => { + if (!credentialToDelete) return + setDeleteError(null) + try { - await deleteCredential.mutateAsync(selectedCredential.id) - setSelectedCredentialId(null) + if (credentialToDelete.type === 'oauth') { + if (!credentialToDelete.accountId || !credentialToDelete.providerId) { + const errorMessage = + 'Cannot disconnect: missing account information. Please try reconnecting this credential first.' + setDeleteError(errorMessage) + logger.error('Cannot disconnect OAuth credential: missing accountId or providerId') + return + } + await disconnectOAuthService.mutateAsync({ + provider: credentialToDelete.providerId.split('-')[0] || credentialToDelete.providerId, + providerId: credentialToDelete.providerId, + serviceId: credentialToDelete.providerId, + accountId: credentialToDelete.accountId, + }) + await refetchCredentials() + window.dispatchEvent( + new CustomEvent('oauth-credentials-updated', { + detail: { providerId: credentialToDelete.providerId, workspaceId }, + }) + ) + } else { + await deleteCredential.mutateAsync(credentialToDelete.id) + } + if (selectedCredentialId === credentialToDelete.id) { + setSelectedCredentialId(null) + } + setShowDeleteConfirmDialog(false) + setCredentialToDelete(null) } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to delete credential' + setDeleteError(message) logger.error('Failed to delete credential', error) } } @@ -851,31 +927,6 @@ export function CredentialsManager() { } } - const handleDisconnectSelectedCredential = async () => { - if (!selectedCredential || selectedCredential.type !== 'oauth' || !selectedCredential.accountId) - return - if (!selectedCredential.providerId) return - - try { - await disconnectOAuthService.mutateAsync({ - provider: selectedCredential.providerId.split('-')[0] || selectedCredential.providerId, - providerId: selectedCredential.providerId, - serviceId: selectedCredential.providerId, - accountId: selectedCredential.accountId, - }) - - setSelectedCredentialId(null) - await refetchCredentials() - window.dispatchEvent( - new CustomEvent('oauth-credentials-updated', { - detail: { providerId: selectedCredential.providerId, workspaceId }, - }) - ) - } catch (error) { - logger.error('Failed to disconnect credential account', error) - } - } - const handleReconnectOAuth = async () => { if ( !selectedCredential || @@ -964,81 +1015,430 @@ export function CredentialsManager() { } } - return ( -
-
-
-
- - setSearchTerm(event.target.value)} - placeholder='Search credentials...' - className='pl-[32px]' - /> -
- -
- -
- {credentialsLoading ? ( -
- - - -
- ) : sortedCredentials.length === 0 ? ( -
- No credentials available for this workspace. + const hasCredentials = credentials && credentials.length > 0 + const showNoResults = + searchTerm.trim() && sortedCredentials.length === 0 && credentials.length > 0 + + const createModalJsx = ( + { + setShowCreateModal(open) + if (!open) resetCreateForm() + }} + > + + Create Secret + +
+
+ +
+ ({ + value: option.value, + label: option.label, + }))} + value={ + createTypeOptions.find((option) => option.value === createType)?.label || '' + } + selectedValue={createType} + onChange={(value) => { + setCreateType(value as CreateCredentialType) + setCreateError(null) + }} + placeholder='Select type' + /> +
- ) : ( -
- {sortedCredentials.map((credential) => ( -