diff --git a/.env.example b/.env.example index 5315d10..43ec90c 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,4 @@ -DATABASE_URL=postgres://eussi:password@localhost:5432/issue \ No newline at end of file +DATABASE_URL=postgres://eussi:password@localhost:5432/issue + +# comma separated list of allowed origins +CORS_ORIGIN=http://localhost:1420 \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 2bdc454..791a0d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,36 +6,103 @@ import { createDemoData } from "./utils"; const DEV = process.argv.find((arg) => ["--dev", "--developer", "-d"].includes(arg.toLowerCase())) != null; const PORT = process.argv.find((arg) => arg.toLowerCase().startsWith("--port="))?.split("=")[1] || 0; +type RouteHandler = (req: T) => Response | Promise; + +const CORS_ALLOWED_ORIGINS = (process.env.CORS_ORIGIN ?? "http://localhost:1420") + .split(",") + .map((origin) => origin.trim()) + .filter(Boolean); + +const CORS_ALLOW_METHODS = process.env.CORS_ALLOW_METHODS ?? "GET,POST,PUT,PATCH,DELETE,OPTIONS"; +const CORS_ALLOW_HEADERS_DEFAULT = process.env.CORS_ALLOW_HEADERS ?? "Content-Type, Authorization"; +const CORS_MAX_AGE = process.env.CORS_MAX_AGE ?? "86400"; + +const getCorsAllowOrigin = (req: Request) => { + const requestOrigin = req.headers.get("Origin"); + if (!requestOrigin) { + return "*"; + } + + if (CORS_ALLOWED_ORIGINS.includes("*")) { + return "*"; + } + + if (CORS_ALLOWED_ORIGINS.includes(requestOrigin)) { + return requestOrigin; + } + + return null; +}; + +const buildCorsHeaders = (req: Request) => { + const headers = new Headers(); + + const allowOrigin = getCorsAllowOrigin(req); + if (allowOrigin) { + headers.set("Access-Control-Allow-Origin", allowOrigin); + if (allowOrigin !== "*") { + headers.set("Vary", "Origin"); + } + } + + headers.set("Access-Control-Allow-Methods", CORS_ALLOW_METHODS); + + const requestedHeaders = req.headers.get("Access-Control-Request-Headers"); + headers.set("Access-Control-Allow-Headers", requestedHeaders || CORS_ALLOW_HEADERS_DEFAULT); + + headers.set("Access-Control-Max-Age", CORS_MAX_AGE); + + return headers; +}; + +const withCors = (handler: RouteHandler): RouteHandler => { + return async (req: T) => { + const corsHeaders = buildCorsHeaders(req); + + if (req.method === "OPTIONS") { + return new Response(null, { status: 204, headers: corsHeaders }); + } + + const res = await handler(req); + const wrapped = new Response(res.body, res); + + corsHeaders.forEach((value, key) => { + wrapped.headers.set(key, value); + }); + + return wrapped; + }; +}; + const main = async () => { const server = Bun.serve({ port: Number(PORT), routes: { - "/": () => new Response(`title: eussi\ndev-mode: ${DEV}\nport: ${PORT}`), - "/issue/create": routes.issueCreate, - "/issue/update": routes.issueUpdate, - "/issue/delete": routes.issueDelete, - "/issues/:projectBlob": routes.issuesInProject, - "/issues/all": routes.issues, + "/": withCors(() => new Response(`title: eussi\ndev-mode: ${DEV}\nport: ${PORT}`)), - "/project/create": routes.projectCreate, - "/project/update": routes.projectUpdate, - "/project/delete": routes.projectDelete, + "/issue/create": withCors(routes.issueCreate), + "/issue/update": withCors(routes.issueUpdate), + "/issue/delete": withCors(routes.issueDelete), + "/issues/:projectBlob": withCors(routes.issuesInProject), + "/issues/all": withCors(routes.issues), + + "/project/create": withCors(routes.projectCreate), + "/project/update": withCors(routes.projectUpdate), + "/project/delete": withCors(routes.projectDelete), }, }); console.log(`eussi (issue server) listening on ${server.url}`); await testDB(); - let users = await db.select().from(User); - - if (DEV && users.length === 0) { - console.log("creating demo data..."); - await createDemoData(); - console.log("demo data created"); - users = await db.select().from(User); + if (DEV) { + const users = await db.select().from(User); + if (users.length === 0) { + console.log("creating demo data..."); + await createDemoData(); + console.log("demo data created"); + } } - - console.log(`serving ${users.length} user${users.length === 1 ? "" : "s"}`); }; main();