refactor(config): add env parsing helpers and tighten startup validation
This commit is contained in:
@@ -9,6 +9,9 @@ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/friendolls_dev?schem
|
|||||||
# Redis
|
# Redis
|
||||||
REDIS_HOST=localhost
|
REDIS_HOST=localhost
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
REDIS_REQUIRED=false
|
||||||
|
REDIS_CONNECT_TIMEOUT_MS=5000
|
||||||
|
REDIS_STARTUP_RETRIES=10
|
||||||
|
|
||||||
# JWT Configuration
|
# JWT Configuration
|
||||||
JWT_SECRET=replace-with-strong-random-secret
|
JWT_SECRET=replace-with-strong-random-secret
|
||||||
|
|||||||
@@ -12,13 +12,24 @@ import { RedisModule } from './database/redis.module';
|
|||||||
import { WsModule } from './ws/ws.module';
|
import { WsModule } from './ws/ws.module';
|
||||||
import { FriendsModule } from './friends/friends.module';
|
import { FriendsModule } from './friends/friends.module';
|
||||||
import { DollsModule } from './dolls/dolls.module';
|
import { DollsModule } from './dolls/dolls.module';
|
||||||
|
import { parseRedisRequired } from './common/config/env.utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates required environment variables.
|
* Validates required environment variables.
|
||||||
* Throws an error if any required variables are missing or invalid.
|
* Throws an error if any required variables are missing or invalid.
|
||||||
* Returns the validated config.
|
* Returns the validated config.
|
||||||
*/
|
*/
|
||||||
function validateEnvironment(config: Record<string, any>): Record<string, any> {
|
function getOptionalEnvString(
|
||||||
|
config: Record<string, unknown>,
|
||||||
|
key: string,
|
||||||
|
): string | undefined {
|
||||||
|
const value = config[key];
|
||||||
|
return typeof value === 'string' ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateEnvironment(
|
||||||
|
config: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> {
|
||||||
const requiredVars = ['JWT_SECRET', 'DATABASE_URL'];
|
const requiredVars = ['JWT_SECRET', 'DATABASE_URL'];
|
||||||
|
|
||||||
const missingVars = requiredVars.filter((varName) => !config[varName]);
|
const missingVars = requiredVars.filter((varName) => !config[varName]);
|
||||||
@@ -30,10 +41,44 @@ function validateEnvironment(config: Record<string, any>): Record<string, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate PORT if provided
|
// Validate PORT if provided
|
||||||
if (config.PORT && isNaN(Number(config.PORT))) {
|
if (config.PORT !== undefined && !Number.isFinite(Number(config.PORT))) {
|
||||||
throw new Error('PORT must be a valid number');
|
throw new Error('PORT must be a valid number');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.NODE_ENV === 'production') {
|
||||||
|
if (
|
||||||
|
typeof config.JWT_SECRET !== 'string' ||
|
||||||
|
config.JWT_SECRET.length < 32
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'JWT_SECRET must be at least 32 characters in production',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const redisRequired = parseRedisRequired({
|
||||||
|
nodeEnv: getOptionalEnvString(config, 'NODE_ENV'),
|
||||||
|
redisRequired: getOptionalEnvString(config, 'REDIS_REQUIRED'),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (redisRequired && !config.REDIS_HOST) {
|
||||||
|
throw new Error(
|
||||||
|
'REDIS_REQUIRED is enabled but REDIS_HOST is not configured',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const redisConnectTimeout = getOptionalEnvString(
|
||||||
|
config,
|
||||||
|
'REDIS_CONNECT_TIMEOUT_MS',
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
redisConnectTimeout !== undefined &&
|
||||||
|
(!Number.isFinite(Number(redisConnectTimeout)) ||
|
||||||
|
Number(redisConnectTimeout) <= 0)
|
||||||
|
) {
|
||||||
|
throw new Error('REDIS_CONNECT_TIMEOUT_MS must be a positive number');
|
||||||
|
}
|
||||||
|
|
||||||
validateOptionalProvider(config, 'GOOGLE');
|
validateOptionalProvider(config, 'GOOGLE');
|
||||||
validateOptionalProvider(config, 'DISCORD');
|
validateOptionalProvider(config, 'DISCORD');
|
||||||
|
|
||||||
@@ -41,7 +86,7 @@ function validateEnvironment(config: Record<string, any>): Record<string, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateOptionalProvider(
|
function validateOptionalProvider(
|
||||||
config: Record<string, any>,
|
config: Record<string, unknown>,
|
||||||
provider: 'GOOGLE' | 'DISCORD',
|
provider: 'GOOGLE' | 'DISCORD',
|
||||||
): void {
|
): void {
|
||||||
const vars = [
|
const vars = [
|
||||||
|
|||||||
66
src/common/config/env.utils.ts
Normal file
66
src/common/config/env.utils.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
export function parseBoolean(
|
||||||
|
value: string | undefined,
|
||||||
|
fallback: boolean,
|
||||||
|
): boolean {
|
||||||
|
if (value === undefined) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = value.trim().toLowerCase();
|
||||||
|
if (['true', '1', 'yes', 'y', 'on'].includes(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['false', '0', 'no', 'n', 'off'].includes(normalized)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parsePositiveInteger(
|
||||||
|
value: string | undefined,
|
||||||
|
fallback: number,
|
||||||
|
): number {
|
||||||
|
if (!value) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.floor(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseCsvList(value: string | undefined): string[] {
|
||||||
|
if (!value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter((item) => item.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLikelyHttpOrigin(origin: string): boolean {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(origin);
|
||||||
|
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRedisRequired(config: {
|
||||||
|
nodeEnv?: string;
|
||||||
|
redisRequired?: string;
|
||||||
|
}): boolean {
|
||||||
|
if (config.redisRequired === undefined) {
|
||||||
|
return config.nodeEnv === 'production';
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseBoolean(config.redisRequired, false);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user