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>
This commit is contained in:
Ullrich Schäfer 2026-03-25 02:14:04 +01:00 committed by GitHub
parent 77d785ccde
commit a5d682d671

View file

@ -3,9 +3,27 @@ import type { Route } from "./+types/api.routes.$id.callback";
import { verifyRouteToken } from "~/lib/jwt.server"; import { verifyRouteToken } from "~/lib/jwt.server";
import { updateRoute, getRoute } from "~/lib/routes.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) { export async function action({ params, request }: Route.ActionArgs) {
if (request.method !== "POST") { if (request.method !== "POST") {
return data({ error: "Method not allowed" }, { status: 405 }); return data({ error: "Method not allowed" }, { status: 405, headers: corsHeaders() });
} }
// Verify JWT token from Authorization header or body // Verify JWT token from Authorization header or body
@ -13,7 +31,7 @@ export async function action({ params, request }: Route.ActionArgs) {
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null; const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
if (!token) { if (!token) {
return data({ error: "Missing authorization token" }, { status: 401 }); return data({ error: "Missing authorization token" }, { status: 401, headers: corsHeaders() });
} }
try { try {
@ -21,17 +39,17 @@ export async function action({ params, request }: Route.ActionArgs) {
// Verify token is for this route // Verify token is for this route
if (routeId !== params.id) { if (routeId !== params.id) {
return data({ error: "Token not valid for this route" }, { status: 403 }); return data({ error: "Token not valid for this route" }, { status: 403, headers: corsHeaders() });
} }
if (!permissions.includes("write")) { if (!permissions.includes("write")) {
return data({ error: "Token does not have write permission" }, { status: 403 }); return data({ error: "Token does not have write permission" }, { status: 403, headers: corsHeaders() });
} }
// Get route to verify it exists // Get route to verify it exists
const route = await getRoute(params.id); const route = await getRoute(params.id);
if (!route) { if (!route) {
return data({ error: "Route not found" }, { status: 404 }); return data({ error: "Route not found" }, { status: 404, headers: corsHeaders() });
} }
// Parse GPX from request body // Parse GPX from request body
@ -39,14 +57,14 @@ export async function action({ params, request }: Route.ActionArgs) {
const { gpx } = body as { gpx: string }; const { gpx } = body as { gpx: string };
if (!gpx) { if (!gpx) {
return data({ error: "Missing GPX data" }, { status: 400 }); return data({ error: "Missing GPX data" }, { status: 400, headers: corsHeaders() });
} }
// Update route with new GPX (creates new version) // Update route with new GPX (creates new version)
await updateRoute(params.id, route.ownerId, { gpx }); await updateRoute(params.id, route.ownerId, { gpx });
return data({ success: true, routeId: params.id }); return data({ success: true, routeId: params.id }, { headers: corsHeaders() });
} catch (e) { } catch (e) {
return data({ error: (e as Error).message }, { status: 401 }); return data({ error: (e as Error).message }, { status: 401, headers: corsHeaders() });
} }
} }