fix: API keys always showed "never used" — middleware blocked them entirely

proxy.ts required a session cookie for every non-public /api/v1/* request,
rejecting with 401 before the request ever reached requireSessionOrApiKey
(lib/api-auth.ts) — the only place that actually verifies a Bearer API key
and updates lastUsedAt. Pure API-key clients never send a session cookie,
so every single API-key request was blocked at the middleware layer; the
lastUsedAt update code was correct but unreachable.

Now lets requests with an `Authorization: Bearer ek_...` header through to
the route, which still does the real verification (and 401s itself on an
invalid/unknown key) — middleware just stops pre-emptively rejecting valid
ones. Also added error logging to the fire-and-forget lastUsedAt update,
previously silent on failure.

Verified locally: hashed a raw key, confirmed it matched the stored hash
(so the lookup itself was never the problem), reproduced the 401 against
the unpatched middleware, then confirmed both the 200 response and
lastUsedAt populating correctly after the fix — visible in the real
Settings → API Keys UI.
This commit is contained in:
Arnaud
2026-07-12 19:34:25 +02:00
parent b69f5845c3
commit eed57cd10b
2 changed files with 10 additions and 2 deletions
+2 -1
View File
@@ -67,7 +67,8 @@ export async function requireSessionOrApiKey(
void db
.update(apiKeys)
.set({ lastUsedAt: new Date() })
.where(eq(apiKeys.id, keyRow.id));
.where(eq(apiKeys.id, keyRow.id))
.catch((err) => console.error("[api-auth] failed to update apiKeys.lastUsedAt", err));
const [user] = await db
.select({
+8 -1
View File
@@ -13,9 +13,16 @@ export async function proxy(request: NextRequest) {
if (isPublic) return NextResponse.next();
// API-key clients authenticate via `Authorization: Bearer ek_...`, not a
// session cookie — they'd otherwise be rejected here before ever reaching
// requireSessionOrApiKey (lib/api-auth.ts), which is the only place that
// actually verifies the key. Defer to it instead of requiring a cookie.
const authHeader = request.headers.get("authorization");
const hasApiKeyHeader = isApi && authHeader?.startsWith("Bearer ek_");
const sessionCookie = getSessionCookie(request);
if (!sessionCookie) {
if (!sessionCookie && !hasApiKeyHeader) {
if (isApi) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}