feat: expose BI configuration readiness
This commit is contained in:
@@ -31,7 +31,7 @@ export async function authMiddleware(c: Context, next: Next) {
|
||||
}
|
||||
|
||||
// 跳过不需要认证的路径
|
||||
if (path === '/api/health' || path.startsWith('/api/auth/')) {
|
||||
if (path.startsWith('/api/health') || path.startsWith('/api/auth/')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -12,6 +12,7 @@ import feedbackRouter from './routes/feedback/index.js';
|
||||
import vehicleHeatmapRouter from './routes/vehicle-heatmap.js';
|
||||
import hydrogenHeatmapRouter from './routes/hydrogen-heatmap.js';
|
||||
import analyticsRouter from './routes/analytics/index.js';
|
||||
import healthRouter from './routes/health.js';
|
||||
import { ensureSchedulingTables } from './routes/scheduling/db-schema.js';
|
||||
import authRouter from './auth/login.js';
|
||||
import { authMiddleware } from './auth/middleware.js';
|
||||
@@ -24,6 +25,7 @@ app.use('/api/*', cors());
|
||||
|
||||
// Auth 路由(不需要中间件)
|
||||
app.route('/api/auth', authRouter);
|
||||
app.route('/api/health', healthRouter);
|
||||
|
||||
// Auth 中间件(保护后续所有 /api/* 路由)
|
||||
app.use('/api/*', authMiddleware);
|
||||
@@ -38,8 +40,6 @@ app.route('/api/vehicle-heatmap', vehicleHeatmapRouter);
|
||||
app.route('/api/hydrogen-heatmap', hydrogenHeatmapRouter);
|
||||
app.route('/api/analytics', analyticsRouter);
|
||||
|
||||
app.get('/api/health', (c) => c.json({ status: 'ok', time: new Date().toISOString() }));
|
||||
|
||||
// Serve static files in production
|
||||
app.use('/*', serveStatic({ root: './dist' }));
|
||||
app.use('/*', serveStatic({ root: './dist', path: 'index.html' }));
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { buildReadinessReport } from './readiness.js';
|
||||
|
||||
const completeEnv: NodeJS.ProcessEnv = {
|
||||
DB_HOST: 'configured',
|
||||
DB_USER: 'configured',
|
||||
DB_PASSWORD: 'do-not-expose-secret',
|
||||
DB_NAME: 'configured',
|
||||
JWT_SECRET: 'configured',
|
||||
ONEOS_MILEAGE_API_BASE_URL: 'configured',
|
||||
ONEOS_MILEAGE_API_KEY: 'configured',
|
||||
HYDROGEN_DB_HOST: 'configured',
|
||||
HYDROGEN_DB_USER: 'configured',
|
||||
HYDROGEN_DB_PASSWORD: 'configured',
|
||||
HYDROGEN_DB_NAME: 'configured',
|
||||
HEATMAP_DB_HOST: 'configured',
|
||||
HEATMAP_DB_USER: 'configured',
|
||||
HEATMAP_DB_PASSWORD: 'configured',
|
||||
HEATMAP_DB_NAME: 'configured',
|
||||
AMAP_WEB_KEY: 'configured',
|
||||
AMAP_SECURITY_JS_CODE: 'configured',
|
||||
OSS_ENDPOINT: 'configured',
|
||||
OSS_ACCESS_KEY_ID: 'configured',
|
||||
OSS_ACCESS_KEY_SECRET: 'configured',
|
||||
OSS_BUCKET: 'configured',
|
||||
};
|
||||
|
||||
test('reports ready only when core and integration configuration is complete', () => {
|
||||
const report = buildReadinessReport(completeEnv, new Date('2026-08-07T08:00:00Z'));
|
||||
assert.equal(report.status, 'ready');
|
||||
assert.equal(report.semantics, 'configuration-only');
|
||||
assert.equal(report.checkedAt, '2026-08-07T08:00:00.000Z');
|
||||
});
|
||||
|
||||
test('distinguishes optional integration gaps from core readiness failures', () => {
|
||||
const degraded = buildReadinessReport({ ...completeEnv, HYDROGEN_DB_PASSWORD: '' });
|
||||
assert.equal(degraded.status, 'degraded');
|
||||
assert.equal(degraded.integrations.hydrogenDatabase, 'incomplete');
|
||||
|
||||
const notReady = buildReadinessReport({ ...completeEnv, DB_PASSWORD: '' });
|
||||
assert.equal(notReady.status, 'not-ready');
|
||||
assert.equal(notReady.core.assetDatabase, 'incomplete');
|
||||
});
|
||||
|
||||
test('does not expose configuration values or variable names', () => {
|
||||
const serialized = JSON.stringify(buildReadinessReport(completeEnv));
|
||||
assert.equal(serialized.includes('do-not-expose-secret'), false);
|
||||
assert.equal(serialized.includes('DB_PASSWORD'), false);
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
export type ConfigurationState = 'configured' | 'incomplete';
|
||||
|
||||
export interface ReadinessReport {
|
||||
status: 'ready' | 'degraded' | 'not-ready';
|
||||
semantics: 'configuration-only';
|
||||
checkedAt: string;
|
||||
core: {
|
||||
assetDatabase: ConfigurationState;
|
||||
authentication: ConfigurationState;
|
||||
};
|
||||
integrations: {
|
||||
oneOsMileage: ConfigurationState;
|
||||
hydrogenDatabase: ConfigurationState;
|
||||
heatmapDatabase: ConfigurationState;
|
||||
mapSdk: ConfigurationState;
|
||||
objectStorage: ConfigurationState;
|
||||
};
|
||||
}
|
||||
|
||||
function hasAll(env: NodeJS.ProcessEnv, names: readonly string[]): ConfigurationState {
|
||||
return names.every(name => Boolean(env[name]?.trim())) ? 'configured' : 'incomplete';
|
||||
}
|
||||
|
||||
export function buildReadinessReport(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
now = new Date(),
|
||||
): ReadinessReport {
|
||||
const core = {
|
||||
assetDatabase: hasAll(env, ['DB_HOST', 'DB_USER', 'DB_PASSWORD', 'DB_NAME']),
|
||||
authentication: hasAll(env, ['JWT_SECRET']),
|
||||
} as const;
|
||||
const integrations = {
|
||||
oneOsMileage: hasAll(env, ['ONEOS_MILEAGE_API_BASE_URL', 'ONEOS_MILEAGE_API_KEY']),
|
||||
hydrogenDatabase: hasAll(env, [
|
||||
'HYDROGEN_DB_HOST',
|
||||
'HYDROGEN_DB_USER',
|
||||
'HYDROGEN_DB_PASSWORD',
|
||||
'HYDROGEN_DB_NAME',
|
||||
]),
|
||||
heatmapDatabase: hasAll(env, [
|
||||
'HEATMAP_DB_HOST',
|
||||
'HEATMAP_DB_USER',
|
||||
'HEATMAP_DB_PASSWORD',
|
||||
'HEATMAP_DB_NAME',
|
||||
]),
|
||||
mapSdk: hasAll(env, ['AMAP_WEB_KEY', 'AMAP_SECURITY_JS_CODE']),
|
||||
objectStorage: hasAll(env, [
|
||||
'OSS_ENDPOINT',
|
||||
'OSS_ACCESS_KEY_ID',
|
||||
'OSS_ACCESS_KEY_SECRET',
|
||||
'OSS_BUCKET',
|
||||
]),
|
||||
} as const;
|
||||
|
||||
const coreReady = Object.values(core).every(value => value === 'configured');
|
||||
const integrationsReady = Object.values(integrations).every(value => value === 'configured');
|
||||
return {
|
||||
status: !coreReady ? 'not-ready' : integrationsReady ? 'ready' : 'degraded',
|
||||
semantics: 'configuration-only',
|
||||
checkedAt: now.toISOString(),
|
||||
core,
|
||||
integrations,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import app from './health.js';
|
||||
|
||||
test('separates liveness from configuration readiness', async () => {
|
||||
const liveness = await app.request('/');
|
||||
const readiness = await app.request('/readiness');
|
||||
const liveBody = await liveness.json();
|
||||
const readyBody = await readiness.json();
|
||||
|
||||
assert.equal(liveness.status, 200);
|
||||
assert.equal(liveBody.semantics, 'liveness');
|
||||
assert.equal(readyBody.semantics, 'configuration-only');
|
||||
assert.ok([200, 503].includes(readiness.status));
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Hono } from 'hono';
|
||||
import { buildReadinessReport } from '../readiness.js';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.get('/', (c) => c.json({
|
||||
status: 'ok',
|
||||
semantics: 'liveness',
|
||||
time: new Date().toISOString(),
|
||||
}));
|
||||
|
||||
app.get('/readiness', (c) => {
|
||||
const report = buildReadinessReport();
|
||||
return c.json(report, report.status === 'not-ready' ? 503 : 200);
|
||||
});
|
||||
|
||||
export default app;
|
||||
Reference in New Issue
Block a user