Transactional emails: - Add nodemailer SMTP email module with dev-mode console logging - Magic link template and welcome template with HTML + plain text - Wire sendMagicLink into login flow, sendWelcome into registration - Update privacy page and deploy docs for SMTP configuration Planner features: - No-go areas: draw polygons on map (leaflet-geoman), synced via Yjs, passed to BRouter as nogos parameter, route recomputes on change - Session notes: collaborative Y.Text textarea in sidebar tab - Crash recovery: periodic localStorage save of Yjs state, restore on reconnect - Rate limit session creation (10/IP/hour) in /new route Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
46 lines
1.5 KiB
TypeScript
46 lines
1.5 KiB
TypeScript
import { data } from "react-router";
|
|
import type { Route } from "./+types/api.route";
|
|
import { computeRoute } from "~/lib/brouter";
|
|
import { checkRateLimit } from "~/lib/rate-limit";
|
|
|
|
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 { waypoints, profile, sessionId, noGoAreas } = body as {
|
|
waypoints: Array<{ lat: number; lon: number }>;
|
|
profile?: string;
|
|
sessionId?: string;
|
|
noGoAreas?: Array<{ points: Array<{ lat: number; lon: number }> }>;
|
|
};
|
|
|
|
if (!waypoints || waypoints.length < 2) {
|
|
return data({ error: "At least 2 waypoints are required" }, { status: 400 });
|
|
}
|
|
|
|
// Rate limit by session ID or IP
|
|
const rateLimitKey = sessionId ?? request.headers.get("x-forwarded-for") ?? "unknown";
|
|
const limit = checkRateLimit(`route:${rateLimitKey}`);
|
|
|
|
if (!limit.allowed) {
|
|
return data(
|
|
{ error: "Rate limit exceeded" },
|
|
{
|
|
status: 429,
|
|
headers: { "Retry-After": String(limit.retryAfterSeconds) },
|
|
},
|
|
);
|
|
}
|
|
|
|
try {
|
|
const route = await computeRoute({ waypoints, profile, noGoAreas });
|
|
return data(route, {
|
|
headers: { "X-RateLimit-Remaining": String(limit.remaining) },
|
|
});
|
|
} catch (e) {
|
|
const message = e instanceof Error ? e.message : "Route computation failed";
|
|
return data({ error: message }, { status: 502 });
|
|
}
|
|
}
|