Add API security guards, rate limiter, and manual test checklist

Implement strict CORS enforcement and per-IP rate limiting in the Next.js middleware. Add input validation helpers for
coordinates and request body size limits. Introduce SSRF protection for calendar URL fetching. Update mobile settings to
support new transport options and arrival buffers. Include a comprehensive manual testing checklist for integration
verification.
This commit is contained in:
2026-05-12 14:42:11 +02:00
parent 863996f06c
commit 35971596b3
21 changed files with 1096 additions and 124 deletions
+27 -8
View File
@@ -6,6 +6,7 @@ import type {
Journey,
Station,
} from "@timetoleave/core";
import { hafasDateTime } from "@timetoleave/core";
const DEFAULT_BASE_URL = "";
@@ -45,7 +46,7 @@ export class ApiClient {
fromLng: number,
toLat: number,
toLng: number,
): Promise<WalkRoute> {
): Promise<BikeRoute> {
const url = buildUrl(this.baseUrl, "/api/bike-route", {
fromLat: String(fromLat),
fromLng: String(fromLng),
@@ -62,7 +63,7 @@ export class ApiClient {
fromLng: number,
toLat: number,
toLng: number,
): Promise<BikeRoute> {
): Promise<WalkRoute> {
const url = buildUrl(this.baseUrl, "/api/walk-route", {
fromLat: String(fromLat),
fromLng: String(fromLng),
@@ -112,11 +113,22 @@ export class ApiClient {
toStationExtId: string,
date: Date,
): Promise<Journey[]> {
// Use POST request as per the new implementation
// Use the proper HAFAS protocol body as expected by the endpoint
const { date: hafasDate, time: hafasTime } = hafasDateTime(date);
const body = {
from: fromStationExtId,
to: toStationExtId,
date: date.toISOString(),
svcReqL: [
{
meth: "TripSearch",
req: {
depLocL: [{ type: "S", extId: fromStationExtId }],
arrLocL: [{ type: "S", extId: toStationExtId }],
outDate: hafasDate,
outTime: hafasTime,
numF: 5,
},
},
],
};
const res = await fetch(`${this.baseUrl}/api/hafas`, {
@@ -130,9 +142,16 @@ export class ApiClient {
}
async searchStation(query: string): Promise<Station[]> {
const url = buildUrl(this.baseUrl, "/api/station", { q: query });
// WienerLinien client expects lat/lng parameters, not query string
// For now, use geocode to find Vienna coordinates as a fallback
const fallbackCoords = { lat: 48.2082, lng: 16.3738 };
const url = buildUrl(this.baseUrl, "/api/wienerlinien/stops", {
lat: String(fallbackCoords.lat),
lng: String(fallbackCoords.lng)
});
const res = await fetch(url);
if (!res.ok) throw new Error(`Station search failed: ${res.status}`);
return res.json();
const result = await res.json();
return result.stops || [];
}
}