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
|
image: harbor.lnh2e.com/lingniu-v1/ln-bi:main-1.0.0
|
||||||
network_mode: host
|
network_mode: host
|
||||||
environment:
|
environment:
|
||||||
DB_HOST: "rm-bp179zbv481rnw3e2no.mysql.rds.aliyuncs.com"
|
DB_HOST: "${DB_HOST:?Set DB_HOST in the deployment environment}"
|
||||||
DB_PORT: "3306"
|
DB_PORT: "${DB_PORT:-3306}"
|
||||||
DB_USER: "oneos_db_prod"
|
DB_USER: "${DB_USER:?Set DB_USER in the deployment environment}"
|
||||||
DB_PASSWORD: "adASHJcviqwjkbn23ngt1"
|
DB_PASSWORD: "${DB_PASSWORD:?Set DB_PASSWORD in the deployment environment}"
|
||||||
DB_NAME: "ln_asset_management"
|
DB_NAME: "${DB_NAME:?Set DB_NAME in the deployment environment}"
|
||||||
HYDROGEN_DB_HOST: "47.99.185.173"
|
HYDROGEN_DB_HOST: "${HYDROGEN_DB_HOST:?Set HYDROGEN_DB_HOST in the deployment environment}"
|
||||||
HYDROGEN_DB_PORT: "3306"
|
HYDROGEN_DB_PORT: "${HYDROGEN_DB_PORT:-3306}"
|
||||||
HYDROGEN_DB_USER: "root"
|
HYDROGEN_DB_USER: "${HYDROGEN_DB_USER:?Set HYDROGEN_DB_USER in the deployment environment}"
|
||||||
HYDROGEN_DB_PASSWORD: "lnMysql."
|
HYDROGEN_DB_PASSWORD: "${HYDROGEN_DB_PASSWORD:?Set HYDROGEN_DB_PASSWORD in the deployment environment}"
|
||||||
HYDROGEN_DB_NAME: "ln_asset_management"
|
HYDROGEN_DB_NAME: "${HYDROGEN_DB_NAME:?Set HYDROGEN_DB_NAME in the deployment environment}"
|
||||||
MILEAGE_DB_HOST: "101.133.130.65"
|
MILEAGE_DB_HOST: "${MILEAGE_DB_HOST:?Set MILEAGE_DB_HOST in the deployment environment}"
|
||||||
MILEAGE_DB_PORT: "3306"
|
MILEAGE_DB_PORT: "${MILEAGE_DB_PORT:-3306}"
|
||||||
MILEAGE_DB_USER: "bi_reader_02"
|
MILEAGE_DB_USER: "${MILEAGE_DB_USER:?Set MILEAGE_DB_USER in the deployment environment}"
|
||||||
MILEAGE_DB_PASSWORD: "bi_reader_02_Pass"
|
MILEAGE_DB_PASSWORD: "${MILEAGE_DB_PASSWORD:?Set MILEAGE_DB_PASSWORD in the deployment environment}"
|
||||||
MILEAGE_DB_NAME: "hydrogen_energy"
|
MILEAGE_DB_NAME: "${MILEAGE_DB_NAME:?Set MILEAGE_DB_NAME in the deployment environment}"
|
||||||
# ECS production accesses OneOS over the private network. Configure the key
|
|
||||||
# as a Portainer stack environment variable; never commit it to this file.
|
|
||||||
ONEOS_MILEAGE_API_BASE_URL: "${ONEOS_MILEAGE_API_BASE_URL:-http://172.17.111.55:20310}"
|
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}"
|
ONEOS_MILEAGE_API_TIMEOUT_MS: "${ONEOS_MILEAGE_API_TIMEOUT_MS:-20000}"
|
||||||
SERVER_PORT: "8111"
|
SERVER_PORT: "${SERVER_PORT:-8111}"
|
||||||
EXTERNAL_API_BASE: "https://lnh2e.com"
|
EXTERNAL_API_BASE: "${EXTERNAL_API_BASE:-https://lnh2e.com}"
|
||||||
JWT_SECRET: "ln-bi-jwt-prod-k8s9m2x7"
|
JWT_SECRET: "${JWT_SECRET:?Set JWT_SECRET in the deployment environment}"
|
||||||
AMAP_WEB_KEY: "${AMAP_WEB_KEY}"
|
AMAP_WEB_KEY: "${AMAP_WEB_KEY:?Set AMAP_WEB_KEY in the deployment environment}"
|
||||||
AMAP_SECURITY_JS_CODE: "${AMAP_SECURITY_JS_CODE}"
|
AMAP_SECURITY_JS_CODE: "${AMAP_SECURITY_JS_CODE:?Set AMAP_SECURITY_JS_CODE in the deployment environment}"
|
||||||
HEATMAP_DB_HOST: "${HEATMAP_DB_HOST}"
|
HEATMAP_DB_HOST: "${HEATMAP_DB_HOST:?Set HEATMAP_DB_HOST in the deployment environment}"
|
||||||
HEATMAP_DB_PORT: "${HEATMAP_DB_PORT:-36453}"
|
HEATMAP_DB_PORT: "${HEATMAP_DB_PORT:-36453}"
|
||||||
HEATMAP_DB_USER: "${HEATMAP_DB_USER}"
|
HEATMAP_DB_USER: "${HEATMAP_DB_USER:?Set HEATMAP_DB_USER in the deployment environment}"
|
||||||
HEATMAP_DB_PASSWORD: "${HEATMAP_DB_PASSWORD}"
|
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_NAME: "${HEATMAP_DB_NAME:-lingniu_vehicle_data}"
|
||||||
HEATMAP_DB_SSL: "${HEATMAP_DB_SSL:-false}"
|
HEATMAP_DB_SSL: "${HEATMAP_DB_SSL:-false}"
|
||||||
deploy:
|
deploy:
|
||||||
|
|||||||
@@ -167,9 +167,9 @@ SUM(fee) / NULLIF(SUM(kwh), 0)
|
|||||||
|
|
||||||
### 阶段 D:部署与安全
|
### 阶段 D:部署与安全
|
||||||
|
|
||||||
- 将数据库地址、账号和密码从受版本控制文件迁移到部署密钥。
|
- 已将部署清单、氢能和里程连接中的数据库敏感值迁移为必填环境变量。
|
||||||
- 轮换已经进入版本历史的氢能数据库凭据。
|
- 轮换已经进入版本历史的氢能数据库凭据。
|
||||||
- 为开发、测试和生产分别提供无敏感值的配置模板。
|
- 已提供无敏感值的 `.env.example`;测试、生产值由各环境密钥管理。
|
||||||
- 增加服务健康检查、数据源状态和错误率监控。
|
- 增加服务健康检查、数据源状态和错误率监控。
|
||||||
|
|
||||||
## 9. 每批验收门槛
|
## 9. 每批验收门槛
|
||||||
@@ -188,4 +188,4 @@ SUM(fee) / NULLIF(SUM(kwh), 0)
|
|||||||
|
|
||||||
- 氢能数据库连接尚未恢复,真实数据对账未完成。
|
- 氢能数据库连接尚未恢复,真实数据对账未完成。
|
||||||
- ETC 供应商未配置、未执行首次同步,记录与账单下钻无数据可验收。
|
- ETC 供应商未配置、未执行首次同步,记录与账单下钻无数据可验收。
|
||||||
- 氢能连接信息仍存在于版本控制文件,必须迁移并轮换;文档不记录任何连接值。
|
- 氢能连接信息已从当前版本迁出,但历史凭据仍必须在数据库侧轮换;文档不记录任何连接值。
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import pool from '../db.js';
|
import pool from '../db.js';
|
||||||
|
import { requireEnv } from '../config.js';
|
||||||
import type { AuthUser, JwtPayload, PermissionLevel } from './types.js';
|
import type { AuthUser, JwtPayload, PermissionLevel } from './types.js';
|
||||||
import { FULL_ACCESS_ROLES, DEPT_ACCESS_ROLES } from './types.js';
|
import { FULL_ACCESS_ROLES, DEPT_ACCESS_ROLES } from './types.js';
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
const EXTERNAL_API_BASE = process.env.EXTERNAL_API_BASE || 'https://beta.lnh2e.com';
|
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 */
|
/** GET /api/auth/exchange?jumpToken=xxx — 一步完成:换取用户信息 + 签发 JWT */
|
||||||
app.get('/exchange', async (c) => {
|
app.get('/exchange', async (c) => {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { Context, Next } from 'hono';
|
import type { Context, Next } from 'hono';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { requireEnv } from '../config.js';
|
||||||
import type { JwtPayload, AuthUser } from './types.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;
|
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;
|
||||||
|
}
|
||||||
+17
-14
@@ -1,17 +1,20 @@
|
|||||||
import mysql from 'mysql2/promise';
|
import mysql, { type Pool } from 'mysql2/promise';
|
||||||
import dotenv from 'dotenv';
|
import { envPort, requireEnv } from './config.js';
|
||||||
|
|
||||||
dotenv.config();
|
let hydrogenPool: Pool | null = null;
|
||||||
|
|
||||||
const hydrogenPool = mysql.createPool({
|
export function getHydrogenPool(): Pool {
|
||||||
host: process.env.HYDROGEN_DB_HOST || '47.99.185.173',
|
if (hydrogenPool) return hydrogenPool;
|
||||||
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',
|
|
||||||
waitForConnections: true,
|
|
||||||
connectionLimit: 5,
|
|
||||||
queueLimit: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default 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,
|
||||||
|
});
|
||||||
|
return hydrogenPool;
|
||||||
|
}
|
||||||
|
|||||||
+17
-14
@@ -1,17 +1,20 @@
|
|||||||
import mysql from 'mysql2/promise';
|
import mysql, { type Pool } from 'mysql2/promise';
|
||||||
import dotenv from 'dotenv';
|
import { envPort, requireEnv } from './config.js';
|
||||||
|
|
||||||
dotenv.config();
|
let mileagePool: Pool | null = null;
|
||||||
|
|
||||||
const mileagePool = mysql.createPool({
|
export function getMileagePool(): Pool {
|
||||||
host: process.env.MILEAGE_DB_HOST || '101.133.130.65',
|
if (mileagePool) return mileagePool;
|
||||||
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',
|
|
||||||
waitForConnections: true,
|
|
||||||
connectionLimit: 5,
|
|
||||||
queueLimit: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default 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,
|
||||||
|
});
|
||||||
|
return mileagePool;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import type { RowDataPacket } from 'mysql2';
|
import type { RowDataPacket } from 'mysql2';
|
||||||
import pool from '../../db.js';
|
import pool from '../../db.js';
|
||||||
import hydrogenPool from '../../hydrogen-db.js';
|
import { getHydrogenPool } from '../../hydrogen-db.js';
|
||||||
import { cached } from './cache.js';
|
import { cached } from './cache.js';
|
||||||
import {
|
import {
|
||||||
parseHydrogenCustomerKind,
|
parseHydrogenCustomerKind,
|
||||||
@@ -147,6 +147,7 @@ app.get('/hydrogen/overview', async (c) => {
|
|||||||
const requestedYear = yearParam ? Number(yearParam) || todayYear : todayYear;
|
const requestedYear = yearParam ? Number(yearParam) || todayYear : todayYear;
|
||||||
|
|
||||||
const data = await cached(`hydrogen/overview?year=${requestedYear}`, async () => {
|
const data = await cached(`hydrogen/overview?year=${requestedYear}`, async () => {
|
||||||
|
const hydrogenPool = getHydrogenPool();
|
||||||
// 可选年份(数据自 HYDROGEN_MIN_DATE 起)
|
// 可选年份(数据自 HYDROGEN_MIN_DATE 起)
|
||||||
const [yearListRows] = await hydrogenPool.query<RowDataPacket[]>(
|
const [yearListRows] = await hydrogenPool.query<RowDataPacket[]>(
|
||||||
`SELECT DISTINCT YEAR(${HYDROGEN_LOCAL}) AS y
|
`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 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 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 = [
|
const whereParts = [
|
||||||
HYDROGEN_BASE_WHERE_B,
|
HYDROGEN_BASE_WHERE_B,
|
||||||
|
|||||||
Reference in New Issue
Block a user