Archived
统一鉴权中心接入(ADR-012):org 重写为 Keycloak 身份 + 角色→端;中间件按路由归属统一判;口令/会话/岗位授权退役;迁移 0032
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
This commit is contained in:
@@ -3,6 +3,18 @@
|
||||
*/
|
||||
import { join } from 'node:path';
|
||||
|
||||
/** 统一鉴权中心(ADR-012)。本服务只验不调,**不配 client secret**(手册 13.2) */
|
||||
export type KeycloakConfig = {
|
||||
readonly url: string;
|
||||
readonly realm: string;
|
||||
/** 校 aud 用 —— 填错全线 401 */
|
||||
readonly clientId: string;
|
||||
readonly issuer: string;
|
||||
readonly jwksUrl: string;
|
||||
/** master realm 的 provisioning 服务账号(只读名册,view-users);可选 */
|
||||
readonly provisioning?: { readonly clientId: string; readonly secret: string };
|
||||
};
|
||||
|
||||
export type AppConfig = {
|
||||
readonly nodeEnv: 'development' | 'test' | 'production';
|
||||
readonly port: number;
|
||||
@@ -11,25 +23,52 @@ export type AppConfig = {
|
||||
readonly attachmentRoot: string;
|
||||
/** 派发器轮询间隔(毫秒) */
|
||||
readonly dispatchIntervalMs: number;
|
||||
/**
|
||||
* 播种口令(ADR-010)—— 新建员工的初始口令。
|
||||
* 源码不写任何真实口令;生产部署必须经环境变量覆盖缺省值。
|
||||
*/
|
||||
readonly seedPassword: string;
|
||||
/** 三样齐了才有;缺一样即 undefined —— main.ts 据此拒绝启动,集成测试可不配 */
|
||||
readonly keycloak?: KeycloakConfig;
|
||||
/** 测试账号(Keycloak 用户名):首次建档 / 播种时落 TEST 岗 + 独立工作组 */
|
||||
readonly testAccounts: readonly string[];
|
||||
};
|
||||
|
||||
const DEFAULT_DATABASE_URL = 'postgres://jcai:jcai@localhost:54329/jcai_wos';
|
||||
/** 开发缺省口令 —— 播种时在 stdout 明示,生产须以 `JCAI_SEED_PASSWORD` 覆盖 */
|
||||
const DEFAULT_SEED_PASSWORD = 'jcai-dev-2026';
|
||||
|
||||
function nonEmpty(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed === undefined || trimmed === '' ? undefined : trimmed;
|
||||
}
|
||||
|
||||
function keycloakOf(env: NodeJS.ProcessEnv): KeycloakConfig | undefined {
|
||||
const url = nonEmpty(env['KEYCLOAK_URL'])?.replace(/\/+$/, '');
|
||||
const realm = nonEmpty(env['KEYCLOAK_REALM']);
|
||||
const clientId = nonEmpty(env['KEYCLOAK_CLIENT_ID']);
|
||||
if (url === undefined || realm === undefined || clientId === undefined) return undefined;
|
||||
const issuer = `${url}/realms/${realm}`;
|
||||
const provisioningId = nonEmpty(env['KEYCLOAK_PROVISIONING_CLIENT_ID']);
|
||||
const provisioningSecret = nonEmpty(env['KEYCLOAK_PROVISIONING_CLIENT_SECRET']);
|
||||
return {
|
||||
url,
|
||||
realm,
|
||||
clientId,
|
||||
issuer,
|
||||
jwksUrl: `${issuer}/protocol/openid-connect/certs`,
|
||||
...(provisioningId === undefined || provisioningSecret === undefined
|
||||
? {}
|
||||
: { provisioning: { clientId: provisioningId, secret: provisioningSecret } }),
|
||||
};
|
||||
}
|
||||
|
||||
export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
|
||||
const nodeEnv = env['NODE_ENV'];
|
||||
const keycloak = keycloakOf(env);
|
||||
return {
|
||||
nodeEnv: nodeEnv === 'production' || nodeEnv === 'test' ? nodeEnv : 'development',
|
||||
port: Number(env['PORT'] ?? 8787),
|
||||
databaseUrl: env['DATABASE_URL'] ?? DEFAULT_DATABASE_URL,
|
||||
attachmentRoot: env['JCAI_ATTACHMENT_ROOT'] ?? join(process.cwd(), '.data', 'attachments'),
|
||||
dispatchIntervalMs: Number(env['JCAI_DISPATCH_INTERVAL_MS'] ?? 2000),
|
||||
seedPassword: env['JCAI_SEED_PASSWORD'] ?? DEFAULT_SEED_PASSWORD,
|
||||
...(keycloak === undefined ? {} : { keycloak }),
|
||||
testAccounts: (env['JCAI_TEST_ACCOUNTS'] ?? '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s !== ''),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user