security: externalize BI data source credentials

This commit is contained in:
kkfluous
2026-08-07 15:42:34 +08:00
parent 616341adc8
commit 60e81cb4ba
10 changed files with 169 additions and 60 deletions
+42
View File
@@ -0,0 +1,42 @@
# Core asset database
DB_HOST=
DB_PORT=3306
DB_USER=
DB_PASSWORD=
DB_NAME=
# Hydrogen ledger database
HYDROGEN_DB_HOST=
HYDROGEN_DB_PORT=3306
HYDROGEN_DB_USER=
HYDROGEN_DB_PASSWORD=
HYDROGEN_DB_NAME=
# Mileage reference database
MILEAGE_DB_HOST=
MILEAGE_DB_PORT=3306
MILEAGE_DB_USER=
MILEAGE_DB_PASSWORD=
MILEAGE_DB_NAME=
# OneOS mileage API
ONEOS_MILEAGE_API_BASE_URL=http://172.17.111.55:20310
ONEOS_MILEAGE_API_KEY=
ONEOS_MILEAGE_API_TIMEOUT_MS=20000
# Authentication and external API
EXTERNAL_API_BASE=https://lnh2e.com
JWT_SECRET=
DEV_BYPASS_AUTH=0
# Vehicle heatmap database and map SDK
HEATMAP_DB_HOST=
HEATMAP_DB_PORT=36453
HEATMAP_DB_USER=
HEATMAP_DB_PASSWORD=
HEATMAP_DB_NAME=lingniu_vehicle_data
HEATMAP_DB_SSL=false
AMAP_WEB_KEY=
AMAP_SECURITY_JS_CODE=
SERVER_PORT=3001
+24 -26
View File
@@ -5,35 +5,33 @@ services:
image: harbor.lnh2e.com/lingniu-v1/ln-bi:main-1.0.0
network_mode: host
environment:
DB_HOST: "rm-bp179zbv481rnw3e2no.mysql.rds.aliyuncs.com"
DB_PORT: "3306"
DB_USER: "oneos_db_prod"
DB_PASSWORD: "adASHJcviqwjkbn23ngt1"
DB_NAME: "ln_asset_management"
HYDROGEN_DB_HOST: "47.99.185.173"
HYDROGEN_DB_PORT: "3306"
HYDROGEN_DB_USER: "root"
HYDROGEN_DB_PASSWORD: "lnMysql."
HYDROGEN_DB_NAME: "ln_asset_management"
MILEAGE_DB_HOST: "101.133.130.65"
MILEAGE_DB_PORT: "3306"
MILEAGE_DB_USER: "bi_reader_02"
MILEAGE_DB_PASSWORD: "bi_reader_02_Pass"
MILEAGE_DB_NAME: "hydrogen_energy"
# ECS production accesses OneOS over the private network. Configure the key
# as a Portainer stack environment variable; never commit it to this file.
DB_HOST: "${DB_HOST:?Set DB_HOST in the deployment environment}"
DB_PORT: "${DB_PORT:-3306}"
DB_USER: "${DB_USER:?Set DB_USER in the deployment environment}"
DB_PASSWORD: "${DB_PASSWORD:?Set DB_PASSWORD in the deployment environment}"
DB_NAME: "${DB_NAME:?Set DB_NAME in the deployment environment}"
HYDROGEN_DB_HOST: "${HYDROGEN_DB_HOST:?Set HYDROGEN_DB_HOST in the deployment environment}"
HYDROGEN_DB_PORT: "${HYDROGEN_DB_PORT:-3306}"
HYDROGEN_DB_USER: "${HYDROGEN_DB_USER:?Set HYDROGEN_DB_USER in the deployment environment}"
HYDROGEN_DB_PASSWORD: "${HYDROGEN_DB_PASSWORD:?Set HYDROGEN_DB_PASSWORD in the deployment environment}"
HYDROGEN_DB_NAME: "${HYDROGEN_DB_NAME:?Set HYDROGEN_DB_NAME in the deployment environment}"
MILEAGE_DB_HOST: "${MILEAGE_DB_HOST:?Set MILEAGE_DB_HOST in the deployment environment}"
MILEAGE_DB_PORT: "${MILEAGE_DB_PORT:-3306}"
MILEAGE_DB_USER: "${MILEAGE_DB_USER:?Set MILEAGE_DB_USER in the deployment environment}"
MILEAGE_DB_PASSWORD: "${MILEAGE_DB_PASSWORD:?Set MILEAGE_DB_PASSWORD in the deployment environment}"
MILEAGE_DB_NAME: "${MILEAGE_DB_NAME:?Set MILEAGE_DB_NAME in the deployment environment}"
ONEOS_MILEAGE_API_BASE_URL: "${ONEOS_MILEAGE_API_BASE_URL:-http://172.17.111.55:20310}"
ONEOS_MILEAGE_API_KEY: "${ONEOS_MILEAGE_API_KEY}"
ONEOS_MILEAGE_API_KEY: "${ONEOS_MILEAGE_API_KEY:?Set ONEOS_MILEAGE_API_KEY in the deployment environment}"
ONEOS_MILEAGE_API_TIMEOUT_MS: "${ONEOS_MILEAGE_API_TIMEOUT_MS:-20000}"
SERVER_PORT: "8111"
EXTERNAL_API_BASE: "https://lnh2e.com"
JWT_SECRET: "ln-bi-jwt-prod-k8s9m2x7"
AMAP_WEB_KEY: "${AMAP_WEB_KEY}"
AMAP_SECURITY_JS_CODE: "${AMAP_SECURITY_JS_CODE}"
HEATMAP_DB_HOST: "${HEATMAP_DB_HOST}"
SERVER_PORT: "${SERVER_PORT:-8111}"
EXTERNAL_API_BASE: "${EXTERNAL_API_BASE:-https://lnh2e.com}"
JWT_SECRET: "${JWT_SECRET:?Set JWT_SECRET in the deployment environment}"
AMAP_WEB_KEY: "${AMAP_WEB_KEY:?Set AMAP_WEB_KEY in the deployment environment}"
AMAP_SECURITY_JS_CODE: "${AMAP_SECURITY_JS_CODE:?Set AMAP_SECURITY_JS_CODE in the deployment environment}"
HEATMAP_DB_HOST: "${HEATMAP_DB_HOST:?Set HEATMAP_DB_HOST in the deployment environment}"
HEATMAP_DB_PORT: "${HEATMAP_DB_PORT:-36453}"
HEATMAP_DB_USER: "${HEATMAP_DB_USER}"
HEATMAP_DB_PASSWORD: "${HEATMAP_DB_PASSWORD}"
HEATMAP_DB_USER: "${HEATMAP_DB_USER:?Set HEATMAP_DB_USER in the deployment environment}"
HEATMAP_DB_PASSWORD: "${HEATMAP_DB_PASSWORD:?Set HEATMAP_DB_PASSWORD in the deployment environment}"
HEATMAP_DB_NAME: "${HEATMAP_DB_NAME:-lingniu_vehicle_data}"
HEATMAP_DB_SSL: "${HEATMAP_DB_SSL:-false}"
deploy:
+3 -3
View File
@@ -167,9 +167,9 @@ SUM(fee) / NULLIF(SUM(kwh), 0)
### 阶段 D:部署与安全
- 将数据库地址、账号和密码从受版本控制文件迁移到部署密钥
- 已将部署清单、氢能和里程连接中的数据库敏感值迁移为必填环境变量
- 轮换已经进入版本历史的氢能数据库凭据。
- 为开发、测试和生产分别提供无敏感值的配置模板
- 已提供无敏感值的 `.env.example`;测试、生产值由各环境密钥管理
- 增加服务健康检查、数据源状态和错误率监控。
## 9. 每批验收门槛
@@ -188,4 +188,4 @@ SUM(fee) / NULLIF(SUM(kwh), 0)
- 氢能数据库连接尚未恢复,真实数据对账未完成。
- ETC 供应商未配置、未执行首次同步,记录与账单下钻无数据可验收。
- 氢能连接信息仍存在于版本控制文件,必须迁移并轮换;文档不记录任何连接值。
- 氢能连接信息已从当前版本迁出,但历史凭据仍必须在数据库侧轮换;文档不记录任何连接值。
+2 -1
View File
@@ -1,13 +1,14 @@
import { Hono } from 'hono';
import jwt from 'jsonwebtoken';
import pool from '../db.js';
import { requireEnv } from '../config.js';
import type { AuthUser, JwtPayload, PermissionLevel } from './types.js';
import { FULL_ACCESS_ROLES, DEPT_ACCESS_ROLES } from './types.js';
const app = new Hono();
const EXTERNAL_API_BASE = process.env.EXTERNAL_API_BASE || 'https://beta.lnh2e.com';
const JWT_SECRET = process.env.JWT_SECRET || 'ln-bi-default-secret';
const JWT_SECRET = requireEnv('JWT_SECRET');
/** GET /api/auth/exchange?jumpToken=xxx — 一步完成:换取用户信息 + 签发 JWT */
app.get('/exchange', async (c) => {
+2 -1
View File
@@ -1,8 +1,9 @@
import type { Context, Next } from 'hono';
import jwt from 'jsonwebtoken';
import { requireEnv } from '../config.js';
import type { JwtPayload, AuthUser } from './types.js';
const JWT_SECRET = process.env.JWT_SECRET || 'ln-bi-default-secret';
const JWT_SECRET = requireEnv('JWT_SECRET');
// 临时:跳过所有认证(保留完整逻辑便于快速恢复)
const BYPASS_AUTH = false;
+26
View File
@@ -0,0 +1,26 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { envPort, MissingEnvironmentError, requireEnv } from './config.js';
test('requireEnv returns a trimmed value', () => {
assert.equal(requireEnv('TOKEN', { TOKEN: ' configured ' }), 'configured');
});
test('requireEnv identifies the missing variable without exposing values', () => {
assert.throws(
() => requireEnv('TOKEN', {}),
(error: unknown) => {
assert.ok(error instanceof MissingEnvironmentError);
assert.equal(error.code, 'MISSING_ENVIRONMENT_VARIABLE');
assert.equal(error.variableName, 'TOKEN');
return true;
},
);
});
test('envPort validates configured ports', () => {
assert.equal(envPort('PORT', 3306, {}), 3306);
assert.equal(envPort('PORT', 3306, { PORT: '8111' }), 8111);
assert.throws(() => envPort('PORT', 3306, { PORT: '70000' }));
assert.throws(() => envPort('PORT', 3306, { PORT: 'invalid' }));
});
+33
View File
@@ -0,0 +1,33 @@
import 'dotenv/config';
export class MissingEnvironmentError extends Error {
readonly code = 'MISSING_ENVIRONMENT_VARIABLE';
constructor(readonly variableName: string) {
super(`Missing required environment variable: ${variableName}`);
this.name = 'MissingEnvironmentError';
}
}
export function requireEnv(
name: string,
env: NodeJS.ProcessEnv = process.env,
): string {
const value = env[name]?.trim();
if (!value) throw new MissingEnvironmentError(name);
return value;
}
export function envPort(
name: string,
fallback: number,
env: NodeJS.ProcessEnv = process.env,
): number {
const raw = env[name]?.trim();
if (!raw) return fallback;
const value = Number(raw);
if (!Number.isInteger(value) || value < 1 || value > 65535) {
throw new Error(`Invalid port in environment variable: ${name}`);
}
return value;
}
+14 -11
View File
@@ -1,17 +1,20 @@
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
import mysql, { type Pool } from 'mysql2/promise';
import { envPort, requireEnv } from './config.js';
dotenv.config();
let hydrogenPool: Pool | null = null;
const hydrogenPool = mysql.createPool({
host: process.env.HYDROGEN_DB_HOST || '47.99.185.173',
port: Number(process.env.HYDROGEN_DB_PORT) || 3306,
user: process.env.HYDROGEN_DB_USER || 'root',
password: process.env.HYDROGEN_DB_PASSWORD || 'lnMysql.',
database: process.env.HYDROGEN_DB_NAME || 'ln_asset_management',
export function getHydrogenPool(): Pool {
if (hydrogenPool) return hydrogenPool;
hydrogenPool = mysql.createPool({
host: requireEnv('HYDROGEN_DB_HOST'),
port: envPort('HYDROGEN_DB_PORT', 3306),
user: requireEnv('HYDROGEN_DB_USER'),
password: requireEnv('HYDROGEN_DB_PASSWORD'),
database: requireEnv('HYDROGEN_DB_NAME'),
waitForConnections: true,
connectionLimit: 5,
queueLimit: 0,
});
export default hydrogenPool;
return hydrogenPool;
}
+14 -11
View File
@@ -1,17 +1,20 @@
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
import mysql, { type Pool } from 'mysql2/promise';
import { envPort, requireEnv } from './config.js';
dotenv.config();
let mileagePool: Pool | null = null;
const mileagePool = mysql.createPool({
host: process.env.MILEAGE_DB_HOST || '101.133.130.65',
port: Number(process.env.MILEAGE_DB_PORT) || 3306,
user: process.env.MILEAGE_DB_USER || 'bi_reader_02',
password: process.env.MILEAGE_DB_PASSWORD || 'bi_reader_02_Pass',
database: process.env.MILEAGE_DB_NAME || 'hydrogen_energy',
export function getMileagePool(): Pool {
if (mileagePool) return mileagePool;
mileagePool = mysql.createPool({
host: requireEnv('MILEAGE_DB_HOST'),
port: envPort('MILEAGE_DB_PORT', 3306),
user: requireEnv('MILEAGE_DB_USER'),
password: requireEnv('MILEAGE_DB_PASSWORD'),
database: requireEnv('MILEAGE_DB_NAME'),
waitForConnections: true,
connectionLimit: 5,
queueLimit: 0,
});
export default mileagePool;
return mileagePool;
}
+3 -1
View File
@@ -1,7 +1,7 @@
import { Hono } from 'hono';
import type { RowDataPacket } from 'mysql2';
import pool from '../../db.js';
import hydrogenPool from '../../hydrogen-db.js';
import { getHydrogenPool } from '../../hydrogen-db.js';
import { cached } from './cache.js';
import {
parseHydrogenCustomerKind,
@@ -147,6 +147,7 @@ app.get('/hydrogen/overview', async (c) => {
const requestedYear = yearParam ? Number(yearParam) || todayYear : todayYear;
const data = await cached(`hydrogen/overview?year=${requestedYear}`, async () => {
const hydrogenPool = getHydrogenPool();
// 可选年份(数据自 HYDROGEN_MIN_DATE 起)
const [yearListRows] = await hydrogenPool.query<RowDataPacket[]>(
`SELECT DISTINCT YEAR(${HYDROGEN_LOCAL}) AS y
@@ -412,6 +413,7 @@ app.get('/hydrogen/daily', async (c) => {
const force = c.req.query('force') === '1';
const data = await cached(`hydrogen/daily?start=${dateRange.start}&end=${dateRange.end}&customer=${customer}&station=${stationId ?? 'all'}&customerName=${encodeURIComponent(customerName ?? 'all')}`, async () => {
const hydrogenPool = getHydrogenPool();
const whereParts = [
HYDROGEN_BASE_WHERE_B,