Files
livedash-node/lib/env.ts
Kaj Kowalski e1abedb148 feat: implement cache layer, CSP improvements, and database performance optimizations
- Add Redis cache implementation with LRU eviction
- Enhance Content Security Policy with nonce generation
- Optimize database queries with connection pooling
- Add cache invalidation API endpoints
- Improve security monitoring performance
2025-07-13 11:52:49 +02:00

217 lines
6.4 KiB
TypeScript

// Centralized environment variable management
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
/**
* Parse environment variable value by removing quotes, comments, and trimming whitespace
*/
function parseEnvValue(value: string | undefined): string {
if (!value) return "";
// Trim whitespace
let cleaned = value.trim();
// Remove inline comments (everything after #)
const commentIndex = cleaned.indexOf("#");
if (commentIndex !== -1) {
cleaned = cleaned.substring(0, commentIndex).trim();
}
// Remove surrounding quotes (both single and double)
if (
(cleaned.startsWith('"') && cleaned.endsWith('"')) ||
(cleaned.startsWith("'") && cleaned.endsWith("'"))
) {
cleaned = cleaned.slice(1, -1);
}
return cleaned;
}
/**
* Parse integer with fallback to default value
*/
function parseIntWithDefault(
value: string | undefined,
defaultValue: number
): number {
const cleaned = parseEnvValue(value);
if (!cleaned) return defaultValue;
const parsed = Number.parseInt(cleaned, 10);
return Number.isNaN(parsed) ? defaultValue : parsed;
}
// Load environment variables from .env.local
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const envPath = join(__dirname, "..", ".env.local");
// Load .env.local if it exists
try {
const envFile = readFileSync(envPath, "utf8");
const envVars = envFile
.split("\n")
.filter((line) => line.trim() && !line.startsWith("#"));
envVars.forEach((line) => {
const [key, ...valueParts] = line.split("=");
if (key && valueParts.length > 0) {
const rawValue = valueParts.join("=");
const cleanedValue = parseEnvValue(rawValue);
if (!process.env[key.trim()]) {
process.env[key.trim()] = cleanedValue;
}
}
});
} catch (_error) {
// Silently fail if .env.local doesn't exist
}
/**
* Typed environment variables with defaults
*/
export const env = {
// NextAuth
NEXTAUTH_URL:
parseEnvValue(process.env.NEXTAUTH_URL) || "http://localhost:3000",
NEXTAUTH_SECRET: parseEnvValue(process.env.NEXTAUTH_SECRET) || "",
NODE_ENV: parseEnvValue(process.env.NODE_ENV) || "development",
// CSRF Protection
CSRF_SECRET: (() => {
const csrfSecret = parseEnvValue(process.env.CSRF_SECRET);
const nextAuthSecret = parseEnvValue(process.env.NEXTAUTH_SECRET);
if (csrfSecret) return csrfSecret;
if (nextAuthSecret) return nextAuthSecret;
throw new Error(
"CSRF_SECRET or NEXTAUTH_SECRET is required for security. Please set one of these environment variables."
);
})(),
// OpenAI
OPENAI_API_KEY: parseEnvValue(process.env.OPENAI_API_KEY) || "",
OPENAI_MOCK_MODE: parseEnvValue(process.env.OPENAI_MOCK_MODE) === "true",
// Scheduler Configuration
SCHEDULER_ENABLED: parseEnvValue(process.env.SCHEDULER_ENABLED) === "true",
CSV_IMPORT_INTERVAL:
parseEnvValue(process.env.CSV_IMPORT_INTERVAL) || "*/15 * * * *",
IMPORT_PROCESSING_INTERVAL:
parseEnvValue(process.env.IMPORT_PROCESSING_INTERVAL) || "*/5 * * * *",
IMPORT_PROCESSING_BATCH_SIZE: parseIntWithDefault(
process.env.IMPORT_PROCESSING_BATCH_SIZE,
50
),
SESSION_PROCESSING_INTERVAL:
parseEnvValue(process.env.SESSION_PROCESSING_INTERVAL) || "0 * * * *",
SESSION_PROCESSING_BATCH_SIZE: parseIntWithDefault(
process.env.SESSION_PROCESSING_BATCH_SIZE,
0
),
SESSION_PROCESSING_CONCURRENCY: parseIntWithDefault(
process.env.SESSION_PROCESSING_CONCURRENCY,
5
),
// Database Configuration
DATABASE_URL: parseEnvValue(process.env.DATABASE_URL) || "",
DATABASE_URL_DIRECT: parseEnvValue(process.env.DATABASE_URL_DIRECT) || "",
// Database Connection Pooling
DATABASE_CONNECTION_LIMIT: parseIntWithDefault(
process.env.DATABASE_CONNECTION_LIMIT,
20
),
DATABASE_POOL_TIMEOUT: parseIntWithDefault(
process.env.DATABASE_POOL_TIMEOUT,
10
),
// Redis Configuration (optional - graceful fallback to in-memory if not provided)
REDIS_URL: parseEnvValue(process.env.REDIS_URL) || "",
REDIS_TTL_DEFAULT: parseIntWithDefault(process.env.REDIS_TTL_DEFAULT, 300), // 5 minutes default
REDIS_TTL_SESSION: parseIntWithDefault(process.env.REDIS_TTL_SESSION, 1800), // 30 minutes
REDIS_TTL_USER: parseIntWithDefault(process.env.REDIS_TTL_USER, 900), // 15 minutes
REDIS_TTL_COMPANY: parseIntWithDefault(process.env.REDIS_TTL_COMPANY, 600), // 10 minutes
// Server
PORT: parseIntWithDefault(process.env.PORT, 3000),
} as const;
/**
* Validate required environment variables
*/
export function validateEnv(): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (!env.DATABASE_URL) {
errors.push("DATABASE_URL is required");
}
if (!env.NEXTAUTH_SECRET) {
errors.push("NEXTAUTH_SECRET is required");
}
// CSRF_SECRET validation is now handled in the IIFE above
// If we reach here, CSRF_SECRET is guaranteed to be set
if (
!env.OPENAI_API_KEY &&
env.NODE_ENV === "production" &&
!env.OPENAI_MOCK_MODE
) {
errors.push(
"OPENAI_API_KEY is required in production (unless OPENAI_MOCK_MODE is enabled)"
);
}
return {
valid: errors.length === 0,
errors,
};
}
/**
* Get scheduler configuration from environment variables
*/
export function getSchedulerConfig() {
return {
enabled: env.SCHEDULER_ENABLED,
csvImport: {
interval: env.CSV_IMPORT_INTERVAL,
},
importProcessing: {
interval: env.IMPORT_PROCESSING_INTERVAL,
batchSize: env.IMPORT_PROCESSING_BATCH_SIZE,
},
sessionProcessing: {
interval: env.SESSION_PROCESSING_INTERVAL,
batchSize: env.SESSION_PROCESSING_BATCH_SIZE,
concurrency: env.SESSION_PROCESSING_CONCURRENCY,
},
};
}
/**
* Log environment configuration (safe for production)
*/
export function logEnvConfig(): void {
console.log("[Environment] Configuration:");
console.log(` NODE_ENV: ${env.NODE_ENV}`);
console.log(` NEXTAUTH_URL: ${env.NEXTAUTH_URL}`);
console.log(` SCHEDULER_ENABLED: ${env.SCHEDULER_ENABLED}`);
console.log(` OPENAI_MOCK_MODE: ${env.OPENAI_MOCK_MODE}`);
console.log(` PORT: ${env.PORT}`);
if (env.SCHEDULER_ENABLED) {
console.log(" Scheduler intervals:");
console.log(` CSV Import: ${env.CSV_IMPORT_INTERVAL}`);
console.log(` Import Processing: ${env.IMPORT_PROCESSING_INTERVAL}`);
console.log(` Session Processing: ${env.SESSION_PROCESSING_INTERVAL}`);
}
}