add production stuff

This commit is contained in:
2026-02-12 19:22:40 +01:00
parent 08d2165a70
commit da131834ce
3 changed files with 36 additions and 6 deletions
+2 -1
View File
@@ -2,7 +2,8 @@
PORT=8000 PORT=8000
NODE_ENV=production NODE_ENV=production
DATABASE_URL=file:/app/prisma/dev.db 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. # Keep disabled unless traffic always comes through a trusted reverse proxy.
TRUST_PROXY=false TRUST_PROXY=false
AUTH_MODE=local AUTH_MODE=local
+18
View File
@@ -12,6 +12,7 @@ interface Config {
nodeEnv: string; nodeEnv: string;
databaseUrl?: string; databaseUrl?: string;
frontendUrl?: string; frontendUrl?: string;
apiBasePath: string;
authMode: AuthMode; authMode: AuthMode;
jwtSecret: string; jwtSecret: string;
jwtAccessExpiresIn: string; jwtAccessExpiresIn: string;
@@ -82,6 +83,22 @@ const parseFrontendUrl = (raw: string | undefined): string | undefined => {
return normalized.length > 0 ? normalized : 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 resolveDatabaseUrl = (rawUrl?: string) => {
const backendRoot = path.resolve(__dirname, "../"); const backendRoot = path.resolve(__dirname, "../");
const defaultDbPath = path.resolve(backendRoot, "prisma/dev.db"); const defaultDbPath = path.resolve(backendRoot, "prisma/dev.db");
@@ -189,6 +206,7 @@ export const config: Config = {
nodeEnv: getOptionalEnv("NODE_ENV", "development"), nodeEnv: getOptionalEnv("NODE_ENV", "development"),
databaseUrl: process.env.DATABASE_URL, databaseUrl: process.env.DATABASE_URL,
frontendUrl: parseFrontendUrl(process.env.FRONTEND_URL), frontendUrl: parseFrontendUrl(process.env.FRONTEND_URL),
apiBasePath: parseApiBasePath(process.env.API_BASE_PATH),
authMode: resolvedAuthMode, authMode: resolvedAuthMode,
jwtSecret: resolveJwtSecret(getOptionalEnv("NODE_ENV", "development")), jwtSecret: resolveJwtSecret(getOptionalEnv("NODE_ENV", "development")),
jwtAccessExpiresIn: getOptionalEnv("JWT_ACCESS_EXPIRES_IN", "15m"), jwtAccessExpiresIn: getOptionalEnv("JWT_ACCESS_EXPIRES_IN", "15m"),
+16 -5
View File
@@ -59,6 +59,7 @@ const normalizeOrigins = (rawOrigins?: string | null): string[] => {
const allowedOrigins = normalizeOrigins(config.frontendUrl); const allowedOrigins = normalizeOrigins(config.frontendUrl);
console.log("Allowed origins:", allowedOrigins); console.log("Allowed origins:", allowedOrigins);
console.log("API base path:", config.apiBasePath);
const isDev = (process.env.NODE_ENV || "development") !== "production"; const isDev = (process.env.NODE_ENV || "development") !== "production";
const isLocalDevOrigin = (origin: string): boolean => { const isLocalDevOrigin = (origin: string): boolean => {
@@ -132,6 +133,10 @@ if (trustProxyValue === true) {
const httpServer = createServer(app); const httpServer = createServer(app);
const io = new Server(httpServer, { const io = new Server(httpServer, {
path:
config.apiBasePath === "/"
? "/socket.io"
: `${config.apiBasePath}/socket.io`,
cors: { cors: {
origin: (origin, cb) => cb(null, isAllowedOrigin(origin ?? undefined)), origin: (origin, cb) => cb(null, isAllowedOrigin(origin ?? undefined)),
credentials: true, credentials: true,
@@ -329,15 +334,18 @@ const generalRateLimiter = rateLimit({
app.use(generalRateLimiter); app.use(generalRateLimiter);
const apiApp = express();
app.use(config.apiBasePath, apiApp);
registerCsrfProtection({ registerCsrfProtection({
app, app: apiApp,
isAllowedOrigin, isAllowedOrigin,
maxRequestsPerWindow: config.csrfMaxRequests, maxRequestsPerWindow: config.csrfMaxRequests,
enableDebugLogging: process.env.DEBUG_CSRF === "true", enableDebugLogging: process.env.DEBUG_CSRF === "true",
}); });
// Authentication routes (no CSRF required, uses JWT) // 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 // Files field can contain arbitrary file metadata, so we use unknown and validate structure
const filesFieldSchema = z const filesFieldSchema = z
@@ -556,13 +564,13 @@ registerSocketHandlers({
jwtSecret: config.jwtSecret, jwtSecret: config.jwtSecret,
}); });
app.get("/health", (req, res) => { apiApp.get("/health", (req, res) => {
res.status(200).json({ status: "ok" }); res.status(200).json({ status: "ok" });
}); });
// Health check endpoint doesn't require auth // Health check endpoint doesn't require auth
registerDashboardRoutes(app, { registerDashboardRoutes(apiApp, {
prisma, prisma,
requireAuth, requireAuth,
asyncHandler, asyncHandler,
@@ -584,7 +592,7 @@ registerDashboardRoutes(app, {
}); });
registerImportExportRoutes({ registerImportExportRoutes({
app, app: apiApp,
prisma, prisma,
requireAuth, requireAuth,
asyncHandler, asyncHandler,
@@ -622,5 +630,8 @@ if (isMain) {
console.log(`Server running on port ${PORT}`); console.log(`Server running on port ${PORT}`);
console.log(`Environment: ${config.nodeEnv}`); console.log(`Environment: ${config.nodeEnv}`);
console.log(`Frontend URL: ${config.frontendUrl}`); console.log(`Frontend URL: ${config.frontendUrl}`);
console.log(
`API endpoints: ${config.apiBasePath === "/" ? "/" : `${config.apiBasePath}/`}*`
);
}); });
} }