diff --git a/src/routes/account/+page.server.ts b/src/routes/account/+page.server.ts index 6900eb6..53be9af 100644 --- a/src/routes/account/+page.server.ts +++ b/src/routes/account/+page.server.ts @@ -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 }; }; diff --git a/src/routes/account/+page.svelte b/src/routes/account/+page.svelte index 885c87d..2739d6b 100644 --- a/src/routes/account/+page.svelte +++ b/src/routes/account/+page.svelte @@ -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(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 @@ + +
+

API Keys

+
+

+ API keys allow programmatic access to your account. Use them with Authorization: Bearer pnc_... +

+ + {#if newlyCreatedKey} +
+

Key created — copy it now, it won't be shown again:

+
+ {newlyCreatedKey} + +
+
+ {/if} + + +
{ e.preventDefault(); await createApiKey(); }} class="flex gap-2 mb-4"> + + +
+ + + {#if apiKeys.length > 0} +
+ {#each apiKeys as key (key.id)} +
+
+

{key.name}

+

{key.keyPrefix}... + · + Created {new Date(key.createdAt).toLocaleDateString()} + {#if key.lastUsedAt} + · + Last used {new Date(key.lastUsedAt).toLocaleDateString()} + {:else} + · + Never used + {/if} +

+
+ +
+ {/each} +
+ {:else} +

No API keys yet

+ {/if} +
+
+

Sessions