import { sql } from "@/lib/db"; import { requireAuth } from "@/lib/jwt"; // The owner API is cross-origin only for the admin dashboard, so the allowed // origin has to be named explicitly. An unset ADMIN_CORS_ORIGIN used to fall // back to "*", which meant a missing env var silently opened every owner // endpoint to every website the owner happened to have open. Fail closed // instead: with nothing configured we send no allow-origin header at all and // the browser blocks the call, which is a loud, obvious failure to fix. // // A comma-separated list is accepted so dev (localhost) and production can be // configured at once; the header echoes back whichever entry matched, since // "Access-Control-Allow-Origin" only ever takes a single value. const allowedOrigins = (): string[] => (process.env.ADMIN_CORS_ORIGIN ?? "") .split(",") .map((origin) => origin.trim()) .filter(Boolean); export const corsHeaders = (req: Request): Record => { const headers: Record = { "Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", Vary: "Origin", }; const allowed = allowedOrigins(); if (allowed.length === 0) return headers; // A wildcard is still honoured when it is configured deliberately — the // change is that it is no longer what you get by forgetting to configure it. if (allowed.includes("*")) { headers["Access-Control-Allow-Origin"] = "*"; return headers; } const origin = req.headers.get("origin"); if (origin && allowed.includes(origin)) { headers["Access-Control-Allow-Origin"] = origin; } return headers; }; // Takes the request first so the origin it echoes is never accidentally // omitted — a call site that forgets it won't compile. export const withCors = (req: Request, response: Response): Response => { for (const [key, value] of Object.entries(corsHeaders(req))) { response.headers.set(key, value); } return response; }; export const preflight = (req: Request): Response => withCors(req, new Response(null, { status: 204 })); // Returns the authenticated owner or a ready-to-return error Response. export const requireOwner = async ( req: Request, ): Promise<{ userId: string; email: string } | { error: Response }> => { const auth = requireAuth(req); if ("error" in auth) return auth; const rows = await sql<{ role: string | null }>` SELECT role FROM users WHERE id = ${auth.userId} `; if (rows[0]?.role !== "owner") { return { error: Response.json({ error: "Forbidden." }, { status: 403 }), }; } return auth; };