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
80 changes: 80 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,19 @@
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.0",
"@nestjs/terminus": "^11.0.0",
"@nestjs/throttler": "^6.5.0",
"@prisma/client": "^6.15.0",
"@turf/turf": "^7.2.0",
"cache-manager": "^7.0.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"compression": "^1.8.1",
"connect-redis": "^9.0.0",
"cookie-parser": "^1.4.7",
"cross-env": "^7.0.3",
"date-fns": "^4.1.0",
"express-session": "^1.18.1",
"helmet": "^8.3.0",
"jsonwebtoken": "^9.0.2",
"redis": "^5.6.0",
"reflect-metadata": "^0.2.2",
Expand Down
84 changes: 84 additions & 0 deletions src/common/config/config-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Canonical config keys as const objects to eliminate stringly-typed typos.
* Bootstrap/shared consumers use these in Phase 2; services are migrated in
* their own phases.
*/
export const ConfigKey = {
Database: {
Url: 'DATABASE_URL',
},
Redis: {
Url: 'REDIS_URL',
OtpUrl: 'OTP_REDIS_URL',
SessionUrl: 'SESSION_REDIS_URL',
},
Session: {
Secret: 'SESSION_SECRET',
Secure: 'COOKIE_SECURE',
SameSite: 'COOKIE_SAMESITE',
Domain: 'COOKIE_DOMAIN',
MaxAge: 'COOKIE_MAX_AGE',
},
Auth: {
CookieSecret: 'COOKIE_SECRET',
MaxSendAttempts: 'MAX_SEND_ATTEMPTS',
MaxCheckAttempts: 'MAX_CHECK_ATTEMPTS',
SendWindow: 'SEND_WINDOW',
BaseBlockTime: 'BASE_BLOCK_TIME',
OtpExpirationTime: 'OTP_EXPIRATION_TIME',
JwtAccessSecret: 'JWT_ACCESS_SECRET_KEY',
JwtTempSecret: 'JWT_TEMP_SECRET_KEY',
JwtProgressSecret: 'JWT_PROGRESS_SECRET_KEY',
},
Pricing: {
BasePrice: 'PRICING_BASE_PRICE',
FuelRate: 'PRICING_FUEL_RATE',
WeightBaseRate: 'PRICING_WEIGHT_BASE_RATE',
PlatformCommission: 'PRICING_PLATFORM_COMMISSION',
DriverShare: 'PRICING_DRIVER_SHARE',
FragileMultiplier: 'PRICING_FRAGILE_MULTIPLIER',
PerishableMultiplier: 'PRICING_PERISHABLE_MULTIPLIER',
BothFragilePerishable: 'PRICING_BOTH_FRAGILE_PERISHABLE',
MajorCityOrigin: 'PRICING_MAJOR_CITY_ORIGIN',
MajorCityDestination: 'PRICING_MAJOR_CITY_DESTINATION',
BothMajorCities: 'PRICING_BOTH_MAJOR_CITIES',
SmallCityFactor: 'PRICING_SMALL_CITY_FACTOR',
DeviationRate: 'PRICING_DEVIATION_RATE',
TimeDeviationRate: 'PRICING_TIME_DEVIATION_RATE',
Tier1Rate: 'PRICING_TIER_1_RATE',
Tier2Rate: 'PRICING_TIER_2_RATE',
Tier3Rate: 'PRICING_TIER_3_RATE',
Tier4Rate: 'PRICING_TIER_4_RATE',
Tier5Rate: 'PRICING_TIER_5_RATE',
MajorCities: 'PRICING_MAJOR_CITIES',
CorridorWidth: 'CORRIDOR_WIDTH',
},
Aws: {
AccessKey: 'AWS_ACCESS_KEY',
SecretKey: 'AWS_SECRET_KEY',
BucketName: 'AWS_BUCKET_NAME',
Endpoint: 'AWS_ENDPOINT',
Region: 'AWS_REGION',
},
Map: {
ApiKey: 'MAP_API_KEY',
ApiUrl: 'MAP_API_URL',
},
Sms: {
ApiKey: 'SMS_API_KEY',
},
Throttle: {
Ttl: 'THROTTLE_TTL',
Limit: 'THROTTLE_LIMIT',
},
Server: {
Port: 'PORT',
ApiPrefix: 'API_PREFIX',
LogLevel: 'LOG_LEVEL',
NodeEnv: 'NODE_ENV',
CorsOrigins: 'CORS_ORIGINS',
TimeoutMs: 'TIMEOUT_MS',
ResponseWrapperEnabled: 'RESPONSE_WRAPPER_ENABLED',
MemoryHeapThresholdMb: 'MEMORY_HEAP_THRESHOLD_MB',
},
} as const;
93 changes: 93 additions & 0 deletions src/common/config/env.validation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import 'reflect-metadata';
import { validateEnv, EnvSchema } from './env.validation';

const makeFullEnv = () => ({
DATABASE_URL: 'postgresql://postgres:postgres@localhost:5432/hambaar-db',
REDIS_URL: 'redis://localhost:6379',
OTP_REDIS_URL: 'redis://localhost:6379/1',
SESSION_REDIS_URL: 'redis://localhost:6379/2',
SESSION_SECRET: 'a'.repeat(16),
COOKIE_SECRET: 'b'.repeat(16),
JWT_ACCESS_SECRET_KEY: 'c'.repeat(16),
JWT_TEMP_SECRET_KEY: 'd'.repeat(16),
JWT_PROGRESS_SECRET_KEY: 'e'.repeat(16),
AWS_ACCESS_KEY: 'aws-access-key',
AWS_SECRET_KEY: 'aws-secret-key',
AWS_BUCKET_NAME: 'aws-bucket',
AWS_ENDPOINT: 'https://storage.example.com',
MAP_API_KEY: 'map-api-key',
MAP_API_URL: 'https://api.example.com',
SMS_API_KEY: 'sms-api-key',
});

describe('validateEnv', () => {
it('passes with a valid full env', () => {
const result = validateEnv(makeFullEnv());
expect(result).toBeDefined();
});

describe('required keys', () => {
const requiredKeys = [
'DATABASE_URL',
'REDIS_URL',
'OTP_REDIS_URL',
'SESSION_REDIS_URL',
'SESSION_SECRET',
'COOKIE_SECRET',
'JWT_ACCESS_SECRET_KEY',
'JWT_TEMP_SECRET_KEY',
'JWT_PROGRESS_SECRET_KEY',
'AWS_ACCESS_KEY',
'AWS_SECRET_KEY',
'AWS_BUCKET_NAME',
'AWS_ENDPOINT',
'MAP_API_KEY',
'MAP_API_URL',
'SMS_API_KEY',
];

it.each(requiredKeys)('fails when %s is missing', (key) => {
const env = makeFullEnv();
delete (env as any)[key];
expect(() => validateEnv(env)).toThrow(/Invalid environment configuration/);
});
});

it('fails on invalid value types (PORT=abc)', () => {
const env = { ...makeFullEnv(), PORT: 'abc' };
expect(() => validateEnv(env)).toThrow(/PORT/);
});

it('fails when JWT secrets are too short (<16 chars)', () => {
const env = { ...makeFullEnv(), JWT_ACCESS_SECRET_KEY: 'short' };
expect(() => validateEnv(env)).toThrow(/JWT_ACCESS_SECRET_KEY/);
});

it('passes when optional keys are absent', () => {
const env = makeFullEnv();
const result = validateEnv(env);
expect(result.PORT).toBeUndefined();
});

it('accepts a valid CORS_ORIGINS list', () => {
const env = {
...makeFullEnv(),
CORS_ORIGINS: 'https://a.example.com,https://b.example.com',
};
expect(() => validateEnv(env)).not.toThrow();
});

it('fails on an invalid CORS_ORIGINS entry', () => {
const env = { ...makeFullEnv(), CORS_ORIGINS: 'not-a-url' };
expect(() => validateEnv(env)).toThrow(/CORS_ORIGINS/);
});

it('does not read the trailing-space MAX_SEND_ATTEMPTS key', () => {
// The canonical key (no trailing space) is what the schema reads.
const env: any = makeFullEnv();
env['MAX_SEND_ATTEMPTS'] = 5;
env['MAX_SEND_ATTEMPTS '] = 99;
const result = validateEnv(env);
expect(result.MAX_SEND_ATTEMPTS).toBe(5);
});
});
Loading
Loading