From 90cae9e4f07d84efb44045bad1b71df0a3bb965e Mon Sep 17 00:00:00 2001 From: zingzy Date: Wed, 22 Jul 2026 21:20:17 +0530 Subject: [PATCH] feat(developer): last used display on API keys Read last_used_at from GET /api/v1/keys and show it per key as relative time next to created; null renders as never used. Mock now serves the field and seeds a never-used key so the row is verifiable in walkthrough mode. --- app/api/mock/[...path]/route.ts | 6 +++++- app/api/mock/[...path]/seed.ts | 13 +++++++++++++ app/dashboard/developer/page.tsx | 6 ++++-- lib/api/keys.ts | 7 ++++--- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/app/api/mock/[...path]/route.ts b/app/api/mock/[...path]/route.ts index 06c3369..d06f114 100644 --- a/app/api/mock/[...path]/route.ts +++ b/app/api/mock/[...path]/route.ts @@ -1660,7 +1660,8 @@ async function handle(req: NextRequest, path: string[]) { /* ---------- api keys ---------- Wire shape mirrors the REAL backend's ApiKeyResponse exactly: envelope - key `keys`, Unix-second timestamps, NO last_used_at (not served yet). + key `keys`, Unix-second timestamps, last_used_at nullable (null until + the key first authenticates; server debounces updates to ~hourly). The frontend normalizes; keeping the mock honest prevents drift. */ const keyToWire = (k: MockKey) => ({ id: k.id, @@ -1673,6 +1674,9 @@ async function handle(req: NextRequest, path: string[]) { expires_at: k.expires_at ? Math.floor(new Date(k.expires_at).getTime() / 1000) : null, + last_used_at: k.last_used_at + ? Math.floor(new Date(k.last_used_at).getTime() / 1000) + : null, revoked: k.revoked, token_prefix: k.token_prefix, }) diff --git a/app/api/mock/[...path]/seed.ts b/app/api/mock/[...path]/seed.ts index 881fc0e..e48f1b9 100644 --- a/app/api/mock/[...path]/seed.ts +++ b/app/api/mock/[...path]/seed.ts @@ -337,6 +337,19 @@ export function buildKeys(): MockKey[] { last_used_at: isoDaysAgo(61, rand), revoked: true, }, + // Never-used key: legal on the wire (last_used_at is null until the + // key first authenticates), so the walkthrough exercises it. + { + id: "key_fresh", + name: "local testing", + description: null, + token_prefix: "spk_live_e51b", + scopes: ["shorten:create", "stats:read"], + created_at: isoDaysAgo(2, rand), + expires_at: null, + last_used_at: null, + revoked: false, + }, ] } diff --git a/app/dashboard/developer/page.tsx b/app/dashboard/developer/page.tsx index 8fd05d5..d3c271e 100644 --- a/app/dashboard/developer/page.tsx +++ b/app/dashboard/developer/page.tsx @@ -110,8 +110,10 @@ function KeyRow({ apiKey }: { apiKey: ApiKey }) { {" "} - · created {formatWhen(apiKey.created_at)} · last used{" "} - {formatWhen(apiKey.last_used_at)} + · created {formatWhen(apiKey.created_at)} ·{" "} + {apiKey.last_used_at + ? `last used ${formatWhen(apiKey.last_used_at)}` + : "never used"} {apiKey.expires_at && ( <> {" "} diff --git a/lib/api/keys.ts b/lib/api/keys.ts index 41dca45..d704580 100644 --- a/lib/api/keys.ts +++ b/lib/api/keys.ts @@ -18,9 +18,10 @@ export type ApiKeyCreated = ApiKey & { } /** Wire shape of /api/v1/keys (backend ApiKeyResponse): the list envelope - key is `keys`, timestamps are Unix SECONDS, and last_used_at is not - served yet (optional so it lights up if the backend adds it). Normalized - here so the rest of the app keeps ISO strings. */ + key is `keys`, timestamps are Unix SECONDS, and last_used_at is null + until the key first authenticates a request (the server debounces + updates to at most once an hour). Normalized here so the rest of the + app keeps ISO strings. */ type ApiKeyWire = { id: string name: string