Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions src/app/api/auth/[key]/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
// GET /api/auth/<key> — wavelog wire-compatible API key validation.
// /api/auth/<key> — wavelog wire-compatible API key validation.
// Returns XML, matching wavelog's <auth>...</auth> shape. The HTTP status is
// 200 regardless of whether the key is valid (matching wavelog's quirk —
// clients distinguish via the body, not the status).
//
// Accepts both GET and POST: wavelog runs on CodeIgniter, whose controllers
// answer any HTTP method by default, and several clients (GridTracker, in
// particular) POST to this URL even though the key is in the path. Honor
// both verbs so those clients work.

import { NextRequest } from 'next/server';
import { verifyApiKeyValue } from '@/lib/api-auth';
Expand All @@ -22,7 +27,7 @@ function invalidKeyResponse() {
return xmlResponse('<auth>\n <message>Key Invalid - either not found or disabled</message>\n</auth>');
}

export async function GET(
async function handle(
_request: NextRequest,
{ params }: { params: Promise<{ key: string }> },
) {
Expand All @@ -35,3 +40,18 @@ export async function GET(
const rights = authResult.auth.isReadOnly ? 'r' : 'rw';
return xmlResponse(`<auth>\n <status>Valid</status>\n <rights>${rights}</rights>\n</auth>`);
}

export const GET = handle;
export const POST = handle;

export function OPTIONS() {
return new Response(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-API-Key',
'Access-Control-Max-Age': '86400',
},
});
}
22 changes: 22 additions & 0 deletions src/app/api/station_info/[key]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// POST /api/station_info/<key> — wavelog wire-compatible variant where the
// API key is passed as a path segment. GridTracker uses this form when
// fetching the station profile list after a successful auth.

import { NextRequest } from 'next/server';
import { stationInfoResponse } from '@/lib/station-info';
import { createCorsPreflightResponse } from '@/lib/cors';

export async function OPTIONS() {
return createCorsPreflightResponse();
}

async function handle(
_request: NextRequest,
{ params }: { params: Promise<{ key: string }> },
) {
const { key } = await params;
return stationInfoResponse(key);
}

export const GET = handle;
export const POST = handle;
45 changes: 5 additions & 40 deletions src/app/api/station_info/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
// POST /api/station_info — wavelog wire-compatible station profile listing.
// Body: { "key": "nextlog_..." }
// Returns 200 with an array of station profiles in wavelog's shape. Respects
// per-key station scoping (api_keys.station_id): a key scoped to a single
// station only sees that station.
// See /api/station_info/[key] for GridTracker's path-segment-auth variant.

import { NextRequest, NextResponse } from 'next/server';
import { verifyApiKeyValue } from '@/lib/api-auth';
import { addCorsHeaders, createCorsPreflightResponse } from '@/lib/cors';
import { query } from '@/lib/db';
import { NextRequest } from 'next/server';
import { stationInfoResponse } from '@/lib/station-info';
import { createCorsPreflightResponse } from '@/lib/cors';

export async function OPTIONS() {
return createCorsPreflightResponse();
Expand All @@ -21,37 +18,5 @@ export async function POST(request: NextRequest) {
} 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;

const sql = auth.stationId !== null
? 'SELECT id, station_name, grid_locator, callsign, is_active FROM stations WHERE user_id = $1 AND id = $2 ORDER BY id'
: 'SELECT id, station_name, grid_locator, callsign, is_active FROM stations WHERE user_id = $1 ORDER BY id';
const params = auth.stationId !== null ? [auth.userId, auth.stationId] : [auth.userId];

const result = await query(sql, params);

// Wavelog returns a plain JSON array (not wrapped in {success,...}), with
// station_active as 1/0 (not boolean) and a station_uuid string. Nextlog
// doesn't have a uuid column — return the id as a string so clients that
// key off it still get a stable value.
const stations = result.rows.map((row: Record<string, unknown>) => ({
station_id: row.id,
station_profile_name: row.station_name,
station_gridsquare: row.grid_locator ?? '',
station_callsign: row.callsign,
station_active: row.is_active ? 1 : 0,
station_uuid: String(row.id),
}));

return addCorsHeaders(NextResponse.json(stations, { status: 200 }));
return stationInfoResponse(key);
}
39 changes: 39 additions & 0 deletions src/lib/station-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Shared response builder for wavelog-compatible station_info lookups.
// Used by both /api/station_info (key in JSON body) and
// /api/station_info/[key] (key as path segment — GridTracker's form).

import { NextResponse } from 'next/server';
import { verifyApiKeyValue } from '@/lib/api-auth';
import { addCorsHeaders } from '@/lib/cors';
import { query } from '@/lib/db';

export async function stationInfoResponse(key: string) {
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;

const sql = auth.stationId !== null
? 'SELECT id, station_name, grid_locator, callsign, is_active FROM stations WHERE user_id = $1 AND id = $2 ORDER BY id'
: 'SELECT id, station_name, grid_locator, callsign, is_active FROM stations WHERE user_id = $1 ORDER BY id';
const params = auth.stationId !== null ? [auth.userId, auth.stationId] : [auth.userId];

const result = await query(sql, params);

const stations = result.rows.map((row: Record<string, unknown>) => ({
station_id: row.id,
station_profile_name: row.station_name,
station_gridsquare: row.grid_locator ?? '',
station_callsign: row.callsign,
station_active: row.is_active ? 1 : 0,
station_uuid: String(row.id),
}));

return addCorsHeaders(NextResponse.json(stations, { status: 200 }));
}
Loading