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:
parent
597527fc04
commit
bf92c0510a
3 changed files with 134 additions and 36 deletions
|
|
@ -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
|
// 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
|
// 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>
|
<a href="https://console.anthropic.com/settings/keys" target="_blank" rel="noopener" class="text-purple-600 hover:underline">Get API key →</a>
|
||||||
</p>
|
</p>
|
||||||
<Show when={settings()?.anthropic_configured}>
|
<Show when={settings()?.anthropic_configured}>
|
||||||
<button
|
<div class="flex gap-1">
|
||||||
type="button"
|
<button
|
||||||
onClick={() => handleTestProvider('anthropic')}
|
type="button"
|
||||||
disabled={testingProvider() === 'anthropic'}
|
onClick={() => handleTestProvider('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"
|
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>
|
{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>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={providerTestResult()?.provider === 'anthropic'}>
|
<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>
|
<a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener" class="text-purple-600 hover:underline">Get API key →</a>
|
||||||
</p>
|
</p>
|
||||||
<Show when={settings()?.openai_configured}>
|
<Show when={settings()?.openai_configured}>
|
||||||
<button
|
<div class="flex gap-1">
|
||||||
type="button"
|
<button
|
||||||
onClick={() => handleTestProvider('openai')}
|
type="button"
|
||||||
disabled={testingProvider() === 'openai'}
|
onClick={() => handleTestProvider('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"
|
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>
|
{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>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={providerTestResult()?.provider === 'openai'}>
|
<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>
|
<a href="https://platform.deepseek.com/api_keys" target="_blank" rel="noopener" class="text-purple-600 hover:underline">Get API key →</a>
|
||||||
</p>
|
</p>
|
||||||
<Show when={settings()?.deepseek_configured}>
|
<Show when={settings()?.deepseek_configured}>
|
||||||
<button
|
<div class="flex gap-1">
|
||||||
type="button"
|
<button
|
||||||
onClick={() => handleTestProvider('deepseek')}
|
type="button"
|
||||||
disabled={testingProvider() === 'deepseek'}
|
onClick={() => handleTestProvider('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"
|
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>
|
{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>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={providerTestResult()?.provider === 'deepseek'}>
|
<Show when={providerTestResult()?.provider === 'deepseek'}>
|
||||||
|
|
@ -769,14 +837,25 @@ export const AISettings: Component = () => {
|
||||||
<span class="text-gray-400"> · Free & local</span>
|
<span class="text-gray-400"> · Free & local</span>
|
||||||
</p>
|
</p>
|
||||||
<Show when={settings()?.ollama_configured}>
|
<Show when={settings()?.ollama_configured}>
|
||||||
<button
|
<div class="flex gap-1">
|
||||||
type="button"
|
<button
|
||||||
onClick={() => handleTestProvider('ollama')}
|
type="button"
|
||||||
disabled={testingProvider() === 'ollama'}
|
onClick={() => handleTestProvider('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"
|
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>
|
{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>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={providerTestResult()?.provider === 'ollama'}>
|
<Show when={providerTestResult()?.provider === 'ollama'}>
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,11 @@ export interface AISettingsUpdateRequest {
|
||||||
deepseek_api_key?: string; // Set DeepSeek API key
|
deepseek_api_key?: string; // Set DeepSeek API key
|
||||||
ollama_base_url?: string; // Set Ollama server URL
|
ollama_base_url?: string; // Set Ollama server URL
|
||||||
openai_base_url?: string; // Set custom OpenAI base 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,11 @@ type AISettingsUpdateRequest struct {
|
||||||
DeepSeekAPIKey *string `json:"deepseek_api_key,omitempty"` // Set DeepSeek API key
|
DeepSeekAPIKey *string `json:"deepseek_api_key,omitempty"` // Set DeepSeek API key
|
||||||
OllamaBaseURL *string `json:"ollama_base_url,omitempty"` // Set Ollama server URL
|
OllamaBaseURL *string `json:"ollama_base_url,omitempty"` // Set Ollama server URL
|
||||||
OpenAIBaseURL *string `json:"openai_base_url,omitempty"` // Set custom OpenAI base 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)
|
// 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
|
// 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)
|
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)
|
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)
|
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)
|
settings.OllamaBaseURL = strings.TrimSpace(*req.OllamaBaseURL)
|
||||||
}
|
}
|
||||||
if req.OpenAIBaseURL != nil {
|
if req.OpenAIBaseURL != nil {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue