From 381dd9554321f6624ca51d18c2d0774b88a10404 Mon Sep 17 00:00:00 2001 From: Matteo Date: Sat, 24 Jan 2026 17:11:50 +0100 Subject: [PATCH] feat(config): add feature flags for optional security features - Add enablePasswordReset, enableRefreshTokenRotation, enableAuditLogging flags - All flags default to false for backward compatibility - Add getOptionalBoolean helper for parsing boolean env vars - Update .env.example with feature flag documentation --- backend/.env.example | 8 +++- backend/src/config.ts | 87 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 backend/src/config.ts diff --git a/backend/.env.example b/backend/.env.example index da110e9..61a0957 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -2,4 +2,10 @@ PORT=8000 NODE_ENV=production DATABASE_URL=file:/app/prisma/dev.db -FRONTEND_URL=http://localhost:6767 \ No newline at end of file +FRONTEND_URL=http://localhost:6767 + +# Optional Feature Flags (all default to false for backward compatibility) +# Set to "true" or "1" to enable: +# ENABLE_PASSWORD_RESET=false +# ENABLE_REFRESH_TOKEN_ROTATION=false +# ENABLE_AUDIT_LOGGING=false \ No newline at end of file diff --git a/backend/src/config.ts b/backend/src/config.ts new file mode 100644 index 0000000..7f92e13 --- /dev/null +++ b/backend/src/config.ts @@ -0,0 +1,87 @@ +/** + * Configuration validation and environment variable management + */ +import dotenv from "dotenv"; + +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 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: getRequiredEnv("DATABASE_URL"), + frontendUrl: getOptionalEnv("FRONTEND_URL", "http://localhost:6767"), + jwtSecret: getRequiredEnv("JWT_SECRET"), + 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"); + } +} + +// Validate frontend URL format +try { + new URL(config.frontendUrl); +} catch { + throw new Error(`Invalid FRONTEND_URL format: ${config.frontendUrl}`); +} + +console.log("Configuration validated successfully"); \ No newline at end of file