feat: add user secrets management panel (#8473)

## 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>
This commit is contained in:
Luke Mino-Altherr
2026-02-02 17:28:58 -08:00
committed by GitHub
parent 139ee32d78
commit bc19bb60fb
22 changed files with 1693 additions and 54 deletions

View File

@@ -83,7 +83,7 @@
<script setup lang="ts">
import type { AssetMetadata } from '@/platform/assets/schemas/assetSchema'
const { result } = defineProps<{
defineProps<{
result: 'processing' | 'success' | 'error'
error?: string
metadata?: AssetMetadata

View File

@@ -45,19 +45,13 @@
</div>
<div class="flex flex-col gap-2">
<div class="relative">
<InputText
v-model="url"
autofocus
:placeholder="$t('assetBrowser.genericLinkPlaceholder')"
class="w-full border-0 bg-secondary-background p-4 pr-10"
data-attr="upload-model-step1-url-input"
/>
<i
v-if="isValidUrl"
class="icon-[lucide--circle-check-big] absolute top-1/2 right-3 size-5 -translate-y-1/2 text-green-500"
/>
</div>
<InputText
v-model="url"
autofocus
:placeholder="$t('assetBrowser.genericLinkPlaceholder')"
class="w-full border-0 bg-secondary-background p-4"
data-attr="upload-model-step1-url-input"
/>
<p v-if="error" class="text-sm text-error">
{{ error }}
</p>
@@ -73,8 +67,24 @@
</div>
</div>
<div class="text-sm text-muted">
{{ $t('assetBrowser.uploadModelHelpFooterText') }}
<div class="flex flex-col gap-6 text-sm text-muted-foreground">
<div v-if="showSecretsHint">
<i18n-t keypath="assetBrowser.apiKeyHint" tag="span">
<template #link>
<Button
variant="textonly"
size="unset"
class="text-muted-foreground underline p-0"
@click="openSecretsSettings"
>
{{ $t('assetBrowser.apiKeyHintLink') }}
</Button>
</template>
</i18n-t>
</div>
<div>
{{ $t('assetBrowser.uploadModelHelpFooterText') }}
</div>
</div>
</div>
</template>
@@ -83,12 +93,18 @@
import InputText from 'primevue/inputtext'
import { computed } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { civitaiImportSource } from '@/platform/assets/importSources/civitaiImportSource'
import { huggingfaceImportSource } from '@/platform/assets/importSources/huggingfaceImportSource'
import { validateSourceUrl } from '@/platform/assets/utils/importSourceUtil'
import { useDialogService } from '@/services/dialogService'
const { flags } = useFeatureFlags()
const dialogService = useDialogService()
const showSecretsHint = computed(() => flags.userSecretsEnabled)
function openSecretsSettings() {
dialogService.showSettingsDialog('secrets')
}
const props = defineProps<{
modelValue: string
@@ -104,14 +120,6 @@ const url = computed({
set: (value: string) => emit('update:modelValue', value)
})
const importSources = [civitaiImportSource, huggingfaceImportSource]
const isValidUrl = computed(() => {
const trimmedUrl = url.value.trim()
if (!trimmedUrl) return false
return importSources.some((source) => validateSourceUrl(trimmedUrl, source))
})
const civitaiIcon = '/assets/images/civitai.svg'
const civitaiUrl = 'https://civitai.com/models'
const huggingFaceIcon = '/assets/images/hf-logo.svg'

View File

@@ -38,19 +38,13 @@
}}</span>
</template>
</i18n-t>
<div class="relative">
<InputText
v-model="url"
autofocus
:placeholder="$t('assetBrowser.civitaiLinkPlaceholder')"
class="w-full border-0 bg-secondary-background p-4 pr-10"
data-attr="upload-model-step1-url-input"
/>
<i
v-if="isValidUrl"
class="icon-[lucide--circle-check-big] absolute top-1/2 right-3 size-5 -translate-y-1/2 text-green-500"
/>
</div>
<InputText
v-model="url"
autofocus
:placeholder="$t('assetBrowser.civitaiLinkPlaceholder')"
class="w-full border-0 bg-secondary-background p-4"
data-attr="upload-model-step1-url-input"
/>
<p v-if="error" class="text-sm text-error">
{{ error }}
</p>
@@ -73,6 +67,21 @@
</a>
</template>
</i18n-t>
<div v-if="showSecretsHint" class="text-sm text-muted">
<i18n-t keypath="assetBrowser.apiKeyHint" tag="span">
<template #link>
<Button
variant="textonly"
size="unset"
class="text-muted underline p-0"
@click="openSecretsSettings"
>
{{ $t('assetBrowser.apiKeyHintLink') }}
</Button>
</template>
</i18n-t>
</div>
</div>
</div>
</template>
@@ -81,21 +90,22 @@
import InputText from 'primevue/inputtext'
import { computed } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { civitaiImportSource } from '@/platform/assets/importSources/civitaiImportSource'
import { validateSourceUrl } from '@/platform/assets/utils/importSourceUtil'
import { useDialogService } from '@/services/dialogService'
const { flags } = useFeatureFlags()
const dialogService = useDialogService()
const showSecretsHint = computed(() => flags.userSecretsEnabled)
function openSecretsSettings() {
dialogService.showSettingsDialog('secrets')
}
defineProps<{
error?: string
}>()
const url = defineModel<string>({ required: true })
const isValidUrl = computed(() => {
const trimmedUrl = url.value.trim()
if (!trimmedUrl) return false
return validateSourceUrl(trimmedUrl, civitaiImportSource)
})
</script>