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(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
key_lookup TEXT NOT NULL UNIQUE,
|
key_lookup TEXT NOT NULL UNIQUE,
|
||||||
key_hash TEXT NOT NULL,
|
key_hash TEXT NOT NULL,
|
||||||
|
key_plain TEXT NOT NULL,
|
||||||
account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||||
label TEXT NOT NULL,
|
label TEXT NOT NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT now(),
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ export const account = new Elysia({ prefix: "/account" })
|
||||||
// Get account settings
|
// Get account settings
|
||||||
.get("/settings", async ({ accountId }) => {
|
.get("/settings", async ({ accountId }) => {
|
||||||
const [acc] = await sql`SELECT id, email_hash, created_at FROM accounts WHERE id = ${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 {
|
return {
|
||||||
account_id: acc.id,
|
account_id: acc.id,
|
||||||
has_email: !!acc.email_hash,
|
has_email: !!acc.email_hash,
|
||||||
|
|
@ -192,7 +192,7 @@ export const account = new Elysia({ prefix: "/account" })
|
||||||
const keyLookup = sha256(norm);
|
const keyLookup = sha256(norm);
|
||||||
const keyHash = await Bun.password.hash(norm, { algorithm: "bcrypt", cost: 10 });
|
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 };
|
return { key: rawKey, id: created.id, label: body.label };
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ export const dashboard = new Elysia()
|
||||||
if (!accountId) return redirect("/dashboard");
|
if (!accountId) return redirect("/dashboard");
|
||||||
|
|
||||||
const [acc] = await sql`SELECT id, email_hash, created_at FROM accounts WHERE id = ${accountId}`;
|
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;
|
const loginKey = cookie?.pingql_key?.value ?? null;
|
||||||
|
|
||||||
return html("settings", { nav: "settings", account: acc, apiKeys, accountId, loginKey });
|
return html("settings", { nav: "settings", account: acc, apiKeys, accountId, loginKey });
|
||||||
|
|
|
||||||
|
|
@ -66,27 +66,22 @@
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Sub-keys list -->
|
||||||
<div id="keys-list" class="space-y-2">
|
<div id="keys-list" class="space-y-2">
|
||||||
<% if (it.apiKeys.length === 0) { %>
|
<% if (it.apiKeys.length === 0) { %>
|
||||||
<p class="text-xs text-gray-600 italic">No sub-keys yet.</p>
|
<p class="text-xs text-gray-600 italic">No sub-keys yet.</p>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<% it.apiKeys.forEach(function(k) { %>
|
<% 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 class="p-3 bg-gray-800/50 rounded-lg border border-gray-700/50">
|
||||||
<div>
|
<div class="flex items-center justify-between mb-2">
|
||||||
<p class="text-sm text-gray-200"><%= k.label %></p>
|
<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>
|
</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>
|
</div>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
@ -143,33 +138,20 @@
|
||||||
async function createKey() {
|
async function createKey() {
|
||||||
const label = document.getElementById('key-label').value.trim();
|
const label = document.getElementById('key-label').value.trim();
|
||||||
if (!label) return;
|
if (!label) return;
|
||||||
const data = await api('/account/keys', { method: 'POST', body: { label } });
|
await api('/account/keys', { method: 'POST', body: { label } });
|
||||||
hideCreateKey();
|
location.reload();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyNewKey() {
|
function copyKey(btn, val) {
|
||||||
const val = document.getElementById('new-key-value').textContent;
|
|
||||||
navigator.clipboard.writeText(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);
|
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;
|
if (!confirm('Revoke this key? Any apps using it will stop working.')) return;
|
||||||
await api(`/account/keys/${id}`, { method: 'DELETE' });
|
await api(`/account/keys/${id}`, { method: 'DELETE' });
|
||||||
location.reload();
|
btn.closest('.p-3').remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showStatus(id, msg, color) {
|
function showStatus(id, msg, color) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue