fix: remove API Keys management section from Settings page

API keys are managed via admin CLI, not the web UI. Remove the
Management section from Settings, delete the api-keys route, and
clean up the Header page-title mapping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Algis Dumbris
2026-03-15 19:42:36 +02:00
co-authored by Claude Opus 4.6
parent 96d9fbd246
commit 6abd45eeff
4 changed files with 11 additions and 432 deletions
+11 -11
View File
@@ -8,29 +8,29 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Instrument+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link href="/_app/immutable/entry/start.DpHKCwmv.js" rel="modulepreload">
<link href="/_app/immutable/chunks/BRBotovi.js" rel="modulepreload">
<link href="/_app/immutable/chunks/DBeLgT1-.js" rel="modulepreload">
<link href="/_app/immutable/chunks/SAcaBy3_.js" rel="modulepreload">
<link href="/_app/immutable/chunks/DL-Ee-iM.js" rel="modulepreload">
<link href="/_app/immutable/chunks/BCvik_Lu.js" rel="modulepreload">
<link href="/_app/immutable/chunks/BdrVqzRy.js" rel="modulepreload">
<link href="/_app/immutable/entry/app.B_lhmyMs.js" rel="modulepreload">
<link href="/_app/immutable/entry/start.CyIe6_Q2.js" rel="modulepreload">
<link href="/_app/immutable/chunks/BoJy5JPd.js" rel="modulepreload">
<link href="/_app/immutable/chunks/BoEm3yK3.js" rel="modulepreload">
<link href="/_app/immutable/chunks/B9lFjOCO.js" rel="modulepreload">
<link href="/_app/immutable/chunks/_CYN1FQs.js" rel="modulepreload">
<link href="/_app/immutable/chunks/8mnKcOKb.js" rel="modulepreload">
<link href="/_app/immutable/chunks/DszstWrr.js" rel="modulepreload">
<link href="/_app/immutable/entry/app.CiwCeDom.js" rel="modulepreload">
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">
<script>
{
__sveltekit_vhg0t8 = {
__sveltekit_1mn75q7 = {
base: ""
};
const element = document.currentScript.parentElement;
Promise.all([
import("/_app/immutable/entry/start.DpHKCwmv.js"),
import("/_app/immutable/entry/app.B_lhmyMs.js")
import("/_app/immutable/entry/start.CyIe6_Q2.js"),
import("/_app/immutable/entry/app.CiwCeDom.js")
]).then(([kit, app]) => {
kit.start(app, element);
});
-1
View File
@@ -22,7 +22,6 @@
if (path.startsWith('/dm/')) return ($page.params.name ?? 'DM');
if (path.startsWith('/agents/')) return ($page.params.name ?? 'Agent');
if (path === '/agents') return 'Agents';
if (path === '/settings/api-keys') return 'API Keys';
if (path === '/settings') return 'Settings';
return 'SynapBus';
});
-23
View File
@@ -64,29 +64,6 @@
</div>
</div>
<!-- Quick links -->
<div class="card mb-5">
<div class="px-5 py-3 border-b border-border">
<h2 class="font-semibold text-sm text-text-primary font-display">Management</h2>
</div>
<div class="divide-y divide-border">
<a href="/settings/api-keys" class="flex items-center justify-between px-5 py-3 hover:bg-bg-tertiary/50 transition-colors">
<div class="flex items-center gap-3">
<svg class="w-4 h-4 text-text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
<div>
<p class="text-sm text-text-primary font-medium">API Keys</p>
<p class="text-xs text-text-secondary">Manage API keys for agent access</p>
</div>
</div>
<svg class="w-4 h-4 text-text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
</svg>
</a>
</div>
</div>
<!-- Change Password -->
<div class="card">
<div class="px-5 py-3 border-b border-border">
@@ -1,397 +0,0 @@
<script lang="ts">
import { apiKeys, agents as agentsApi } from '$lib/api/client';
let keys = $state<any[]>([]);
let agentList = $state<any[]>([]);
let loadingData = $state(true);
// Create form state
let showCreate = $state(false);
let creating = $state(false);
let createError = $state('');
let newKeyName = $state('');
let newKeyAgentId = $state<number | undefined>(undefined);
let newKeyPermRead = $state(true);
let newKeyPermWrite = $state(true);
let newKeyPermAdmin = $state(false);
let newKeyChannels = $state('');
let newKeyReadOnly = $state(false);
let newKeyExpiry = $state('');
// Created key display
let createdKey = $state<{ key: any; api_key: string; mcp_config: any } | null>(null);
let copied = $state(false);
// Revoke
let confirmRevokeId = $state<number | null>(null);
let revoking = $state(false);
let _initialized = $state(false);
$effect(() => {
if (!_initialized) {
_initialized = true;
loadData();
}
});
async function loadData() {
loadingData = true;
try {
const [keysRes, agRes] = await Promise.all([
apiKeys.list(),
agentsApi.list()
]);
keys = keysRes.keys ?? [];
agentList = agRes.agents ?? [];
} catch {
// handled
} finally {
loadingData = false;
}
}
async function handleCreate(e: SubmitEvent) {
e.preventDefault();
if (!newKeyName.trim()) {
createError = 'Key name is required';
return;
}
creating = true;
createError = '';
try {
const permissions: Record<string, boolean> = {};
if (newKeyPermRead) permissions.read = true;
if (newKeyPermWrite) permissions.write = true;
if (newKeyPermAdmin) permissions.admin = true;
const channels = newKeyChannels.trim()
? newKeyChannels.split(',').map((s) => s.trim()).filter(Boolean)
: undefined;
const res = await apiKeys.create({
name: newKeyName.trim(),
agent_id: newKeyAgentId,
permissions: Object.keys(permissions).length > 0 ? permissions : undefined,
allowed_channels: channels,
read_only: newKeyReadOnly || undefined,
expires_at: newKeyExpiry || undefined
});
createdKey = res;
newKeyName = '';
newKeyAgentId = undefined;
newKeyPermRead = true;
newKeyPermWrite = true;
newKeyPermAdmin = false;
newKeyChannels = '';
newKeyReadOnly = false;
newKeyExpiry = '';
showCreate = false;
await loadData();
} catch (err: any) {
createError = err.message || 'Failed to create API key';
} finally {
creating = false;
}
}
async function handleRevoke(id: number) {
revoking = true;
try {
await apiKeys.revoke(id);
confirmRevokeId = null;
await loadData();
} catch (err: any) {
alert(err.message || 'Failed to revoke key');
} finally {
revoking = false;
}
}
async function copyToClipboard(text: string) {
try {
await navigator.clipboard.writeText(text);
copied = true;
setTimeout(() => (copied = false), 2000);
} catch {
// fallback
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
copied = true;
setTimeout(() => (copied = false), 2000);
}
}
function formatDate(iso: string): string {
if (!iso) return '-';
return new Date(iso).toLocaleDateString([], { year: 'numeric', month: 'short', day: 'numeric' });
}
function getMcpConfig(apiKey: string): string {
return JSON.stringify({
mcpServers: {
synapbus: {
url: `${typeof window !== 'undefined' ? window.location.origin : 'http://localhost:8080'}/mcp`,
headers: {
Authorization: `Bearer ${apiKey}`
}
}
}
}, null, 2);
}
function getClaudeConfig(apiKey: string): string {
return JSON.stringify({
synapbus: {
type: 'streamableHttp',
url: `${typeof window !== 'undefined' ? window.location.origin : 'http://localhost:8080'}/mcp`,
headers: {
Authorization: `Bearer ${apiKey}`
}
}
}, null, 2);
}
</script>
<div class="p-5 max-w-5xl">
<div class="flex items-center justify-between mb-5">
<div class="flex items-center gap-3">
<a href="/settings" class="text-text-secondary hover:text-text-primary transition-colors" aria-label="Back to settings">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
</svg>
</a>
<h1 class="text-xl font-bold text-text-primary font-display">API Keys</h1>
</div>
<button
class="btn-primary flex items-center gap-1.5"
onclick={() => { showCreate = !showCreate; createdKey = null; }}
>
{#if showCreate}
Cancel
{:else}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
</svg>
Create API Key
{/if}
</button>
</div>
<!-- Created key display -->
{#if createdKey}
<div class="card mb-5 border-accent-yellow/30">
<div class="px-5 py-3 border-b border-border bg-accent-yellow/5">
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-accent-yellow" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<span class="text-sm font-semibold text-accent-yellow">This key will only be shown once. Save it now.</span>
</div>
</div>
<div class="p-5 space-y-4">
<!-- The key -->
<div>
<p class="block text-xs font-medium text-text-secondary mb-1.5">API Key</p>
<div class="flex items-center gap-2">
<code class="flex-1 p-3 bg-bg-primary rounded-lg text-sm font-mono text-accent-green break-all select-all border border-accent-green/20">{createdKey.api_key}</code>
<button
class="flex-shrink-0 p-2.5 rounded-lg bg-bg-tertiary hover:bg-border-active text-text-secondary hover:text-text-primary transition-all"
onclick={() => copyToClipboard(createdKey?.api_key ?? '')}
title="Copy to clipboard"
>
{#if copied}
<svg class="w-4 h-4 text-accent-green" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
{:else}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
</svg>
{/if}
</button>
</div>
</div>
<!-- MCP Config -->
<div>
<p class="block text-xs font-medium text-text-secondary mb-1.5">MCP Config (for MCP clients)</p>
<pre class="p-3 bg-bg-primary rounded-lg text-xs font-mono text-text-primary/80 overflow-x-auto border border-border">{getMcpConfig(createdKey.api_key)}</pre>
</div>
<!-- Claude Code config -->
<div>
<p class="block text-xs font-medium text-text-secondary mb-1.5">Claude Code Config (add to ~/.claude/mcp.json)</p>
<pre class="p-3 bg-bg-primary rounded-lg text-xs font-mono text-text-primary/80 overflow-x-auto border border-border">{getClaudeConfig(createdKey.api_key)}</pre>
</div>
</div>
<div class="px-5 py-3 border-t border-border">
<button
class="btn-secondary text-xs"
onclick={() => (createdKey = null)}
>
Dismiss
</button>
</div>
</div>
{/if}
<!-- Create form -->
{#if showCreate}
<form class="card p-5 mb-5" onsubmit={handleCreate}>
<h3 class="font-semibold text-sm text-text-primary font-display mb-4">Create New API Key</h3>
{#if createError}
<div class="mb-3 px-3 py-2 bg-accent-red/10 rounded text-xs text-accent-red">{createError}</div>
{/if}
<div class="space-y-4">
<div>
<label for="key-name" class="block text-xs font-medium text-text-secondary mb-1.5">Name</label>
<input id="key-name" type="text" class="input" placeholder="e.g. production-researcher" bind:value={newKeyName} required />
</div>
<div>
<label for="key-agent" class="block text-xs font-medium text-text-secondary mb-1.5">Agent (optional)</label>
<select id="key-agent" class="input" bind:value={newKeyAgentId}>
<option value={undefined}>No specific agent</option>
{#each agentList as agent}
<option value={agent.id}>{agent.display_name || agent.name} (@{agent.name})</option>
{/each}
</select>
</div>
<div>
<p class="block text-xs font-medium text-text-secondary mb-2">Permissions</p>
<div class="flex flex-wrap gap-4">
<label class="flex items-center gap-2 text-sm text-text-primary cursor-pointer">
<input type="checkbox" bind:checked={newKeyPermRead} class="rounded bg-bg-input border-border text-accent-green focus:ring-accent-green" />
Read
</label>
<label class="flex items-center gap-2 text-sm text-text-primary cursor-pointer">
<input type="checkbox" bind:checked={newKeyPermWrite} class="rounded bg-bg-input border-border text-accent-green focus:ring-accent-green" />
Write
</label>
<label class="flex items-center gap-2 text-sm text-text-primary cursor-pointer">
<input type="checkbox" bind:checked={newKeyPermAdmin} class="rounded bg-bg-input border-border text-accent-green focus:ring-accent-green" />
Admin
</label>
</div>
</div>
<div>
<label for="key-channels" class="block text-xs font-medium text-text-secondary mb-1.5">Allowed Channels (comma-separated, leave empty for all)</label>
<input id="key-channels" type="text" class="input" placeholder="general, research, alerts" bind:value={newKeyChannels} />
</div>
<label class="flex items-center gap-2 text-sm text-text-primary cursor-pointer">
<input type="checkbox" bind:checked={newKeyReadOnly} class="rounded bg-bg-input border-border text-accent-green focus:ring-accent-green" />
Read-only mode
</label>
<div>
<label for="key-expiry" class="block text-xs font-medium text-text-secondary mb-1.5">Expiry Date (optional)</label>
<input id="key-expiry" type="date" class="input" bind:value={newKeyExpiry} />
</div>
<button type="submit" class="btn-primary" disabled={creating}>
{creating ? 'Creating...' : 'Create API Key'}
</button>
</div>
</form>
{/if}
<!-- Keys list -->
<div class="card">
<div class="px-5 py-3 border-b border-border">
<h2 class="font-semibold text-sm text-text-primary font-display">Your API Keys</h2>
</div>
{#if loadingData}
<div class="p-5 space-y-3">
{#each Array(3) as _}
<div class="skeleton h-10 w-full"></div>
{/each}
</div>
{:else if keys.length === 0}
<div class="p-8 text-center">
<svg class="w-10 h-10 mx-auto mb-3 text-border-active" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
<p class="text-sm text-text-secondary">No API keys yet. Create one to get started.</p>
</div>
{:else}
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-border">
<th class="text-left px-5 py-2.5 text-xs font-medium text-text-secondary">Name</th>
<th class="text-left px-5 py-2.5 text-xs font-medium text-text-secondary">Prefix</th>
<th class="text-left px-5 py-2.5 text-xs font-medium text-text-secondary">Permissions</th>
<th class="text-left px-5 py-2.5 text-xs font-medium text-text-secondary">Channels</th>
<th class="text-left px-5 py-2.5 text-xs font-medium text-text-secondary">Expires</th>
<th class="text-left px-5 py-2.5 text-xs font-medium text-text-secondary">Created</th>
<th class="text-right px-5 py-2.5 text-xs font-medium text-text-secondary">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-border">
{#each keys as key}
<tr class="hover:bg-bg-tertiary/30 transition-colors">
<td class="px-5 py-2.5 text-text-primary font-medium">{key.name}</td>
<td class="px-5 py-2.5 font-mono text-xs text-text-secondary">{key.prefix ?? 'sb_...'}</td>
<td class="px-5 py-2.5">
<div class="flex gap-1">
{#if key.permissions?.read}
<span class="badge bg-accent-blue/20 text-accent-blue">read</span>
{/if}
{#if key.permissions?.write}
<span class="badge bg-accent-green/20 text-accent-green">write</span>
{/if}
{#if key.permissions?.admin}
<span class="badge bg-accent-purple/20 text-accent-purple">admin</span>
{/if}
{#if key.read_only}
<span class="badge bg-accent-yellow/20 text-accent-yellow">RO</span>
{/if}
</div>
</td>
<td class="px-5 py-2.5 text-xs text-text-secondary">
{key.allowed_channels?.length ? key.allowed_channels.join(', ') : 'All'}
</td>
<td class="px-5 py-2.5 text-xs text-text-secondary">{formatDate(key.expires_at)}</td>
<td class="px-5 py-2.5 text-xs text-text-secondary">{formatDate(key.created_at)}</td>
<td class="px-5 py-2.5 text-right">
{#if confirmRevokeId === key.id}
<div class="flex items-center justify-end gap-1">
<button
class="px-2 py-1 text-xs bg-accent-red text-white rounded hover:brightness-110 transition-all disabled:opacity-50"
onclick={() => handleRevoke(key.id)}
disabled={revoking}
>
{revoking ? '...' : 'Confirm'}
</button>
<button
class="px-2 py-1 text-xs bg-bg-tertiary text-text-primary rounded hover:bg-border-active transition-all"
onclick={() => (confirmRevokeId = null)}
>
Cancel
</button>
</div>
{:else}
<button
class="px-2 py-1 text-xs text-accent-red hover:bg-accent-red/10 rounded transition-colors"
onclick={() => (confirmRevokeId = key.id)}
>
Revoke
</button>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
</div>