/** * Configuration validation and environment variable management */ import dotenv from "dotenv"; import crypto from "crypto"; import path from "path"; dotenv.config(); interface Config { port: number; nodeEnv: string; databaseUrl?: string; frontendUrl?: string; jwtSecret: string; jwtAccessExpiresIn: string; jwtRefreshExpiresIn: string; rateLimitMaxRequests: number; csrfMaxRequests: number; csrfSecret: string | null; // Feature flags - all default to false for backward compatibility enablePasswordReset: boolean; enableRefreshTokenRotation: boolean; enableAuditLogging: boolean; } const getRequiredEnv = (key: string): string => { const value = process.env[key]; if (!value || value.trim().length === 0) { throw new Error(`Missing required environment variable: ${key}`); } return value; }; const getOptionalEnv = (key: string, defaultValue: string): string => { return process.env[key] || defaultValue; }; const resolveJwtSecret = (nodeEnv: string): string => { const provided = process.env.JWT_SECRET; if (provided && provided.trim().length > 0) { return provided; } if (nodeEnv === "production") { throw new Error("Missing required environment variable: JWT_SECRET"); } const generated = crypto.randomBytes(32).toString("hex"); console.warn( "[security] JWT_SECRET is not set (non-production). Using an ephemeral secret; tokens will be invalidated on restart." ); return generated; }; const parseFrontendUrl = (raw: string | undefined): string | undefined => { if (!raw || raw.trim().length === 0) return undefined; const first = raw.split(",")[0]?.trim(); if (!first) return undefined; try { // Validate basic format new URL(/^https?:\/\//i.test(first) ? first : `http://${first}`); } catch { // Don't hard-fail; FRONTEND_URL supports multiple origins in other parts of the app. return first; } return first; }; const resolveDatabaseUrl = (rawUrl?: string) => { const backendRoot = path.resolve(__dirname, "../"); const defaultDbPath = path.resolve(backendRoot, "prisma/dev.db"); if (!rawUrl || rawUrl.trim().length === 0) { return `file:${defaultDbPath}`; } if (!rawUrl.startsWith("file:")) { return rawUrl; } const filePath = rawUrl.replace(/^file:/, ""); const prismaDir = path.resolve(backendRoot, "prisma"); const normalizedRelative = filePath.replace(/^\.\/?/, ""); const hasLeadingPrismaDir = normalizedRelative === "prisma" || normalizedRelative.startsWith("prisma/"); const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(hasLeadingPrismaDir ? backendRoot : prismaDir, normalizedRelative); return `file:${absolutePath}`; }; // Ensure DATABASE_URL is resolved before any PrismaClient is created. process.env.DATABASE_URL = resolveDatabaseUrl(process.env.DATABASE_URL); const getOptionalBoolean = (key: string, defaultValue: boolean): boolean => { const value = process.env[key]; if (!value) return defaultValue; return value.toLowerCase() === "true" || value === "1"; }; const getRequiredEnvNumber = (key: string, defaultValue: number): number => { const value = process.env[key]; if (!value) return defaultValue; const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0) { throw new Error(`Invalid value for environment variable ${key}: must be a positive number`); } return parsed; }; export const config: Config = { port: getRequiredEnvNumber("PORT", 8000), nodeEnv: getOptionalEnv("NODE_ENV", "development"), databaseUrl: process.env.DATABASE_URL, frontendUrl: parseFrontendUrl(process.env.FRONTEND_URL), jwtSecret: resolveJwtSecret(getOptionalEnv("NODE_ENV", "development")), jwtAccessExpiresIn: getOptionalEnv("JWT_ACCESS_EXPIRES_IN", "15m"), jwtRefreshExpiresIn: getOptionalEnv("JWT_REFRESH_EXPIRES_IN", "7d"), rateLimitMaxRequests: getRequiredEnvNumber("RATE_LIMIT_MAX_REQUESTS", 1000), csrfMaxRequests: getRequiredEnvNumber("CSRF_MAX_REQUESTS", 60), csrfSecret: process.env.CSRF_SECRET || null, // Feature flags - disabled by default for backward compatibility enablePasswordReset: getOptionalBoolean("ENABLE_PASSWORD_RESET", false), enableRefreshTokenRotation: getOptionalBoolean("ENABLE_REFRESH_TOKEN_ROTATION", false), enableAuditLogging: getOptionalBoolean("ENABLE_AUDIT_LOGGING", false), }; // Validate JWT_SECRET strength in production if (config.nodeEnv === "production") { if (config.jwtSecret.length < 32) { throw new Error("JWT_SECRET must be at least 32 characters long in production"); } if (config.jwtSecret === "your-secret-key-change-in-production") { throw new Error("JWT_SECRET must be changed from default value in production"); } } console.log("Configuration validated successfully");