diff --git a/internal/providers/service.go b/internal/providers/service.go index 3eec8824..b1a205f0 100644 --- a/internal/providers/service.go +++ b/internal/providers/service.go @@ -142,10 +142,7 @@ func (s *Service) Update(ctx context.Context, id string, req UpdateRequest) (Get baseURL = *req.BaseURL } - apiKey := existing.ApiKey - if req.APIKey != nil { - apiKey = *req.APIKey - } + apiKey := resolveUpdatedAPIKey(existing.ApiKey, req.APIKey) metadata := existing.Metadata if req.Metadata != nil { @@ -253,3 +250,15 @@ func maskAPIKey(apiKey string) string { } return apiKey[:8] + strings.Repeat("*", len(apiKey)-8) } + +// resolveUpdatedAPIKey keeps the original key when the request value matches the masked version. +// This prevents masked placeholder values from overwriting the real stored credential. +func resolveUpdatedAPIKey(existing string, updated *string) string { + if updated == nil { + return existing + } + if *updated == maskAPIKey(existing) { + return existing + } + return *updated +} diff --git a/internal/providers/service_test.go b/internal/providers/service_test.go new file mode 100644 index 00000000..a4426d2f --- /dev/null +++ b/internal/providers/service_test.go @@ -0,0 +1,40 @@ +package providers + +import "testing" + +func TestResolveUpdatedAPIKey(t *testing.T) { + t.Parallel() + + existing := "sk-1234567890abcdef" + masked := maskAPIKey(existing) + + t.Run("nil update keeps existing", func(t *testing.T) { + t.Parallel() + if got := resolveUpdatedAPIKey(existing, nil); got != existing { + t.Fatalf("expected existing key, got %q", got) + } + }) + + t.Run("masked update keeps existing", func(t *testing.T) { + t.Parallel() + if got := resolveUpdatedAPIKey(existing, &masked); got != existing { + t.Fatalf("expected existing key, got %q", got) + } + }) + + t.Run("new key replaces existing", func(t *testing.T) { + t.Parallel() + next := "sk-new-secret" + if got := resolveUpdatedAPIKey(existing, &next); got != next { + t.Fatalf("expected new key, got %q", got) + } + }) + + t.Run("empty update clears key", func(t *testing.T) { + t.Parallel() + empty := "" + if got := resolveUpdatedAPIKey(existing, &empty); got != empty { + t.Fatalf("expected empty key, got %q", got) + } + }) +} diff --git a/packages/web/src/pages/models/components/provider-form.vue b/packages/web/src/pages/models/components/provider-form.vue index a18a7cbc..4b6c8538 100644 --- a/packages/web/src/pages/models/components/provider-form.vue +++ b/packages/web/src/pages/models/components/provider-form.vue @@ -32,8 +32,8 @@ @@ -109,7 +109,7 @@ const props = defineProps<{ }>() const emit = defineEmits<{ - submit: [values: typeof form.values] + submit: [values: Record] delete: [] }>() @@ -117,7 +117,7 @@ const providerSchema = toTypedSchema(z.object({ name: z.string().min(1), base_url: z.string().min(1), client_type: z.string().min(1), - api_key: z.string().min(1), + api_key: z.string().optional(), metadata: z.object({ additionalProp1: z.object({}), }), @@ -133,23 +133,40 @@ watch(() => props.provider, (newVal) => { name: newVal.name, base_url: newVal.base_url, client_type: newVal.client_type, - api_key: newVal.api_key, + // Keep key input empty by default so masked placeholders are never submitted back. + api_key: '', }) } }, { immediate: true }) const hasChanges = computed(() => { const raw = props.provider - return JSON.stringify(form.values) !== JSON.stringify({ + const baseChanged = JSON.stringify({ + name: form.values.name, + base_url: form.values.base_url, + client_type: form.values.client_type, + metadata: form.values.metadata, + }) !== JSON.stringify({ name: raw?.name, base_url: raw?.base_url, client_type: raw?.client_type, - api_key: raw?.api_key, metadata: { additionalProp1: {} }, }) + + const apiKeyChanged = Boolean(form.values.api_key && form.values.api_key.trim() !== '') + return baseChanged || apiKeyChanged }) const editProvider = form.handleSubmit(async (value) => { - emit('submit', value) + const payload: Record = { + name: value.name, + base_url: value.base_url, + client_type: value.client_type, + metadata: value.metadata, + } + if (value.api_key && value.api_key.trim() !== '') { + payload.api_key = value.api_key + } + emit('submit', payload) })