feat(admin): let a token read the growth scoreboard without a session

The weekly digest reads /api/admin/growth from a script, which has no browser
and therefore no NextAuth session. The alternative was copying a session cookie
out of a browser by hand: those are JWTs with a 30-day lifetime, so a scheduled
job built on one stops working a month later and reports nothing rather than
reporting a failure.

The token path is off unless OPENFRAME_ADMIN_API_TOKEN is set, so an instance
that never sets it keeps session-only admin access. A value under 32 characters
is treated as no token at all: behind this header sit every paying account's
name, email and usage, and a short token is a guessable path to all of it.
Comparison runs over SHA-256 digests so it stays constant time without leaking
the token's length.
This commit is contained in:
2026-08-18 10:22:42 +03:00
parent 32164db15c
commit bfe3cb28b4
5 changed files with 227 additions and 7 deletions
+12 -6
View File
@@ -1,5 +1,6 @@
import { NextRequest } from 'next/server';
import { auth } from '@/lib/auth';
import { isAdminApiTokenRequest } from '@/lib/admin-api-token';
import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response';
import { conversionRates, getScoreboard } from '@/lib/analytics/scoreboard';
import { isProductAnalyticsEnabled } from '@/lib/feature-flags';
@@ -9,12 +10,17 @@ import { logError } from '@/lib/logger';
// the scoreboard instead of somebody retyping it into a table.
export async function GET(request: NextRequest) {
try {
const session = await auth();
if (!session?.user?.id) {
return apiErrors.unauthorized();
}
if (!session.user.isAdmin) {
return apiErrors.forbidden('Admin access required');
// A bearer token stands in for the admin session so the weekly digest can
// read this without a browser. Unset by default, in which case the only way
// in is still an admin session.
if (!isAdminApiTokenRequest(request)) {
const session = await auth();
if (!session?.user?.id) {
return apiErrors.unauthorized();
}
if (!session.user.isAdmin) {
return apiErrors.forbidden('Admin access required');
}
}
if (!isProductAnalyticsEnabled()) {