docs: 基本完成初版数据库设计
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import { DynamicModule, Global, Module } from '@nestjs/common';
|
||||
import {
|
||||
getRuntimeOptions,
|
||||
type RuntimeOptions,
|
||||
} from '../common/runtime-options';
|
||||
|
||||
/** 注入已经校验的配置,业务模块无需直接读取 process.env。 */
|
||||
export const SERVER_CONFIG = Symbol('SERVER_CONFIG');
|
||||
/** API 契约的主版本前缀,与服务端发布版本、小程序版本分别维护。 */
|
||||
export const API_PREFIX = 'api/v1';
|
||||
export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
|
||||
|
||||
/** 规范化的启动配置;仅在服务端使用,包含不得下发或记录的 JWT 密钥。 */
|
||||
export interface ServerConfig extends RuntimeOptions {
|
||||
nodeEnv: 'development' | 'test' | 'production';
|
||||
logLevel: LogLevel;
|
||||
bodyLimitBytes: number;
|
||||
swaggerEnabled: boolean;
|
||||
corsOrigins: string[];
|
||||
trustedProxyCidrs: string[];
|
||||
jwtSecret: string;
|
||||
jwtIssuer: string;
|
||||
jwtAudience: string;
|
||||
jwtExpiresInSeconds: number;
|
||||
}
|
||||
|
||||
/** 按十进制整数字符串解析并检查范围,防止空串、科学计数法等被 Number 宽松接受。 */
|
||||
function integer(
|
||||
env: NodeJS.ProcessEnv,
|
||||
key: string,
|
||||
fallback: number,
|
||||
min: number,
|
||||
max: number,
|
||||
): number {
|
||||
const raw = env[key] ?? String(fallback);
|
||||
const value = Number(raw);
|
||||
if (
|
||||
!/^\d+$/.test(raw) ||
|
||||
!Number.isSafeInteger(value) ||
|
||||
value < min ||
|
||||
value > max
|
||||
) {
|
||||
throw new Error(`${key} must be an integer between ${min} and ${max}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** 启动前一次性校验环境变量;错误消息只包含配置项要求,不回显实际配置值。 */
|
||||
export function getServerConfig(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): ServerConfig {
|
||||
const nodeEnv = env.NODE_ENV ?? 'development';
|
||||
if (!['development', 'test', 'production'].includes(nodeEnv)) {
|
||||
throw new Error('NODE_ENV must be development, test or production');
|
||||
}
|
||||
const logLevel = env.LOG_LEVEL ?? 'info';
|
||||
if (!['error', 'warn', 'info', 'debug'].includes(logLevel)) {
|
||||
throw new Error('LOG_LEVEL must be error, warn, info or debug');
|
||||
}
|
||||
// 未显式配置时生产关闭文档,避免沿用开发环境的公开文档入口。
|
||||
const rawSwagger =
|
||||
env.SWAGGER_ENABLED ?? (nodeEnv === 'production' ? 'false' : 'true');
|
||||
if (!['true', 'false'].includes(rawSwagger)) {
|
||||
throw new Error('SWAGGER_ENABLED must be true or false');
|
||||
}
|
||||
// 密钥没有默认值;按字节检查长度,并拒绝示例中的占位密码。
|
||||
const jwtSecret = env.JWT_SECRET ?? '';
|
||||
if (
|
||||
Buffer.byteLength(jwtSecret.trim()) < 32 ||
|
||||
jwtSecret.includes('change-me')
|
||||
) {
|
||||
throw new Error(
|
||||
'JWT_SECRET must be a configured secret of at least 32 bytes',
|
||||
);
|
||||
}
|
||||
const jwtIssuer = (env.JWT_ISSUER ?? 'doodle-store').trim();
|
||||
const jwtAudience = (env.JWT_AUDIENCE ?? 'doodle-mini').trim();
|
||||
if (!jwtIssuer || !jwtAudience) {
|
||||
throw new Error('JWT_ISSUER and JWT_AUDIENCE must not be empty');
|
||||
}
|
||||
// 只接受可直接比较的完整 origin;路径、凭证、通配符不属于白名单项。
|
||||
const corsOrigins = (env.CORS_ORIGINS ?? '')
|
||||
.split(',')
|
||||
.map((origin) => origin.trim())
|
||||
.filter(Boolean);
|
||||
for (const origin of corsOrigins) {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(origin);
|
||||
} catch {
|
||||
throw new Error('CORS_ORIGINS must contain HTTP(S) origins');
|
||||
}
|
||||
if (
|
||||
!['http:', 'https:'].includes(url.protocol) ||
|
||||
url.origin !== origin
|
||||
) {
|
||||
throw new Error(
|
||||
'CORS_ORIGINS must contain exact HTTP(S) origins without paths',
|
||||
);
|
||||
}
|
||||
}
|
||||
const trustedProxyCidrs = (env.TRUSTED_PROXY_CIDRS ?? '')
|
||||
.split(',')
|
||||
.map((cidr) => cidr.trim())
|
||||
.filter(Boolean);
|
||||
// Express 在安装 trust proxy 时校验具体网段;不接受 true 或 hop count。
|
||||
if (
|
||||
trustedProxyCidrs.some(
|
||||
(value) => ['true', 'false'].includes(value) || /^\d+$/.test(value),
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'TRUSTED_PROXY_CIDRS must contain explicit proxy addresses or CIDRs',
|
||||
);
|
||||
}
|
||||
return {
|
||||
...getRuntimeOptions(env),
|
||||
nodeEnv: nodeEnv as ServerConfig['nodeEnv'],
|
||||
logLevel: logLevel as LogLevel,
|
||||
bodyLimitBytes: integer(
|
||||
env,
|
||||
'API_BODY_LIMIT_BYTES',
|
||||
1_048_576,
|
||||
1024,
|
||||
10 * 1024 * 1024,
|
||||
),
|
||||
swaggerEnabled: rawSwagger === 'true',
|
||||
corsOrigins,
|
||||
trustedProxyCidrs,
|
||||
jwtSecret,
|
||||
jwtIssuer,
|
||||
jwtAudience,
|
||||
jwtExpiresInSeconds: integer(
|
||||
env,
|
||||
'JWT_EXPIRES_IN_SECONDS',
|
||||
86_400,
|
||||
60,
|
||||
604_800,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@Global()
|
||||
@Module({})
|
||||
/** 将同一份配置提供给所有模块;测试可传入独立配置,避免修改进程环境。 */
|
||||
export class ServerConfigModule {
|
||||
static forRoot(config: ServerConfig): DynamicModule {
|
||||
return {
|
||||
module: ServerConfigModule,
|
||||
providers: [{ provide: SERVER_CONFIG, useValue: config }],
|
||||
exports: [SERVER_CONFIG],
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user