diff --git a/backend/.env.example b/backend/.env.example index 71c20bf..867c39a 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -2,7 +2,8 @@ PORT=8000 NODE_ENV=production DATABASE_URL=file:/app/prisma/dev.db -FRONTEND_URL=http://localhost:6767 +FRONTEND_URL=https://draw.louiscreates.com +API_BASE_PATH=/api # Keep disabled unless traffic always comes through a trusted reverse proxy. TRUST_PROXY=false AUTH_MODE=local diff --git a/backend/src/config.ts b/backend/src/config.ts index 99d395c..ddb60f7 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -12,6 +12,7 @@ interface Config { nodeEnv: string; databaseUrl?: string; frontendUrl?: string; + apiBasePath: string; authMode: AuthMode; jwtSecret: string; jwtAccessExpiresIn: string; @@ -82,6 +83,22 @@ const parseFrontendUrl = (raw: string | undefined): string | undefined => { return normalized.length > 0 ? normalized : undefined; }; +const parseApiBasePath = (raw: string | undefined): string => { + const fallback = "/api"; + if (!raw || raw.trim().length === 0) return fallback; + + const trimmed = raw.trim(); + if (trimmed === "/") return "/"; + + const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`; + const withoutTrailingSlash = + withLeadingSlash.length > 1 && withLeadingSlash.endsWith("/") + ? withLeadingSlash.slice(0, -1) + : withLeadingSlash; + + return withoutTrailingSlash.length > 0 ? withoutTrailingSlash : fallback; +}; + const resolveDatabaseUrl = (rawUrl?: string) => { const backendRoot = path.resolve(__dirname, "../"); const defaultDbPath = path.resolve(backendRoot, "prisma/dev.db"); @@ -189,6 +206,7 @@ export const config: Config = { nodeEnv: getOptionalEnv("NODE_ENV", "development"), databaseUrl: process.env.DATABASE_URL, frontendUrl: parseFrontendUrl(process.env.FRONTEND_URL), + apiBasePath: parseApiBasePath(process.env.API_BASE_PATH), authMode: resolvedAuthMode, jwtSecret: resolveJwtSecret(getOptionalEnv("NODE_ENV", "development")), jwtAccessExpiresIn: getOptionalEnv("JWT_ACCESS_EXPIRES_IN", "15m"), diff --git a/backend/src/index.ts b/backend/src/index.ts index 23da4ba..b0aabfc 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -59,6 +59,7 @@ const normalizeOrigins = (rawOrigins?: string | null): string[] => { const allowedOrigins = normalizeOrigins(config.frontendUrl); console.log("Allowed origins:", allowedOrigins); +console.log("API base path:", config.apiBasePath); const isDev = (process.env.NODE_ENV || "development") !== "production"; const isLocalDevOrigin = (origin: string): boolean => { @@ -132,6 +133,10 @@ if (trustProxyValue === true) { const httpServer = createServer(app); const io = new Server(httpServer, { + path: + config.apiBasePath === "/" + ? "/socket.io" + : `${config.apiBasePath}/socket.io`, cors: { origin: (origin, cb) => cb(null, isAllowedOrigin(origin ?? undefined)), credentials: true, @@ -329,15 +334,18 @@ const generalRateLimiter = rateLimit({ app.use(generalRateLimiter); +const apiApp = express(); +app.use(config.apiBasePath, apiApp); + registerCsrfProtection({ - app, + app: apiApp, isAllowedOrigin, maxRequestsPerWindow: config.csrfMaxRequests, enableDebugLogging: process.env.DEBUG_CSRF === "true", }); // Authentication routes (no CSRF required, uses JWT) -app.use("/auth", authRouter); +apiApp.use("/auth", authRouter); // Files field can contain arbitrary file metadata, so we use unknown and validate structure const filesFieldSchema = z @@ -556,13 +564,13 @@ registerSocketHandlers({ jwtSecret: config.jwtSecret, }); -app.get("/health", (req, res) => { +apiApp.get("/health", (req, res) => { res.status(200).json({ status: "ok" }); }); // Health check endpoint doesn't require auth -registerDashboardRoutes(app, { +registerDashboardRoutes(apiApp, { prisma, requireAuth, asyncHandler, @@ -584,7 +592,7 @@ registerDashboardRoutes(app, { }); registerImportExportRoutes({ - app, + app: apiApp, prisma, requireAuth, asyncHandler, @@ -622,5 +630,8 @@ if (isMain) { console.log(`Server running on port ${PORT}`); console.log(`Environment: ${config.nodeEnv}`); console.log(`Frontend URL: ${config.frontendUrl}`); + console.log( + `API endpoints: ${config.apiBasePath === "/" ? "/" : `${config.apiBasePath}/`}*` + ); }); }