feat(server): 管理面与成员视图 API + Scalar 在线文档
契约 19 路径全量:引导/会话/改密、engine 桩、keys、overview/logs、payments、 qpu、members、audit、成员视图。鉴权优先、422 信封、登录节流、上游验证接线。
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
export interface Env {
|
||||
/** 本服务监听端口(避开 3000/3001 等占用) */
|
||||
PORT: number;
|
||||
/** PostgreSQL 连接串 */
|
||||
DATABASE_URL: string;
|
||||
/**
|
||||
* 已登记 sk-key 的加密密钥(≥32 字节 base64url)。
|
||||
* 经 HKDF 派生 hmac / aes 子密钥;**入库后不可更换**,否则存量 key_ciphertext 无法解密。
|
||||
*/
|
||||
SK_KEY_ENCRYPTION_SECRET: string;
|
||||
/** 上游 NewAPI 实例根地址(非本服务) */
|
||||
NEWAPI_BASE: string;
|
||||
/** 会话 cookie 是否带 Secure;默认 true。本地 HTTP 调试可设 false */
|
||||
COOKIE_SECURE: boolean;
|
||||
/** 业务日志级别,默认 info */
|
||||
LOG_LEVEL: "debug" | "info" | "warning" | "error" | "fatal";
|
||||
/** 空库 + 二者均存在时静默落库管理员(与引导页双通道) */
|
||||
ADMIN_INIT_USERNAME?: string;
|
||||
ADMIN_INIT_PASSWORD?: string;
|
||||
}
|
||||
|
||||
function parseBool(value: string | undefined, fallback: boolean): boolean {
|
||||
if (value === undefined || value === "") return fallback;
|
||||
const v = value.trim().toLowerCase();
|
||||
if (v === "1" || v === "true" || v === "yes" || v === "on") return true;
|
||||
if (v === "0" || v === "false" || v === "no" || v === "off") return false;
|
||||
throw new Error(`invalid boolean: ${value}`);
|
||||
}
|
||||
|
||||
function parseLogLevel(
|
||||
value: string | undefined,
|
||||
): "debug" | "info" | "warning" | "error" | "fatal" {
|
||||
const v = (value ?? "info").trim().toLowerCase();
|
||||
if (
|
||||
v === "debug" ||
|
||||
v === "info" ||
|
||||
v === "warning" ||
|
||||
v === "error" ||
|
||||
v === "fatal"
|
||||
) {
|
||||
return v;
|
||||
}
|
||||
throw new Error(`LOG_LEVEL must be debug|info|warning|error|fatal`);
|
||||
}
|
||||
|
||||
export function loadEnv(source: Record<string, string | undefined>): Env {
|
||||
const databaseUrl = source.DATABASE_URL;
|
||||
const skKeyEncryptionSecret = source.SK_KEY_ENCRYPTION_SECRET;
|
||||
if (!databaseUrl) throw new Error("DATABASE_URL is required");
|
||||
if (!skKeyEncryptionSecret) {
|
||||
throw new Error("SK_KEY_ENCRYPTION_SECRET is required");
|
||||
}
|
||||
|
||||
const port = Number(source.PORT ?? 18456);
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
throw new Error("PORT must be an integer in 1..65535");
|
||||
}
|
||||
|
||||
return {
|
||||
PORT: port,
|
||||
DATABASE_URL: databaseUrl,
|
||||
SK_KEY_ENCRYPTION_SECRET: skKeyEncryptionSecret,
|
||||
NEWAPI_BASE: source.NEWAPI_BASE ?? "http://127.0.0.1:8080",
|
||||
COOKIE_SECURE: parseBool(source.COOKIE_SECURE, true),
|
||||
LOG_LEVEL: parseLogLevel(source.LOG_LEVEL),
|
||||
ADMIN_INIT_USERNAME: source.ADMIN_INIT_USERNAME,
|
||||
ADMIN_INIT_PASSWORD: source.ADMIN_INIT_PASSWORD,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user