trails/apps/journal/app/routes/api.routes.$id.callback.ts
Ullrich Schäfer a5d682d671 Fix CORS on Planner→Journal callback endpoint
The Save to Journal button makes a cross-origin POST from
planner.trails.cool to trails.cool. The browser's preflight OPTIONS
request was returning HTML (no CORS headers), blocking the save flow.

Add CORS headers to all responses and handle OPTIONS preflight in a
loader. Origin is restricted to PLANNER_URL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 01:16:32 +00:00

70 lines
2.4 KiB
TypeScript

import { data } from "react-router";
import type { Route } from "./+types/api.routes.$id.callback";
import { verifyRouteToken } from "~/lib/jwt.server";
import { updateRoute, getRoute } from "~/lib/routes.server";
const PLANNER_ORIGIN = process.env.PLANNER_URL ?? "http://localhost:3001";
function corsHeaders() {
return {
"Access-Control-Allow-Origin": PLANNER_ORIGIN,
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
}
export async function loader({ request }: Route.LoaderArgs) {
// Handle CORS preflight
if (request.method === "OPTIONS") {
return new Response(null, { status: 204, headers: corsHeaders() });
}
return data({ error: "Method not allowed" }, { status: 405, headers: corsHeaders() });
}
export async function action({ params, request }: Route.ActionArgs) {
if (request.method !== "POST") {
return data({ error: "Method not allowed" }, { status: 405, headers: corsHeaders() });
}
// Verify JWT token from Authorization header or body
const authHeader = request.headers.get("Authorization");
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
if (!token) {
return data({ error: "Missing authorization token" }, { status: 401, headers: corsHeaders() });
}
try {
const { routeId, permissions } = await verifyRouteToken(token);
// Verify token is for this route
if (routeId !== params.id) {
return data({ error: "Token not valid for this route" }, { status: 403, headers: corsHeaders() });
}
if (!permissions.includes("write")) {
return data({ error: "Token does not have write permission" }, { status: 403, headers: corsHeaders() });
}
// Get route to verify it exists
const route = await getRoute(params.id);
if (!route) {
return data({ error: "Route not found" }, { status: 404, headers: corsHeaders() });
}
// Parse GPX from request body
const body = await request.json();
const { gpx } = body as { gpx: string };
if (!gpx) {
return data({ error: "Missing GPX data" }, { status: 400, headers: corsHeaders() });
}
// Update route with new GPX (creates new version)
await updateRoute(params.id, route.ownerId, { gpx });
return data({ success: true, routeId: params.id }, { headers: corsHeaders() });
} catch (e) {
return data({ error: (e as Error).message }, { status: 401, headers: corsHeaders() });
}
}