import { existsSync } from 'node:fs'; import { resolve } from 'node:path'; import { Global, Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { z } from 'zod'; export const APP_CONFIG = Symbol('APP_CONFIG'); function findEnvironmentFile(): string { const directories = [ process.cwd(), resolve(process.cwd(), '..'), resolve(process.cwd(), '../..'), ]; const environmentFile = directories .map((directory) => resolve(directory, '.env')) .find((file) => existsSync(file)); return environmentFile ?? resolve(process.cwd(), '.env'); } // 此 schema 是 API 运行配置的唯一来源;默认值只允许用于不含秘密的本地开发场景。 const environmentSchema = z.object({ NODE_ENV: z .enum(['development', 'test', 'production']) .default('development'), API_PORT: z.coerce.number().int().min(1).max(65_535).default(8200), WEB_ORIGIN: z.url().default('http://localhost:8100'), DATABASE_URL: z.url(), REDIS_URL: z.url(), AUTH_JWT_SECRET: z.string().min(32), AUTH_RATE_LIMIT_KEY_SECRET: z.string().min(32), STORE_SECRET_ENCRYPTION_KEY: z.string().min(32), STORE_SECRET_ENCRYPTION_KEY_VERSION: z .string() .min(1) .max(64) .default('v1'), IDEMPOTENCY_KEY_SECRET: z.string().min(32), AUTH_PASSWORD_MIN_LENGTH: z.coerce .number() .int() .min(12) .max(256) .default(12), API_BODY_LIMIT_BYTES: z.coerce .number() .int() .min(1_024) .max(10 * 1024 * 1024) .default(1_048_576), AUTH_RATE_LIMIT_WINDOW_SECONDS: z.coerce .number() .int() .min(1) .max(86_400) .default(900), AUTH_LOGIN_IP_LIMIT: z.coerce.number().int().min(1).max(1000).default(10), AUTH_LOGIN_EMAIL_LIMIT: z.coerce.number().int().min(1).max(1000).default(5), AUTH_REGISTER_IP_LIMIT: z.coerce.number().int().min(1).max(1000).default(5), AUTH_REGISTER_EMAIL_LIMIT: z.coerce .number() .int() .min(1) .max(1000) .default(3), AUTH_REFRESH_IP_LIMIT: z.coerce .number() .int() .min(1) .max(10_000) .default(30), AUTH_CHANGE_PASSWORD_IP_LIMIT: z.coerce .number() .int() .min(1) .max(1_000) .default(10), AUTH_CHANGE_PASSWORD_USER_LIMIT: z.coerce .number() .int() .min(1) .max(1_000) .default(5), TRUSTED_PROXY_CIDRS: z.string().default(''), SWAGGER_ENABLED: z.enum(['true', 'false']).default('true'), LOG_LEVEL: z.enum(['error', 'warn', 'log', 'debug']).default('log'), }); type Environment = z.infer; export type AppConfig = Omit< Environment, 'TRUSTED_PROXY_CIDRS' | 'SWAGGER_ENABLED' > & { trustedProxyCidrs: string[]; swaggerEnabled: boolean; }; function validateEnvironment( environment: Record, ): Environment { const result = environmentSchema.safeParse(environment); if (result.success) return result.data; // 在 Nest 开始监听前失败,避免半配置 API 对外提供不安全服务。 throw new Error( `Invalid environment configuration: ${result.error.issues.map((issue) => issue.path.join('.')).join(', ')}`, ); } function createAppConfig(environment: Environment): AppConfig { return { ...environment, // 只有显式给出代理网段时才信任转发头,避免客户端伪造来源 IP。 trustedProxyCidrs: environment.TRUSTED_PROXY_CIDRS.split(',') .map((value) => value.trim()) .filter(Boolean), swaggerEnabled: environment.SWAGGER_ENABLED === 'true', }; } @Global() @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, cache: true, // Turbo 从 apps/api 运行脚本,但共享开发环境文件位于仓库根目录。 envFilePath: findEnvironmentFile(), }), ], providers: [ { provide: APP_CONFIG, // 导出规范化配置,业务服务不得散落读取 process.env。 useFactory: (): AppConfig => createAppConfig(validateEnvironment(process.env)), }, ], exports: [APP_CONFIG], }) export class AppConfigModule {}