feat: Add clear credentials button for each AI provider

- Add clear_anthropic_key, clear_openai_key, clear_deepseek_key, clear_ollama_url flags to API
- Backend handles clearing with confirmation prompt
- Each provider accordion shows Test and Clear buttons when configured
- Clear button requires confirmation before removing credentials
- Frontend automatically refreshes settings after clearing
This commit is contained in:
rcourtman 2025-12-11 18:24:25 +00:00
parent 597527fc04
commit bf92c0510a
3 changed files with 134 additions and 36 deletions

View file

@ -341,6 +341,41 @@ export const AISettings: Component = () => {
}
};
const handleClearProvider = async (provider: string) => {
if (!confirm(`Clear ${provider} credentials? You'll need to re-enter them to use this provider.`)) {
return;
}
setSaving(true);
try {
const clearPayload: Record<string, boolean> = {};
if (provider === 'anthropic') clearPayload.clear_anthropic_key = true;
if (provider === 'openai') clearPayload.clear_openai_key = true;
if (provider === 'deepseek') clearPayload.clear_deepseek_key = true;
if (provider === 'ollama') clearPayload.clear_ollama_url = true;
await AIAPI.updateSettings(clearPayload);
// Reload settings to reflect the change
const newSettings = await AIAPI.getSettings();
setSettings(newSettings);
// Clear the local form field
if (provider === 'anthropic') setForm('anthropicApiKey', '');
if (provider === 'openai') setForm('openaiApiKey', '');
if (provider === 'deepseek') setForm('deepseekApiKey', '');
if (provider === 'ollama') setForm('ollamaBaseUrl', '');
notificationStore.success(`${provider} credentials cleared`);
} catch (error) {
logger.error(`[AISettings] Clear ${provider} failed:`, error);
const message = error instanceof Error ? error.message : 'Failed to clear credentials';
notificationStore.error(message);
} finally {
setSaving(false);
}
};
// OAuth handlers removed - OAuth is currently unavailable from Anthropic for third-party apps
// When OAuth becomes available, handlers can be added back to the Anthropic accordion section
@ -589,14 +624,25 @@ export const AISettings: Component = () => {
<a href="https://console.anthropic.com/settings/keys" target="_blank" rel="noopener" class="text-purple-600 hover:underline">Get API key </a>
</p>
<Show when={settings()?.anthropic_configured}>
<button
type="button"
onClick={() => handleTestProvider('anthropic')}
disabled={testingProvider() === 'anthropic'}
class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800 disabled:opacity-50"
>
{testingProvider() === 'anthropic' ? 'Testing...' : 'Test'}
</button>
<div class="flex gap-1">
<button
type="button"
onClick={() => handleTestProvider('anthropic')}
disabled={testingProvider() === 'anthropic' || saving()}
class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800 disabled:opacity-50"
>
{testingProvider() === 'anthropic' ? 'Testing...' : 'Test'}
</button>
<button
type="button"
onClick={() => handleClearProvider('anthropic')}
disabled={saving()}
class="px-2 py-1 text-xs bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300 rounded hover:bg-red-200 dark:hover:bg-red-800 disabled:opacity-50"
title="Clear API key"
>
Clear
</button>
</div>
</Show>
</div>
<Show when={providerTestResult()?.provider === 'anthropic'}>
@ -654,14 +700,25 @@ export const AISettings: Component = () => {
<a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener" class="text-purple-600 hover:underline">Get API key </a>
</p>
<Show when={settings()?.openai_configured}>
<button
type="button"
onClick={() => handleTestProvider('openai')}
disabled={testingProvider() === 'openai'}
class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800 disabled:opacity-50"
>
{testingProvider() === 'openai' ? 'Testing...' : 'Test'}
</button>
<div class="flex gap-1">
<button
type="button"
onClick={() => handleTestProvider('openai')}
disabled={testingProvider() === 'openai' || saving()}
class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800 disabled:opacity-50"
>
{testingProvider() === 'openai' ? 'Testing...' : 'Test'}
</button>
<button
type="button"
onClick={() => handleClearProvider('openai')}
disabled={saving()}
class="px-2 py-1 text-xs bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300 rounded hover:bg-red-200 dark:hover:bg-red-800 disabled:opacity-50"
title="Clear API key"
>
Clear
</button>
</div>
</Show>
</div>
<Show when={providerTestResult()?.provider === 'openai'}>
@ -711,14 +768,25 @@ export const AISettings: Component = () => {
<a href="https://platform.deepseek.com/api_keys" target="_blank" rel="noopener" class="text-purple-600 hover:underline">Get API key </a>
</p>
<Show when={settings()?.deepseek_configured}>
<button
type="button"
onClick={() => handleTestProvider('deepseek')}
disabled={testingProvider() === 'deepseek'}
class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800 disabled:opacity-50"
>
{testingProvider() === 'deepseek' ? 'Testing...' : 'Test'}
</button>
<div class="flex gap-1">
<button
type="button"
onClick={() => handleTestProvider('deepseek')}
disabled={testingProvider() === 'deepseek' || saving()}
class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800 disabled:opacity-50"
>
{testingProvider() === 'deepseek' ? 'Testing...' : 'Test'}
</button>
<button
type="button"
onClick={() => handleClearProvider('deepseek')}
disabled={saving()}
class="px-2 py-1 text-xs bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300 rounded hover:bg-red-200 dark:hover:bg-red-800 disabled:opacity-50"
title="Clear API key"
>
Clear
</button>
</div>
</Show>
</div>
<Show when={providerTestResult()?.provider === 'deepseek'}>
@ -769,14 +837,25 @@ export const AISettings: Component = () => {
<span class="text-gray-400"> · Free & local</span>
</p>
<Show when={settings()?.ollama_configured}>
<button
type="button"
onClick={() => handleTestProvider('ollama')}
disabled={testingProvider() === 'ollama'}
class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800 disabled:opacity-50"
>
{testingProvider() === 'ollama' ? 'Testing...' : 'Test'}
</button>
<div class="flex gap-1">
<button
type="button"
onClick={() => handleTestProvider('ollama')}
disabled={testingProvider() === 'ollama' || saving()}
class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800 disabled:opacity-50"
>
{testingProvider() === 'ollama' ? 'Testing...' : 'Test'}
</button>
<button
type="button"
onClick={() => handleClearProvider('ollama')}
disabled={saving()}
class="px-2 py-1 text-xs bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300 rounded hover:bg-red-200 dark:hover:bg-red-800 disabled:opacity-50"
title="Clear Ollama URL"
>
Clear
</button>
</div>
</Show>
</div>
<Show when={providerTestResult()?.provider === 'ollama'}>

View file

@ -61,6 +61,11 @@ export interface AISettingsUpdateRequest {
deepseek_api_key?: string; // Set DeepSeek API key
ollama_base_url?: string; // Set Ollama server URL
openai_base_url?: string; // Set custom OpenAI base URL
// Clear flags for removing credentials
clear_anthropic_key?: boolean; // Clear Anthropic API key
clear_openai_key?: boolean; // Clear OpenAI API key
clear_deepseek_key?: boolean; // Clear DeepSeek API key
clear_ollama_url?: boolean; // Clear Ollama URL
}

View file

@ -152,6 +152,11 @@ type AISettingsUpdateRequest struct {
DeepSeekAPIKey *string `json:"deepseek_api_key,omitempty"` // Set DeepSeek API key
OllamaBaseURL *string `json:"ollama_base_url,omitempty"` // Set Ollama server URL
OpenAIBaseURL *string `json:"openai_base_url,omitempty"` // Set custom OpenAI base URL
// Clear flags for removing credentials
ClearAnthropicKey *bool `json:"clear_anthropic_key,omitempty"` // Clear Anthropic API key
ClearOpenAIKey *bool `json:"clear_openai_key,omitempty"` // Clear OpenAI API key
ClearDeepSeekKey *bool `json:"clear_deepseek_key,omitempty"` // Clear DeepSeek API key
ClearOllamaURL *bool `json:"clear_ollama_url,omitempty"` // Clear Ollama URL
}
// HandleGetAISettings returns the current AI settings (GET /api/settings/ai)
@ -335,16 +340,25 @@ func (h *AISettingsHandler) HandleUpdateAISettings(w http.ResponseWriter, r *htt
}
// Handle multi-provider credentials
if req.AnthropicAPIKey != nil {
// Clear flags take priority over setting new values
if req.ClearAnthropicKey != nil && *req.ClearAnthropicKey {
settings.AnthropicAPIKey = ""
} else if req.AnthropicAPIKey != nil {
settings.AnthropicAPIKey = strings.TrimSpace(*req.AnthropicAPIKey)
}
if req.OpenAIAPIKey != nil {
if req.ClearOpenAIKey != nil && *req.ClearOpenAIKey {
settings.OpenAIAPIKey = ""
} else if req.OpenAIAPIKey != nil {
settings.OpenAIAPIKey = strings.TrimSpace(*req.OpenAIAPIKey)
}
if req.DeepSeekAPIKey != nil {
if req.ClearDeepSeekKey != nil && *req.ClearDeepSeekKey {
settings.DeepSeekAPIKey = ""
} else if req.DeepSeekAPIKey != nil {
settings.DeepSeekAPIKey = strings.TrimSpace(*req.DeepSeekAPIKey)
}
if req.OllamaBaseURL != nil {
if req.ClearOllamaURL != nil && *req.ClearOllamaURL {
settings.OllamaBaseURL = ""
} else if req.OllamaBaseURL != nil {
settings.OllamaBaseURL = strings.TrimSpace(*req.OllamaBaseURL)
}
if req.OpenAIBaseURL != nil {