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
+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;
}
+17 -14
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',
waitForConnections: true,
connectionLimit: 5,
queueLimit: 0,
});
export function getHydrogenPool(): Pool {
if (hydrogenPool) return hydrogenPool;
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
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',
waitForConnections: true,
connectionLimit: 5,
queueLimit: 0,
});
export function getMileagePool(): Pool {
if (mileagePool) return mileagePool;
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;
}
+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,