fix: store key_plain on sub-keys, display always in settings with copy button
This commit is contained in:
parent
c684d96d90
commit
54c89a5a11
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -66,27 +66,22 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New sub-key reveal (shown after creation) -->
|
||||
<div id="new-key-reveal" class="hidden mb-4 p-4 bg-green-950/30 rounded-lg border border-green-900/50">
|
||||
<p class="text-xs text-green-400 mb-2">Sub-key created — copy it now, it won't be shown again.</p>
|
||||
<div class="flex gap-2">
|
||||
<code id="new-key-value" class="flex-1 bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-blue-400 text-sm font-mono select-all"></code>
|
||||
<button onclick="copyNewKey()" class="px-3 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg text-gray-400 hover:text-white text-xs transition-colors">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sub-keys list -->
|
||||
<div id="keys-list" class="space-y-2">
|
||||
<% if (it.apiKeys.length === 0) { %>
|
||||
<p class="text-xs text-gray-600 italic">No sub-keys yet.</p>
|
||||
<% } else { %>
|
||||
<% it.apiKeys.forEach(function(k) { %>
|
||||
<div class="flex items-center justify-between p-3 bg-gray-800/50 rounded-lg border border-gray-700/50">
|
||||
<div>
|
||||
<div class="p-3 bg-gray-800/50 rounded-lg border border-gray-700/50">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<p class="text-sm text-gray-200"><%= k.label %></p>
|
||||
<p class="text-xs text-gray-600 mt-0.5">created <%= new Date(k.created_at).toLocaleDateString() %> <%~ k.last_used_at ? '· last used ' + it.timeAgoSSR(k.last_used_at) : '· never used' %></p>
|
||||
<button onclick="deleteKey('<%= k.id %>', this)" class="text-xs text-gray-600 hover:text-red-400 transition-colors px-2 py-1">Revoke</button>
|
||||
</div>
|
||||
<button onclick="deleteKey('<%= k.id %>')" class="text-xs text-gray-600 hover:text-red-400 transition-colors px-2 py-1">Revoke</button>
|
||||
<div class="flex gap-2">
|
||||
<code class="flex-1 bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-blue-400 text-xs font-mono select-all"><%= k.key_plain %></code>
|
||||
<button onclick="copyKey(this, '<%= k.key_plain %>')" class="px-3 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg text-gray-400 hover:text-white text-xs transition-colors">Copy</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 mt-1.5">created <%= new Date(k.created_at).toLocaleDateString() %> <%~ k.last_used_at ? '· last used ' + it.timeAgoSSR(k.last_used_at) : '· never used' %></p>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
|
|
@ -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 = `<div><p class="text-sm text-gray-200">${label}</p><p class="text-xs text-gray-600 mt-0.5">just now · never used</p></div><button onclick="deleteKey('${data.id}')" class="text-xs text-gray-600 hover:text-red-400 transition-colors px-2 py-1">Revoke</button>`;
|
||||
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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue