- 支持 TRUST_PROXY 配置,由 express 正确解析客户端 IP,防止伪造 X-Forwarded-For - 修复 IP 封锁使用对象作为 Map key 导致失效的问题,改用字符串 IP - 主密码解锁失败时记录 IP 失败次数,达到阈值后封锁 - 补充条目更新操作的审计日志
95 行
4.1 KiB
JavaScript
95 行
4.1 KiB
JavaScript
'use strict';
|
||
const process = require('process');
|
||
const path = require('path');
|
||
|
||
// ===== 环境变量优先的取数助手 =====
|
||
// 配置写在 config.js,敏感值(口令固定串、SESSION_SECRET)可用同名环境变量覆盖,
|
||
// 便于 Docker 注入密钥,避免把密钥明文写进仓库。
|
||
const env = process.env;
|
||
const pick = (key, fallback) =>
|
||
(env[key] !== undefined && env[key] !== '') ? env[key] : fallback;
|
||
|
||
// 解析带单位的体积(如 5M / 1024K / 5242880),返回字节数;非法时回退到 fallbackBytes
|
||
function parseSize(val, fallbackBytes = 5 * 1024 * 1024) {
|
||
if (typeof val === 'number') return val > 0 ? val : fallbackBytes;
|
||
const s = String(val).trim().toLowerCase();
|
||
const m = s.match(/^(\d+(?:\.\d+)?)\s*([kmg]?b?)$/);
|
||
if (!m) return fallbackBytes;
|
||
const n = parseFloat(m[1]);
|
||
const unit = m[2];
|
||
const mult = unit.startsWith('k') ? 1024
|
||
: unit.startsWith('m') ? 1024 * 1024
|
||
: unit.startsWith('g') ? 1024 * 1024 * 1024
|
||
: 1;
|
||
return Math.floor(n * mult);
|
||
}
|
||
|
||
// 日期格式化(默认 yyyymmdd)
|
||
function formatDate(d, fmt) {
|
||
const y = String(d.getFullYear());
|
||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||
const day = String(d.getDate()).padStart(2, '0');
|
||
return fmt.replace(/yyyy/g, y).replace(/mm/g, m).replace(/dd/g, day);
|
||
}
|
||
|
||
const config = {
|
||
// 1) APP 门户口令:dynamic = 当天日期 + 固定串;static = 固定口令
|
||
// 敏感值必须来自环境变量(.env / docker environment),不要在文件里写死真实值
|
||
appPassword: {
|
||
mode: pick('APP_PW_MODE', 'dynamic'), // 'dynamic' | 'static'
|
||
dateFormat: pick('APP_PW_DATE_FMT', 'yyyymmdd'),
|
||
datePosition: pick('APP_PW_DATE_POS', 'prefix'), // 'prefix' | 'suffix'
|
||
dynamicSecret: pick('APP_PW_DYNAMIC', ''),
|
||
staticPassword: pick('APP_PW_STATIC', ''),
|
||
},
|
||
|
||
// 2) 密码库 / 密钥文件路径(容器内路径,由 docker-compose 挂载)
|
||
kdbxPath: pick('KDBX_PATH', '/app/vault/vault.kdbx'),
|
||
keyfilePath: pick('KEYFILE_PATH', '/app/vault/vault.key'),
|
||
|
||
// 3) 权限:默认只读;true 时开放网页端编辑并写回 kdbx
|
||
// 注意:WRITABLE=true 时必须把 docker-compose 里 vault 挂载从 :ro 改为 :rw
|
||
writable: pick('WRITABLE', 'false') === 'true',
|
||
|
||
// 4) 会话与安全
|
||
sessionSecret: pick('SESSION_SECRET', ''),
|
||
sessionMaxAge: parseInt(pick('SESSION_MAX_AGE', String(30 * 60 * 1000)), 10),
|
||
// 反向代理层数/地址(如 "1" 或 "127.0.0.1")。设置后由 express 正确解析客户端 IP,
|
||
// 防止攻击者伪造 X-Forwarded-For 绕过 IP 封锁。直连部署留空。
|
||
trustProxy: pick('TRUST_PROXY', ''),
|
||
|
||
// 审计日志:本地文件持久化目录(可按大小自动切分),默认 ./logs
|
||
logDir: pick('LOG_DIR', path.join(__dirname, 'logs')),
|
||
// 审计日志单文件切分阈值(字节),默认 5MB;支持 K/M 单位
|
||
auditFileMax: parseSize(pick('AUDIT_FILE_MAX', '5M')),
|
||
// 审计日志切分文件最多保留份数(audit.1.log ~ audit.N.log),默认 10
|
||
auditFileKeep: parseInt(pick('AUDIT_FILE_KEEP', '10'), 10),
|
||
|
||
port: parseInt(pick('PORT', '3000'), 10),
|
||
|
||
// 缺失敏感值的友好提示(仅提醒,不阻断启动)
|
||
_warnMissing: [],
|
||
};
|
||
|
||
if (!config.appPassword.dynamicSecret && !config.appPassword.staticPassword)
|
||
config._warnMissing.push('APP_PW_DYNAMIC / APP_PW_STATIC 均未设置,登录将失败');
|
||
|
||
// 今日期望的 APP 口令(服务端动态计算,前端/用户都看不到拼接逻辑)
|
||
config.getExpectedAppPassword = function () {
|
||
const ap = this.appPassword;
|
||
if (ap.mode === 'static') return ap.staticPassword;
|
||
const dateStr = formatDate(new Date(), ap.dateFormat);
|
||
return ap.datePosition === 'suffix'
|
||
? ap.dynamicSecret + dateStr
|
||
: dateStr + ap.dynamicSecret; // 默认:20260825 + 固定串
|
||
};
|
||
|
||
// 启动前检查必要敏感值是否已从环境变量注入
|
||
if (!config.sessionSecret) config._warnMissing.push('SESSION_SECRET 未设置');
|
||
if (config._warnMissing.length) {
|
||
console.warn('[config] 警告:以下敏感值未从环境变量取得,请检查 .env:\n - ' +
|
||
config._warnMissing.join('\n - '));
|
||
}
|
||
|
||
module.exports = config;
|