security: externalize BI data source credentials
This commit is contained in:
@@ -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
@@ -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:
|
||||
|
||||
@@ -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 供应商未配置、未执行首次同步,记录与账单下钻无数据可验收。
|
||||
- 氢能连接信息仍存在于版本控制文件,必须迁移并轮换;文档不记录任何连接值。
|
||||
- 氢能连接信息已从当前版本迁出,但历史凭据仍必须在数据库侧轮换;文档不记录任何连接值。
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' }));
|
||||
});
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user