Private
Public Access
1
0

Add API key management UI to account settings

Users can create, view, and delete API keys from their profile.
Keys are shown once on creation with a copy button. List shows
key prefix, creation date, and last used date.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-03-08 11:44:16 -04:00
parent d8b3935fc5
commit 827f0394ef
2 changed files with 141 additions and 4 deletions
+19 -4
View File
@@ -1,16 +1,31 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types.js';
import { withBypassRLS } from '$lib/server/rls.js';
import { prisma } from '$lib/server/db.js';
export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user) throw redirect(302, '/auth/login');
const sessionCount = await withBypassRLS(async (tx) => {
return tx.session.count({ where: { userId: locals.user!.id } });
});
const [sessionCount, apiKeys] = await Promise.all([
withBypassRLS(async (tx) => {
return tx.session.count({ where: { userId: locals.user!.id } });
}),
prisma.apiKey.findMany({
where: { userId: locals.user.id },
select: {
id: true,
name: true,
keyPrefix: true,
lastUsedAt: true,
createdAt: true
},
orderBy: { createdAt: 'desc' }
})
]);
return {
user: locals.user,
sessionCount
sessionCount,
apiKeys
};
};
+122
View File
@@ -28,11 +28,19 @@
let sessionCount = $state(0);
let revokingSessions = $state(false);
// API Keys
let apiKeys = $state<{ id: string; name: string; keyPrefix: string; lastUsedAt: Date | string | null; createdAt: Date | string }[]>([]);
let newKeyName = $state('');
let creatingKey = $state(false);
let newlyCreatedKey = $state<string | null>(null);
let copiedKey = $state(false);
$effect.pre(() => {
name = data.user.name;
email = data.user.email;
avatarUrl = data.user.avatarUrl;
sessionCount = data.sessionCount;
apiKeys = data.apiKeys;
});
function onFileSelect(e: Event) {
@@ -115,6 +123,43 @@
}
}
async function createApiKey() {
if (!newKeyName.trim()) return;
creatingKey = true;
newlyCreatedKey = null;
try {
const result = await api('/api/account/api-keys', {
method: 'POST',
body: JSON.stringify({ name: newKeyName.trim() })
});
if (result.ok) {
newlyCreatedKey = result.data.key;
apiKeys = [result.data.apiKey, ...apiKeys];
newKeyName = '';
toast.success('API key created');
}
} finally {
creatingKey = false;
}
}
async function deleteApiKey(id: string) {
const result = await api('/api/account/api-keys', {
method: 'DELETE',
body: JSON.stringify({ id })
});
if (result.ok) {
apiKeys = apiKeys.filter((k) => k.id !== id);
toast.success('API key deleted');
}
}
async function copyKey(key: string) {
await navigator.clipboard.writeText(key);
copiedKey = true;
setTimeout(() => (copiedKey = false), 2000);
}
async function revokeOtherSessions() {
revokingSessions = true;
try {
@@ -270,6 +315,83 @@
</div>
</section>
<!-- API Keys -->
<section class="mb-8">
<h2 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-3">API Keys</h2>
<div class="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-4">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
API keys allow programmatic access to your account. Use them with <code class="text-xs bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">Authorization: Bearer pnc_...</code>
</p>
{#if newlyCreatedKey}
<div class="mb-4 p-3 rounded-lg border border-green-300 dark:border-green-700 bg-green-50 dark:bg-green-900/20">
<p class="text-sm font-medium text-green-800 dark:text-green-200 mb-2">Key created — copy it now, it won't be shown again:</p>
<div class="flex items-center gap-2">
<code class="flex-1 text-xs bg-white dark:bg-gray-800 border border-green-200 dark:border-green-800 rounded px-3 py-2 font-mono break-all select-all">{newlyCreatedKey}</code>
<button
onclick={() => copyKey(newlyCreatedKey!)}
class="flex-shrink-0 rounded-lg border border-gray-300 dark:border-gray-600 px-3 py-2 text-sm hover:bg-gray-50 dark:hover:bg-gray-800 transition"
type="button"
>
{copiedKey ? 'Copied!' : 'Copy'}
</button>
</div>
</div>
{/if}
<!-- Create new key -->
<form onsubmit={async (e) => { e.preventDefault(); await createApiKey(); }} class="flex gap-2 mb-4">
<input
type="text"
bind:value={newKeyName}
placeholder="Key name (e.g. Claude Code)"
maxlength={100}
class="flex-1 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 px-3 py-2 text-sm focus:border-[var(--color-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-primary)]"
/>
<button
type="submit"
disabled={creatingKey || !newKeyName.trim()}
class="rounded-lg bg-[var(--color-primary)] px-4 py-2 text-sm font-medium text-white hover:opacity-90 disabled:opacity-50 transition whitespace-nowrap"
>
{creatingKey ? 'Creating...' : 'Create Key'}
</button>
</form>
<!-- Key list -->
{#if apiKeys.length > 0}
<div class="space-y-2">
{#each apiKeys as key (key.id)}
<div class="flex items-center justify-between p-3 rounded-lg border border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-gray-800/50">
<div class="min-w-0">
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">{key.name}</p>
<p class="text-xs text-gray-500 dark:text-gray-400 font-mono">{key.keyPrefix}...
<span class="mx-1">·</span>
Created {new Date(key.createdAt).toLocaleDateString()}
{#if key.lastUsedAt}
<span class="mx-1">·</span>
Last used {new Date(key.lastUsedAt).toLocaleDateString()}
{:else}
<span class="mx-1">·</span>
Never used
{/if}
</p>
</div>
<button
onclick={() => deleteApiKey(key.id)}
class="flex-shrink-0 rounded-lg border border-red-300 dark:border-red-700 px-3 py-1.5 text-xs font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition"
type="button"
>
Delete
</button>
</div>
{/each}
</div>
{:else}
<p class="text-sm text-gray-500 dark:text-gray-400 text-center py-2">No API keys yet</p>
{/if}
</div>
</section>
<!-- Sessions -->
<section>
<h2 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-3">Sessions</h2>