mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
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.
45 lines
1.8 KiB
TypeScript
45 lines
1.8 KiB
TypeScript
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';
|
|
import { logError } from '@/lib/logger';
|
|
|
|
// The same numbers /admin/growth renders, as JSON, so the Monday digest can pull
|
|
// the scoreboard instead of somebody retyping it into a table.
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
// 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()) {
|
|
return apiErrors.badRequest('Analytics are disabled by this host');
|
|
}
|
|
|
|
const weeksParam = Number(request.nextUrl.searchParams.get('weeks'));
|
|
const scoreboard = await getScoreboard({
|
|
weeks: Number.isSafeInteger(weeksParam) && weeksParam > 0 ? weeksParam : undefined,
|
|
});
|
|
|
|
const response = successResponse({
|
|
...scoreboard,
|
|
weeks: scoreboard.weeks.map((week) => ({ ...week, rates: conversionRates(week) })),
|
|
});
|
|
return withCacheControl(response, 'private, no-store');
|
|
} catch (error) {
|
|
logError('Error building the growth scoreboard:', error);
|
|
return apiErrors.internalError('Failed to build the scoreboard');
|
|
}
|
|
}
|