Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import { AuthRateLimitMiddleware } from './auth/middleware/auth.middleware';
isGlobal: true,
load: [configuration, valuationConfig],
envFilePath: ['.env.local', '.env.development', '.env'],
cache: true, // Enable configuration caching
expandVariables: true, // Allow environment variable expansion
}),
ConfigurationModule,

Expand Down
129 changes: 129 additions & 0 deletions src/config/config.loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import * as Joi from 'joi';
import { ConfigEncryptionUtil } from './utils/config.encryption';
import { configValidationSchema } from './validation/config.validation';

/**
* Configuration loader with validation and environment-specific management
*/
export class ConfigLoader {
/**
* Load and validate configuration from environment variables
* @param env Environment variables object (defaults to process.env)
* @returns Validated configuration object
*/
static load(env: NodeJS.ProcessEnv = process.env): any {
// Process the environment variables for encryption
const processedEnv = this.processEncryptedValues(env);

// Validate the configuration
const { error, value: validatedConfig } = configValidationSchema.validate(processedEnv, {
abortEarly: false, // Return all validation errors
stripUnknown: true, // Remove unknown keys
convert: true, // Convert values to appropriate types
});

if (error) {
const validationErrors = error.details.map(detail => detail.message);
throw new Error(`Configuration validation failed:\n${validationErrors.join('\n')}`);
}

// Add helper methods to the configuration
return {
...validatedConfig,
// Helper to check environment
isDevelopment: validatedConfig.NODE_ENV === 'development',
isProduction: validatedConfig.NODE_ENV === 'production',
isStaging: validatedConfig.NODE_ENV === 'staging',
isTest: validatedConfig.NODE_ENV === 'test',

// Helper to get environment-specific values
getEnvValue: (key: string, defaultValue?: any) => {
return validatedConfig[key] || defaultValue;
},

// Parse allowed file types from string to array
ALLOWED_FILE_TYPES: validatedConfig.ALLOWED_FILE_TYPES?.split(',').map((type: string) => type.trim()) || [],
};
}

/**
* Process environment variables for encrypted values
* @param env Environment variables object
* @returns Processed environment variables with decrypted values
*/
private static processEncryptedValues(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const processedEnv = { ...env };

// Define which environment variables should be treated as sensitive
const sensitiveKeys = [
'JWT_SECRET',
'JWT_REFRESH_SECRET',
'ENCRYPTION_KEY',
'PRIVATE_KEY',
'REDIS_PASSWORD',
'SESSION_SECRET',
'STORAGE_SIGNING_SECRET',
'S3_ACCESS_KEY_ID',
'S3_SECRET_ACCESS_KEY',
'SMTP_PASS',
'ZILLOW_API_KEY',
'REDFIN_API_KEY',
'CORE_LOGIC_API_KEY',
'MAXMIND_LICENSE_KEY',
'MARKET_TRENDS_API_KEY',
'COINGECKO_API_KEY',
'OPENSEA_API_KEY',
'ETHERSCAN_API_KEY',
'WEB3_STORAGE_TOKEN',
'IPFS_PROJECT_ID',
'IPFS_PROJECT_SECRET',
];

const encryptionKey = env.ENCRYPTION_KEY;

if (!encryptionKey) {
console.warn('ENCRYPTION_KEY not found in environment. Values will not be decrypted.');
return processedEnv;
}

if (encryptionKey.length !== 32) {
console.warn('ENCRYPTION_KEY must be exactly 32 characters. Values will not be decrypted.');
return processedEnv;
}

// Process each sensitive key
for (const key of sensitiveKeys) {
const value = processedEnv[key];
if (value && ConfigEncryptionUtil.isEncrypted(value)) {
try {
processedEnv[key] = ConfigEncryptionUtil.decrypt(value, encryptionKey);
} catch (error) {
console.error(`Failed to decrypt ${key}:`, error.message);
throw new Error(`Failed to decrypt ${key}: ${error.message}`);
}
}
}

return processedEnv;
}

/**
* Get environment-specific configuration file path
* @param env Environment name
* @returns Path to environment-specific config file
*/
static getEnvironmentConfigPath(env: string): string {
const baseDir = process.cwd();
switch (env.toLowerCase()) {
case 'production':
return `${baseDir}/config/production.env`;
case 'staging':
return `${baseDir}/config/staging.env`;
case 'test':
return `${baseDir}/config/test.env`;
case 'development':
default:
return `${baseDir}/.env.local`;
}
}
}
136 changes: 136 additions & 0 deletions src/config/configuration.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ export class ConfigurationService {
return this.configService.get<number>('THROTTLE_LIMIT');
}

get apiKeyRateLimitPerMinute(): number {
return this.configService.get<number>('API_KEY_RATE_LIMIT_PER_MINUTE');
}

// File Upload
get maxFileSize(): number {
return this.configService.get<number>('MAX_FILE_SIZE');
Expand Down Expand Up @@ -204,6 +208,133 @@ export class ConfigurationService {
return this.configService.get<boolean>('ENABLE_SEED_DATA');
}

// Storage Configuration
get storageProvider(): 's3' | 'memory' {
return this.configService.get<'s3' | 'memory'>('STORAGE_PROVIDER');
}

get storageSignedUrlExpiresIn(): number {
return this.configService.get<number>('STORAGE_SIGNED_URL_EXPIRES_IN');
}

get storageSigningSecret(): string {
return this.configService.get<string>('STORAGE_SIGNING_SECRET');
}

get thumbnailWidth(): number {
return this.configService.get<number>('THUMBNAIL_WIDTH');
}

get thumbnailHeight(): number {
return this.configService.get<number>('THUMBNAIL_HEIGHT');
}

get thumbnailFormat(): 'jpeg' | 'png' | 'webp' {
return this.configService.get<'jpeg' | 'png' | 'webp'>('THUMBNAIL_FORMAT');
}

get thumbnailQuality(): number {
return this.configService.get<number>('THUMBNAIL_QUALITY');
}

// S3 Configuration
get s3Bucket(): string {
return this.configService.get<string>('S3_BUCKET');
}

get s3Region(): string {
return this.configService.get<string>('S3_REGION');
}

get s3AccessKeyId(): string | undefined {
return this.configService.get<string>('S3_ACCESS_KEY_ID');
}

get s3SecretAccessKey(): string | undefined {
return this.configService.get<string>('S3_SECRET_ACCESS_KEY');
}

get s3Endpoint(): string | undefined {
return this.configService.get<string>('S3_ENDPOINT');
}

get s3ForcePathStyle(): boolean {
return this.configService.get<boolean>('S3_FORCE_PATH_STYLE');
}

// Valuation Configuration
get zillowApiKey(): string | undefined {
return this.configService.get<string>('ZILLOW_API_KEY');
}

get redfinApiKey(): string | undefined {
return this.configService.get<string>('REDFIN_API_KEY');
}

get coreLogicApiKey(): string | undefined {
return this.configService.get<string>('CORE_LOGIC_API_KEY');
}

get maxmindLicenseKey(): string | undefined {
return this.configService.get<string>('MAXMIND_LICENSE_KEY');
}

get valuationConfidenceThreshold(): number {
return this.configService.get<number>('VALUATION_CONFIDENCE_THRESHOLD');
}

get valuationCacheTtl(): number {
return this.configService.get<number>('VALUATION_CACHE_TTL');
}

get valuationMaxRetries(): number {
return this.configService.get<number>('VALUATION_MAX_RETRIES');
}

get valuationTimeout(): number {
return this.configService.get<number>('VALUATION_TIMEOUT');
}

get marketTrendsApiEndpoint(): string | undefined {
return this.configService.get<string>('MARKET_TRENDS_API_ENDPOINT');
}

get marketTrendsApiKey(): string | undefined {
return this.configService.get<string>('MARKET_TRENDS_API_KEY');
}

get marketTrendsUpdateFreq(): number {
return this.configService.get<number>('MARKET_TRENDS_UPDATE_FREQ');
}

get valuationRateLimitPerMinute(): number {
return this.configService.get<number>('VALUATION_RATE_LIMIT_PER_MINUTE');
}

get valuationRateLimitPerHour(): number {
return this.configService.get<number>('VALUATION_RATE_LIMIT_PER_HOUR');
}

get locationWeight(): number {
return this.configService.get<number>('LOCATION_WEIGHT');
}

get sizeWeight(): number {
return this.configService.get<number>('SIZE_WEIGHT');
}

get ageWeight(): number {
return this.configService.get<number>('AGE_WEIGHT');
}

get amenitiesWeight(): number {
return this.configService.get<number>('AMENITIES_WEIGHT');
}

get marketConditionsWeight(): number {
return this.configService.get<number>('MARKET_CONDITIONS_WEIGHT');
}

// Helper method to check if we're in development
get isDevelopment(): boolean {
return this.nodeEnv === 'development';
Expand All @@ -218,4 +349,9 @@ export class ConfigurationService {
get isTest(): boolean {
return this.nodeEnv === 'test';
}

// Helper method to check if we're in staging
get isStaging(): boolean {
return this.nodeEnv === 'staging';
}
}
91 changes: 5 additions & 86 deletions src/config/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,6 @@
import { JoiSchemaConfig } from './interfaces/joi-schema-config.interface';
import { ConfigLoader } from './config.loader';

export default (): JoiSchemaConfig => ({
// Application
NODE_ENV: process.env.NODE_ENV || 'development',
PORT: parseInt(process.env.PORT, 10) || 3000,
HOST: process.env.HOST || '0.0.0.0',
API_PREFIX: process.env.API_PREFIX || 'api',
CORS_ORIGIN: process.env.CORS_ORIGIN || '*',
SWAGGER_ENABLED: process.env.SWAGGER_ENABLED !== 'false',

// Database
DATABASE_URL: process.env.DATABASE_URL || 'postgresql://postgres:password@localhost:5432/propchain',

// Redis
REDIS_HOST: process.env.REDIS_HOST || 'localhost',
REDIS_PORT: parseInt(process.env.REDIS_PORT, 10) || 6379,
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
REDIS_DB: parseInt(process.env.REDIS_DB, 10) || 0,

// JWT
JWT_SECRET: process.env.JWT_SECRET || 'your-super-secret-jwt-key',
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '24h',
JWT_REFRESH_SECRET: process.env.JWT_REFRESH_SECRET || 'your-super-secret-refresh-key',
JWT_REFRESH_EXPIRES_IN: process.env.JWT_REFRESH_EXPIRES_IN || '7d',

// API Keys
API_KEY: process.env.API_KEY,
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY || 'your-32-char-encryption-key-here',

// Blockchain/Web3
BLOCKCHAIN_NETWORK: process.env.BLOCKCHAIN_NETWORK || 'sepolia',
RPC_URL: process.env.RPC_URL || 'https://sepolia.infura.io/v3/YOUR_PROJECT_ID',
PRIVATE_KEY: process.env.PRIVATE_KEY,
ETHERSCAN_API_KEY: process.env.ETHERSCAN_API_KEY,
WEB3_STORAGE_TOKEN: process.env.WEB3_STORAGE_TOKEN,

// IPFS
IPFS_GATEWAY_URL: process.env.IPFS_GATEWAY_URL || 'https://ipfs.io/ipfs/',
IPFS_API_URL: process.env.IPFS_API_URL || 'https://ipfs.infura.io:5001',
IPFS_PROJECT_ID: process.env.IPFS_PROJECT_ID,
IPFS_PROJECT_SECRET: process.env.IPFS_PROJECT_SECRET,

// Rate Limiting
THROTTLE_TTL: parseInt(process.env.THROTTLE_TTL, 10) || 60,
THROTTLE_LIMIT: parseInt(process.env.THROTTLE_LIMIT, 10) || 10,

// API Key Rate Limiting
API_KEY_RATE_LIMIT_PER_MINUTE: parseInt(process.env.API_KEY_RATE_LIMIT_PER_MINUTE, 10) || 60,

// File Upload
MAX_FILE_SIZE: parseInt(process.env.MAX_FILE_SIZE, 10) || 10 * 1024 * 1024, // 10MB
ALLOWED_FILE_TYPES: process.env.ALLOWED_FILE_TYPES?.split(',') || [
'image/jpeg',
'image/png',
'image/webp',
'application/pdf',
],

// Email (for notifications)
SMTP_HOST: process.env.SMTP_HOST,
SMTP_PORT: parseInt(process.env.SMTP_PORT, 10) || 587,
SMTP_USER: process.env.SMTP_USER,
SMTP_PASS: process.env.SMTP_PASS,
EMAIL_FROM: process.env.EMAIL_FROM || 'noreply@propchain.io',

// Monitoring
SENTRY_DSN: process.env.SENTRY_DSN,
LOG_LEVEL: process.env.LOG_LEVEL || 'info',

// External Services
COINGECKO_API_KEY: process.env.COINGECKO_API_KEY,
OPENSEA_API_KEY: process.env.OPENSEA_API_KEY,

// Smart Contracts
PROPERTY_NFT_ADDRESS: process.env.PROPERTY_NFT_ADDRESS,
ESCROW_CONTRACT_ADDRESS: process.env.ESCROW_CONTRACT_ADDRESS,
GOVERNANCE_CONTRACT_ADDRESS: process.env.GOVERNANCE_CONTRACT_ADDRESS,

// Security
BCRYPT_ROUNDS: parseInt(process.env.BCRYPT_ROUNDS, 10) || 12,
SESSION_SECRET: process.env.SESSION_SECRET || 'your-session-secret-key',

// Development
MOCK_BLOCKCHAIN: process.env.MOCK_BLOCKCHAIN === 'true',
ENABLE_SEED_DATA: process.env.ENABLE_SEED_DATA === 'true',
});
// Export the validated configuration using our new loader
export default () => {
return ConfigLoader.load();
};
Loading
Loading