diff --git a/apps/web/src/db.ts b/apps/web/src/db.ts index fd2181e..2208135 100644 --- a/apps/web/src/db.ts +++ b/apps/web/src/db.ts @@ -59,6 +59,7 @@ export async function migrate() { id UUID PRIMARY KEY DEFAULT gen_random_uuid(), key_lookup TEXT NOT NULL UNIQUE, key_hash TEXT NOT NULL, + key_plain TEXT NOT NULL, account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, label TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT now(), diff --git a/apps/web/src/routes/auth.ts b/apps/web/src/routes/auth.ts index e417ff9..a55aa99 100644 --- a/apps/web/src/routes/auth.ts +++ b/apps/web/src/routes/auth.ts @@ -147,7 +147,7 @@ export const account = new Elysia({ prefix: "/account" }) // Get account settings .get("/settings", async ({ accountId }) => { const [acc] = await sql`SELECT id, email_hash, created_at FROM accounts WHERE id = ${accountId}`; - const keys = await sql`SELECT id, label, created_at, last_used_at FROM api_keys WHERE account_id = ${accountId} ORDER BY created_at DESC`; + const keys = await sql`SELECT id, key_plain, label, created_at, last_used_at FROM api_keys WHERE account_id = ${accountId} ORDER BY created_at DESC`; return { account_id: acc.id, has_email: !!acc.email_hash, @@ -192,7 +192,7 @@ export const account = new Elysia({ prefix: "/account" }) const keyLookup = sha256(norm); const keyHash = await Bun.password.hash(norm, { algorithm: "bcrypt", cost: 10 }); - const [created] = await sql`INSERT INTO api_keys (key_lookup, key_hash, account_id, label) VALUES (${keyLookup}, ${keyHash}, ${accountId}, ${body.label}) RETURNING id`; + const [created] = await sql`INSERT INTO api_keys (key_lookup, key_hash, key_plain, account_id, label) VALUES (${keyLookup}, ${keyHash}, ${rawKey}, ${accountId}, ${body.label}) RETURNING id`; return { key: rawKey, id: created.id, label: body.label }; }, { body: t.Object({ diff --git a/apps/web/src/routes/dashboard.ts b/apps/web/src/routes/dashboard.ts index dd9611a..a0defa5 100644 --- a/apps/web/src/routes/dashboard.ts +++ b/apps/web/src/routes/dashboard.ts @@ -141,7 +141,7 @@ export const dashboard = new Elysia() if (!accountId) return redirect("/dashboard"); const [acc] = await sql`SELECT id, email_hash, created_at FROM accounts WHERE id = ${accountId}`; - const apiKeys = await sql`SELECT id, label, created_at, last_used_at FROM api_keys WHERE account_id = ${accountId} ORDER BY created_at DESC`; + const apiKeys = await sql`SELECT id, key_plain, label, created_at, last_used_at FROM api_keys WHERE account_id = ${accountId} ORDER BY created_at DESC`; const loginKey = cookie?.pingql_key?.value ?? null; return html("settings", { nav: "settings", account: acc, apiKeys, accountId, loginKey }); diff --git a/apps/web/src/views/settings.ejs b/apps/web/src/views/settings.ejs index 39f3cdd..085546c 100644 --- a/apps/web/src/views/settings.ejs +++ b/apps/web/src/views/settings.ejs @@ -66,27 +66,22 @@ - - -
<% if (it.apiKeys.length === 0) { %>

No sub-keys yet.

<% } else { %> <% it.apiKeys.forEach(function(k) { %> -
-
+
+

<%= k.label %>

-

created <%= new Date(k.created_at).toLocaleDateString() %> <%~ k.last_used_at ? '· last used ' + it.timeAgoSSR(k.last_used_at) : '· never used' %>

+
- +
+ <%= k.key_plain %> + +
+

created <%= new Date(k.created_at).toLocaleDateString() %> <%~ k.last_used_at ? '· last used ' + it.timeAgoSSR(k.last_used_at) : '· never used' %>

<% }) %> <% } %> @@ -143,33 +138,20 @@ async function createKey() { const label = document.getElementById('key-label').value.trim(); if (!label) return; - const data = await api('/account/keys', { method: 'POST', body: { label } }); - hideCreateKey(); - document.getElementById('new-key-value').textContent = data.key; - document.getElementById('new-key-reveal').classList.remove('hidden'); - // Add to list without reload - const list = document.getElementById('keys-list'); - const empty = list.querySelector('p.italic'); - if (empty) empty.remove(); - const item = document.createElement('div'); - item.className = 'flex items-center justify-between p-3 bg-gray-800/50 rounded-lg border border-gray-700/50'; - item.innerHTML = `

${label}

just now · never used

`; - list.appendChild(item); + await api('/account/keys', { method: 'POST', body: { label } }); + location.reload(); } - function copyNewKey() { - const val = document.getElementById('new-key-value').textContent; + function copyKey(btn, val) { navigator.clipboard.writeText(val); - const btn = event.target; - btn.textContent = 'Copied!'; - btn.classList.add('text-green-400'); + btn.textContent = 'Copied!'; btn.classList.add('text-green-400'); setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('text-green-400'); }, 1500); } - async function deleteKey(id) { + async function deleteKey(id, btn) { if (!confirm('Revoke this key? Any apps using it will stop working.')) return; await api(`/account/keys/${id}`, { method: 'DELETE' }); - location.reload(); + btn.closest('.p-3').remove(); } function showStatus(id, msg, color) {