- Docker: Multi-stage Dockerfile with security hardening, docker-compose for production and development environments - Environment: Comprehensive .env.example with all config options, lib/config/env.ts for typed environment validation - Logging: Structured JSON logging with request/response middleware - Monitoring: Prometheus metrics endpoint, Grafana dashboard, health checks (liveness/readiness probes) - Security: Security headers, rate limiting, CORS middleware - CI/CD: GitHub Actions workflows for CI, production deploy, and preview deployments - Error tracking: Sentry integration foundation Files created: - Docker: Dockerfile, docker-compose.yml, docker-compose.dev.yml, .dockerignore - Config: lib/config/env.ts, lib/config/index.ts - Logging: lib/logging/logger.ts, lib/logging/middleware.ts - Monitoring: lib/monitoring/sentry.ts, lib/monitoring/metrics.ts, lib/monitoring/health.ts - Security: lib/security/headers.ts, lib/security/rateLimit.ts, lib/security/cors.ts - API: pages/api/health/*, pages/api/metrics.ts - Infra: infra/prometheus/prometheus.yml, infra/grafana/*
280 lines
8.2 KiB
TypeScript
280 lines
8.2 KiB
TypeScript
/**
|
|
* Environment Configuration
|
|
* Agent 4: Production Deployment
|
|
*
|
|
* Validates and exports environment variables with type safety.
|
|
* Throws errors in production if required variables are missing.
|
|
*/
|
|
|
|
type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace';
|
|
type LogFormat = 'json' | 'pretty';
|
|
type PrivacyMode = 'standard' | 'enhanced' | 'maximum';
|
|
type LocationObfuscation = 'none' | 'fuzzy' | 'region' | 'hidden';
|
|
type StorageProvider = 'local' | 's3' | 'r2' | 'minio';
|
|
|
|
interface EnvConfig {
|
|
// Application
|
|
nodeEnv: 'development' | 'production' | 'test';
|
|
port: number;
|
|
apiUrl: string;
|
|
appName: string;
|
|
|
|
// Database
|
|
databaseUrl: string;
|
|
dbHost: string;
|
|
dbPort: number;
|
|
dbUser: string;
|
|
dbPassword: string;
|
|
dbName: string;
|
|
|
|
// Redis
|
|
redisUrl: string;
|
|
redisHost: string;
|
|
redisPort: number;
|
|
|
|
// Authentication
|
|
nextAuthUrl: string;
|
|
nextAuthSecret: string;
|
|
githubClientId?: string;
|
|
githubClientSecret?: string;
|
|
googleClientId?: string;
|
|
googleClientSecret?: string;
|
|
|
|
// Sentry
|
|
sentryDsn?: string;
|
|
sentryOrg?: string;
|
|
sentryProject?: string;
|
|
sentryAuthToken?: string;
|
|
|
|
// Logging
|
|
logLevel: LogLevel;
|
|
logFormat: LogFormat;
|
|
|
|
// Monitoring
|
|
prometheusEnabled: boolean;
|
|
metricsPort: number;
|
|
|
|
// Plants.net
|
|
plantsNetApiKey?: string;
|
|
|
|
// Tor
|
|
torEnabled: boolean;
|
|
torSocksHost: string;
|
|
torSocksPort: number;
|
|
torControlPort: number;
|
|
torHiddenServiceDir: string;
|
|
|
|
// Privacy
|
|
defaultPrivacyMode: PrivacyMode;
|
|
allowAnonymousRegistration: boolean;
|
|
locationObfuscationDefault: LocationObfuscation;
|
|
|
|
// Storage
|
|
storageProvider: StorageProvider;
|
|
s3Bucket?: string;
|
|
s3Region?: string;
|
|
s3AccessKeyId?: string;
|
|
s3SecretAccessKey?: string;
|
|
s3Endpoint?: string;
|
|
|
|
// Email
|
|
smtpHost: string;
|
|
smtpPort: number;
|
|
smtpUser?: string;
|
|
smtpPassword?: string;
|
|
smtpFrom: string;
|
|
|
|
// Rate Limiting
|
|
rateLimitWindowMs: number;
|
|
rateLimitMaxRequests: number;
|
|
|
|
// Security
|
|
corsOrigins: string[];
|
|
cspReportUri?: string;
|
|
|
|
// Feature Flags
|
|
isProduction: boolean;
|
|
isDevelopment: boolean;
|
|
isTest: boolean;
|
|
}
|
|
|
|
function getEnvString(key: string, defaultValue?: string): string {
|
|
const value = process.env[key] ?? defaultValue;
|
|
if (value === undefined) {
|
|
if (process.env.NODE_ENV === 'production') {
|
|
throw new Error(`Missing required environment variable: ${key}`);
|
|
}
|
|
return '';
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function getEnvNumber(key: string, defaultValue: number): number {
|
|
const value = process.env[key];
|
|
if (value === undefined) {
|
|
return defaultValue;
|
|
}
|
|
const parsed = parseInt(value, 10);
|
|
if (isNaN(parsed)) {
|
|
throw new Error(`Environment variable ${key} must be a number, got: ${value}`);
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function getEnvBoolean(key: string, defaultValue: boolean): boolean {
|
|
const value = process.env[key];
|
|
if (value === undefined) {
|
|
return defaultValue;
|
|
}
|
|
return value.toLowerCase() === 'true' || value === '1';
|
|
}
|
|
|
|
function getEnvArray(key: string, defaultValue: string[] = []): string[] {
|
|
const value = process.env[key];
|
|
if (value === undefined || value === '') {
|
|
return defaultValue;
|
|
}
|
|
return value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
}
|
|
|
|
function validateLogLevel(value: string): LogLevel {
|
|
const validLevels: LogLevel[] = ['error', 'warn', 'info', 'debug', 'trace'];
|
|
if (!validLevels.includes(value as LogLevel)) {
|
|
return 'info';
|
|
}
|
|
return value as LogLevel;
|
|
}
|
|
|
|
function validateLogFormat(value: string): LogFormat {
|
|
return value === 'pretty' ? 'pretty' : 'json';
|
|
}
|
|
|
|
function validatePrivacyMode(value: string): PrivacyMode {
|
|
const validModes: PrivacyMode[] = ['standard', 'enhanced', 'maximum'];
|
|
if (!validModes.includes(value as PrivacyMode)) {
|
|
return 'standard';
|
|
}
|
|
return value as PrivacyMode;
|
|
}
|
|
|
|
function validateLocationObfuscation(value: string): LocationObfuscation {
|
|
const validModes: LocationObfuscation[] = ['none', 'fuzzy', 'region', 'hidden'];
|
|
if (!validModes.includes(value as LocationObfuscation)) {
|
|
return 'fuzzy';
|
|
}
|
|
return value as LocationObfuscation;
|
|
}
|
|
|
|
function validateStorageProvider(value: string): StorageProvider {
|
|
const validProviders: StorageProvider[] = ['local', 's3', 'r2', 'minio'];
|
|
if (!validProviders.includes(value as StorageProvider)) {
|
|
return 'local';
|
|
}
|
|
return value as StorageProvider;
|
|
}
|
|
|
|
function validateNodeEnv(value: string): 'development' | 'production' | 'test' {
|
|
if (value === 'production' || value === 'test') {
|
|
return value;
|
|
}
|
|
return 'development';
|
|
}
|
|
|
|
/**
|
|
* Load and validate environment configuration
|
|
*/
|
|
function loadEnv(): EnvConfig {
|
|
const nodeEnv = validateNodeEnv(process.env.NODE_ENV || 'development');
|
|
|
|
return {
|
|
// Application
|
|
nodeEnv,
|
|
port: getEnvNumber('PORT', 3001),
|
|
apiUrl: getEnvString('NEXT_PUBLIC_API_URL', 'http://localhost:3001'),
|
|
appName: getEnvString('NEXT_PUBLIC_APP_NAME', 'LocalGreenChain'),
|
|
|
|
// Database
|
|
databaseUrl: getEnvString('DATABASE_URL', 'postgresql://lgc:lgc_password@localhost:5432/localgreenchain'),
|
|
dbHost: getEnvString('DB_HOST', 'localhost'),
|
|
dbPort: getEnvNumber('DB_PORT', 5432),
|
|
dbUser: getEnvString('DB_USER', 'lgc'),
|
|
dbPassword: getEnvString('DB_PASSWORD', 'lgc_password'),
|
|
dbName: getEnvString('DB_NAME', 'localgreenchain'),
|
|
|
|
// Redis
|
|
redisUrl: getEnvString('REDIS_URL', 'redis://localhost:6379'),
|
|
redisHost: getEnvString('REDIS_HOST', 'localhost'),
|
|
redisPort: getEnvNumber('REDIS_PORT', 6379),
|
|
|
|
// Authentication
|
|
nextAuthUrl: getEnvString('NEXTAUTH_URL', 'http://localhost:3001'),
|
|
nextAuthSecret: getEnvString('NEXTAUTH_SECRET', 'development-secret-change-in-production'),
|
|
githubClientId: process.env.GITHUB_CLIENT_ID,
|
|
githubClientSecret: process.env.GITHUB_CLIENT_SECRET,
|
|
googleClientId: process.env.GOOGLE_CLIENT_ID,
|
|
googleClientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
|
|
// Sentry
|
|
sentryDsn: process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN,
|
|
sentryOrg: process.env.SENTRY_ORG,
|
|
sentryProject: process.env.SENTRY_PROJECT,
|
|
sentryAuthToken: process.env.SENTRY_AUTH_TOKEN,
|
|
|
|
// Logging
|
|
logLevel: validateLogLevel(getEnvString('LOG_LEVEL', 'info')),
|
|
logFormat: validateLogFormat(getEnvString('LOG_FORMAT', 'json')),
|
|
|
|
// Monitoring
|
|
prometheusEnabled: getEnvBoolean('PROMETHEUS_ENABLED', false),
|
|
metricsPort: getEnvNumber('METRICS_PORT', 9091),
|
|
|
|
// Plants.net
|
|
plantsNetApiKey: process.env.PLANTS_NET_API_KEY,
|
|
|
|
// Tor
|
|
torEnabled: getEnvBoolean('TOR_ENABLED', false),
|
|
torSocksHost: getEnvString('TOR_SOCKS_HOST', '127.0.0.1'),
|
|
torSocksPort: getEnvNumber('TOR_SOCKS_PORT', 9050),
|
|
torControlPort: getEnvNumber('TOR_CONTROL_PORT', 9051),
|
|
torHiddenServiceDir: getEnvString('TOR_HIDDEN_SERVICE_DIR', '/var/lib/tor/localgreenchain'),
|
|
|
|
// Privacy
|
|
defaultPrivacyMode: validatePrivacyMode(getEnvString('DEFAULT_PRIVACY_MODE', 'standard')),
|
|
allowAnonymousRegistration: getEnvBoolean('ALLOW_ANONYMOUS_REGISTRATION', true),
|
|
locationObfuscationDefault: validateLocationObfuscation(getEnvString('LOCATION_OBFUSCATION_DEFAULT', 'fuzzy')),
|
|
|
|
// Storage
|
|
storageProvider: validateStorageProvider(getEnvString('STORAGE_PROVIDER', 'local')),
|
|
s3Bucket: process.env.S3_BUCKET,
|
|
s3Region: process.env.S3_REGION,
|
|
s3AccessKeyId: process.env.S3_ACCESS_KEY_ID,
|
|
s3SecretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
|
s3Endpoint: process.env.S3_ENDPOINT,
|
|
|
|
// Email
|
|
smtpHost: getEnvString('SMTP_HOST', 'localhost'),
|
|
smtpPort: getEnvNumber('SMTP_PORT', 1025),
|
|
smtpUser: process.env.SMTP_USER,
|
|
smtpPassword: process.env.SMTP_PASSWORD,
|
|
smtpFrom: getEnvString('SMTP_FROM', 'noreply@localgreenchain.local'),
|
|
|
|
// Rate Limiting
|
|
rateLimitWindowMs: getEnvNumber('RATE_LIMIT_WINDOW_MS', 60000),
|
|
rateLimitMaxRequests: getEnvNumber('RATE_LIMIT_MAX_REQUESTS', 100),
|
|
|
|
// Security
|
|
corsOrigins: getEnvArray('CORS_ORIGINS', ['http://localhost:3001']),
|
|
cspReportUri: process.env.CSP_REPORT_URI,
|
|
|
|
// Feature Flags
|
|
isProduction: nodeEnv === 'production',
|
|
isDevelopment: nodeEnv === 'development',
|
|
isTest: nodeEnv === 'test',
|
|
};
|
|
}
|
|
|
|
// Export singleton config
|
|
export const env = loadEnv();
|
|
|
|
// Re-export types
|
|
export type { EnvConfig, LogLevel, LogFormat, PrivacyMode, LocationObfuscation, StorageProvider };
|