From 41560ab05f5d32430d404febdbbd55f0a7cd956e Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Mon, 11 May 2026 13:37:45 -0500 Subject: [PATCH] feat(api): add wavelog /api/statistics and /api/version stubs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GridTracker (and several Cloudlog-flavored clients) pre-flight an API key by hitting /api/statistics — and some additionally probe /api/version. Without these endpoints the key validation hangs at "Testing API Key" because GT never gets a parseable response. POST /api/statistics { key } → 200 { Today, total_qsos, month_qsos, year_qsos } POST /api/version { key } → 200 { status: 'ok', version: '2.7.0' } Both 401 with {status,reason} on bad/missing keys. Both respect the per-key station scoping that the rest of the wavelog endpoints honor (if api_keys.station_id is set, statistics count only that station's QSOs). Counts are computed in a single FILTER aggregate against the contacts table; UTC-anchored boundaries to match how callers display. The version string reports 2.7.0 to match what /api/cloudlog claims as the compatibility target — clients enable the same feature set they'd use against real wavelog at that version. Tested locally against a postgres container loaded with a snapshot of the production schema. Two seed QSOs → statistics returns {Today:2,total_qsos:2,month_qsos:2,year_qsos:2}; bad keys return 401. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/api/statistics/route.ts | 65 +++++++++++++++++++++++++++++++++ src/app/api/version/route.ts | 42 +++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 src/app/api/statistics/route.ts create mode 100644 src/app/api/version/route.ts diff --git a/src/app/api/statistics/route.ts b/src/app/api/statistics/route.ts new file mode 100644 index 0000000..bc5026c --- /dev/null +++ b/src/app/api/statistics/route.ts @@ -0,0 +1,65 @@ +// POST /api/statistics — wavelog wire-compatible QSO statistics. +// Body: { "key": "nextlog_..." } +// Returns 200 with { Today, total_qsos, month_qsos, year_qsos } counts for +// the API key's owner. Some third-party clients (GridTracker, dashboards) +// pre-flight this endpoint when validating an API key. + +import { NextRequest, NextResponse } from 'next/server'; +import { verifyApiKeyValue } from '@/lib/api-auth'; +import { addCorsHeaders, createCorsPreflightResponse } from '@/lib/cors'; +import { query } from '@/lib/db'; + +export async function OPTIONS() { + return createCorsPreflightResponse(); +} + +export async function POST(request: NextRequest) { + let key = ''; + try { + const body = await request.json(); + if (typeof body?.key === 'string') key = body.key; + } catch { + // fall through to auth failure + } + + const authResult = await verifyApiKeyValue(key); + if (!authResult.success || !authResult.auth) { + return addCorsHeaders( + NextResponse.json( + { status: 'failed', reason: 'missing or invalid api key' }, + { status: 401 }, + ), + ); + } + const auth = authResult.auth; + + // Station scoping: if the key targets a single station, restrict counts to + // that station's QSOs; otherwise count across all of the user's stations. + const stationClause = auth.stationId !== null ? ' AND station_id = $2' : ''; + const baseParams: (number | null)[] = auth.stationId !== null ? [auth.userId, auth.stationId] : [auth.userId]; + + const sql = ` + SELECT + COUNT(*) FILTER (WHERE datetime >= date_trunc('day', CURRENT_TIMESTAMP AT TIME ZONE 'UTC')) AS today, + COUNT(*) FILTER (WHERE datetime >= date_trunc('month', CURRENT_TIMESTAMP AT TIME ZONE 'UTC')) AS month_qsos, + COUNT(*) FILTER (WHERE datetime >= date_trunc('year', CURRENT_TIMESTAMP AT TIME ZONE 'UTC')) AS year_qsos, + COUNT(*) AS total_qsos + FROM contacts + WHERE user_id = $1${stationClause} + `; + + const result = await query(sql, baseParams); + const row = result.rows[0]; + + return addCorsHeaders( + NextResponse.json( + { + Today: Number(row.today), + total_qsos: Number(row.total_qsos), + month_qsos: Number(row.month_qsos), + year_qsos: Number(row.year_qsos), + }, + { status: 200 }, + ), + ); +} diff --git a/src/app/api/version/route.ts b/src/app/api/version/route.ts new file mode 100644 index 0000000..010a8e0 --- /dev/null +++ b/src/app/api/version/route.ts @@ -0,0 +1,42 @@ +// POST /api/version — wavelog wire-compatible version probe. +// Body: { "key": "nextlog_..." } +// Returns 200 { status: "ok", version: "" } or 401 on bad key. +// +// The version string is what GridTracker / Log4OM-style clients compare +// against to decide which features are available. We report a recent +// wavelog version so clients enable the full feature set they'd use with +// real wavelog. + +import { NextRequest, NextResponse } from 'next/server'; +import { verifyApiKeyValue } from '@/lib/api-auth'; +import { addCorsHeaders, createCorsPreflightResponse } from '@/lib/cors'; + +const WAVELOG_COMPAT_VERSION = '2.7.0'; + +export async function OPTIONS() { + return createCorsPreflightResponse(); +} + +export async function POST(request: NextRequest) { + let key = ''; + try { + const body = await request.json(); + if (typeof body?.key === 'string') key = body.key; + } catch { + // fall through to auth failure + } + + const authResult = await verifyApiKeyValue(key); + if (!authResult.success || !authResult.auth) { + return addCorsHeaders( + NextResponse.json( + { status: 'failed', reason: 'missing or invalid api key' }, + { status: 401 }, + ), + ); + } + + return addCorsHeaders( + NextResponse.json({ status: 'ok', version: WAVELOG_COMPAT_VERSION }, { status: 200 }), + ); +}