mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 15:40:10 +00:00
## Summary Add a Secrets panel to user settings for managing third-party API keys (HuggingFace, Civitai). Secrets are encrypted server-side; plaintext values are never returned. ## Changes - Add `SecretsPanel` to settings for managing third-party API keys - Create `secretsApi` service following the `workspaceApi` pattern - Add `SecretListItem` and `SecretFormDialog` components - Add `user_secrets_enabled` feature flag - Support HuggingFace and Civitai providers - Add i18n translations for secrets UI ## Files Added - `src/platform/secrets/types.ts` - TypeScript types - `src/platform/secrets/api/secretsApi.ts` - Axios-based API service - `src/platform/secrets/components/SecretsPanel.vue` - Main settings panel - `src/platform/secrets/components/SecretListItem.vue` - Individual secret row - `src/platform/secrets/components/SecretFormDialog.vue` - Create/edit dialog ## Files Modified - `src/platform/remoteConfig/types.ts` - Add `user_secrets_enabled` flag type - `src/composables/useFeatureFlags.ts` - Add flag getter - `src/platform/settings/composables/useSettingUI.ts` - Integrate secrets panel - `src/locales/en/main.json` - Add translations ## Testing Panel appears in Settings under: - "Workspace" group when team workspaces is enabled - "Account" group in legacy mode Only visible when user is logged in AND `user_secrets_enabled` feature flag is enabled. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8473-feat-add-user-secrets-management-panel-2f86d73d36508187b4a1ed04ce07ce51) by [Unito](https://www.unito.io) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Secrets management UI in Settings with create/edit/delete for API keys/secrets; settings dialog entry and contextual Secrets hint in upload/import flows (feature-flag gated). * **APIs** * Added backend-facing secrets CRUD surface and client-side form/composable support for managing secrets. * **Localization** * New English translations for Secrets UI and many expanded asset import/upload error and hint messages. * **Tests** * Comprehensive unit tests for secrets UI, form flows, and composables. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Amp <amp@ampcode.com>
172 lines
4.1 KiB
TypeScript
172 lines
4.1 KiB
TypeScript
import { whenever } from '@vueuse/core'
|
|
import type { MaybeRefOrGetter } from 'vue'
|
|
import { computed, reactive, ref, toValue } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import { createSecret, SecretsApiError, updateSecret } from '../api/secretsApi'
|
|
import { SECRET_PROVIDERS } from '../providers'
|
|
import type { SecretErrorCode, SecretMetadata, SecretProvider } from '../types'
|
|
|
|
interface SecretFormState {
|
|
name: string
|
|
secretValue: string
|
|
provider: SecretProvider | null
|
|
}
|
|
|
|
interface SecretFormErrors {
|
|
name: string
|
|
secretValue: string
|
|
provider: string
|
|
}
|
|
|
|
interface ProviderOption {
|
|
label: string
|
|
value: SecretProvider
|
|
disabled: boolean
|
|
}
|
|
|
|
interface UseSecretFormOptions {
|
|
mode: 'create' | 'edit'
|
|
secret?: MaybeRefOrGetter<SecretMetadata | undefined>
|
|
existingProviders: MaybeRefOrGetter<SecretProvider[]>
|
|
visible: { value: boolean }
|
|
onSaved: () => void
|
|
}
|
|
|
|
export function useSecretForm(options: UseSecretFormOptions) {
|
|
const { t } = useI18n()
|
|
const {
|
|
mode,
|
|
secret: secretRef,
|
|
existingProviders,
|
|
visible,
|
|
onSaved
|
|
} = options
|
|
|
|
const loading = ref(false)
|
|
const apiErrorCode = ref<SecretErrorCode | null>(null)
|
|
const apiErrorMessage = ref<string | null>(null)
|
|
|
|
const form = reactive<SecretFormState>({
|
|
name: '',
|
|
secretValue: '',
|
|
provider: null
|
|
})
|
|
|
|
const errors = reactive<SecretFormErrors>({
|
|
name: '',
|
|
secretValue: '',
|
|
provider: ''
|
|
})
|
|
|
|
const providerOptions = computed<ProviderOption[]>(() =>
|
|
SECRET_PROVIDERS.map((p) => ({
|
|
label: p.label,
|
|
value: p.value,
|
|
disabled:
|
|
mode === 'edit' ? false : toValue(existingProviders).includes(p.value)
|
|
}))
|
|
)
|
|
|
|
const apiError = computed(() => {
|
|
if (!apiErrorCode.value && !apiErrorMessage.value) return null
|
|
switch (apiErrorCode.value) {
|
|
case 'DUPLICATE_NAME':
|
|
return t('secrets.errors.duplicateName')
|
|
case 'DUPLICATE_PROVIDER':
|
|
return t('secrets.errors.duplicateProvider')
|
|
default:
|
|
return apiErrorMessage.value
|
|
}
|
|
})
|
|
|
|
function resetForm() {
|
|
const secret = toValue(secretRef)
|
|
if (mode === 'edit' && secret) {
|
|
form.name = secret.name
|
|
form.provider = secret.provider ?? null
|
|
form.secretValue = ''
|
|
} else {
|
|
form.name = ''
|
|
form.secretValue = ''
|
|
form.provider = null
|
|
}
|
|
errors.name = ''
|
|
errors.secretValue = ''
|
|
errors.provider = ''
|
|
apiErrorCode.value = null
|
|
apiErrorMessage.value = null
|
|
}
|
|
|
|
whenever(() => visible.value, resetForm)
|
|
|
|
function validate(): boolean {
|
|
errors.name = ''
|
|
errors.secretValue = ''
|
|
errors.provider = ''
|
|
|
|
if (!form.name.trim()) {
|
|
errors.name = t('secrets.errors.nameRequired')
|
|
return false
|
|
}
|
|
if (form.name.length > 255) {
|
|
errors.name = t('secrets.errors.nameTooLong')
|
|
return false
|
|
}
|
|
if (!form.provider) {
|
|
errors.provider = t('secrets.errors.providerRequired')
|
|
return false
|
|
}
|
|
if (mode === 'create' && !form.secretValue) {
|
|
errors.secretValue = t('secrets.errors.secretValueRequired')
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
if (!validate()) return
|
|
|
|
loading.value = true
|
|
apiErrorCode.value = null
|
|
apiErrorMessage.value = null
|
|
|
|
try {
|
|
const secret = toValue(secretRef)
|
|
if (mode === 'create') {
|
|
await createSecret({
|
|
name: form.name.trim(),
|
|
secret_value: form.secretValue,
|
|
provider: form.provider!
|
|
})
|
|
} else if (secret) {
|
|
const updatePayload: { name: string; secret_value?: string } = {
|
|
name: form.name.trim()
|
|
}
|
|
if (form.secretValue) {
|
|
updatePayload.secret_value = form.secretValue
|
|
}
|
|
await updateSecret(secret.id, updatePayload)
|
|
}
|
|
onSaved()
|
|
visible.value = false
|
|
} catch (err) {
|
|
if (err instanceof SecretsApiError) {
|
|
apiErrorCode.value = err.code ?? null
|
|
apiErrorMessage.value = err.message
|
|
}
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
return {
|
|
form,
|
|
errors,
|
|
loading,
|
|
apiError,
|
|
providerOptions,
|
|
handleSubmit
|
|
}
|
|
}
|