trails/apps/planner/app/routes/api.sessions.ts
Ullrich Schäfer 6f18ce8099
fix(planner): bound /api/sessions listing (default 50, max 200)
Addresses planner-audit #8. `listSessions()` had no LIMIT — every call
to `GET /api/sessions` returned every non-closed session and did a
full table scan ordered by last_activity. On a long-running planner
host that's both a memory cliff and a query-plan footgun.

Now: defaults to 50 rows, clamps to [1, 200], accepts `?limit=` for
pagination-light scenarios. `sessions_last_activity_idx` (added in
#438) backs the ORDER BY + LIMIT efficiently.
2026-05-26 00:46:38 +02:00

55 lines
2 KiB
TypeScript

import { data } from "react-router";
import type { Route } from "./+types/api.sessions";
import { createSession, listSessions } from "~/lib/sessions";
import { parseGpxAsync, extractWaypoints } from "@trails-cool/gpx";
import { withDb } from "@trails-cool/db";
import type { Waypoint } from "@trails-cool/types";
export async function action({ request }: Route.ActionArgs) {
if (request.method !== "POST") {
return data({ error: "Method not allowed" }, { status: 405 });
}
const body = await request.json();
const { callbackUrl, callbackToken, gpx } = body as {
callbackUrl?: string;
callbackToken?: string;
gpx?: string;
};
return withDb(async () => {
const session = await createSession({ callbackUrl, callbackToken });
let initialWaypoints: Waypoint[] | undefined;
let initialNoGoAreas: Array<{ points: Array<{ lat: number; lon: number }> }> | undefined;
let initialNotes: string | undefined;
if (gpx) {
try {
const gpxData = await parseGpxAsync(gpx);
const wps = extractWaypoints(gpxData);
if (wps.length > 0) initialWaypoints = wps;
if (gpxData.noGoAreas.length > 0) initialNoGoAreas = gpxData.noGoAreas;
if (gpxData.description) initialNotes = gpxData.description;
} catch {
// Continue with empty session if GPX is invalid
}
}
return data(
{ sessionId: session.id, url: `/session/${session.id}`, initialWaypoints, initialNoGoAreas, initialNotes },
{ status: 201 },
);
});
}
export async function loader({ request }: Route.LoaderArgs) {
return withDb(async () => {
const url = new URL(request.url);
// Accept an explicit `?limit=` but rely on listSessions to clamp
// it to a sane upper bound.
const limitParam = Number(url.searchParams.get("limit"));
const limit = Number.isFinite(limitParam) && limitParam > 0 ? limitParam : undefined;
const sessions = await listSessions(limit);
return data({ sessions });
});
}