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:
@@ -1,17 +1,39 @@
|
||||
/**
|
||||
* module: org —— 对外类型(编码规范 §10.1)。只有类型和常量,没有实现。
|
||||
*
|
||||
* 身份归统一鉴权中心(ADR-012):`Staff.id` 就是 Keycloak 的 `sub`。
|
||||
* 本模块管三件事:①验签后的人是谁(名册即时更新);②他能进哪几个端(角色→端);③岗位与工作组。
|
||||
* 口令 / 会话 / 岗位授权(客体三类 × 动作三种)已退役。
|
||||
*/
|
||||
import type { DisableCheck, Grant, GrantAction, GrantObject, Position, Session, Staff, StaffId, WorkGroup, WorkGroupBinding } from '@contracts';
|
||||
import type {
|
||||
InFlightWork,
|
||||
Position,
|
||||
PositionId,
|
||||
Principal,
|
||||
RoleAccess,
|
||||
RosterSyncOutcome,
|
||||
Staff,
|
||||
StaffId,
|
||||
Surface,
|
||||
WorkGroup,
|
||||
WorkGroupBinding,
|
||||
} from '@contracts';
|
||||
|
||||
export type { DisableCheck, Grant, GrantAction, GrantObject, Position, Session, Staff, WorkGroup, WorkGroupBinding };
|
||||
export type { InFlightWork, Position, Principal, RoleAccess, RosterSyncOutcome, Staff, Surface, WorkGroup, WorkGroupBinding };
|
||||
|
||||
/** 员工连同其岗位的读模型 —— 治理台员工管理列表用 */
|
||||
export type StaffWithPosition = Staff & { readonly position: Position };
|
||||
|
||||
export type CreateStaffInput = {
|
||||
/** 从 token 或 Admin API 归一出来的一个人 —— `sub` 即 Staff.id */
|
||||
export type Identity = {
|
||||
readonly sub: string;
|
||||
/** preferred_username */
|
||||
readonly account: string;
|
||||
/** name claim,缺则回退 account */
|
||||
readonly name: string;
|
||||
readonly positionId: string;
|
||||
readonly email?: string;
|
||||
/** realm 角色,噪声角色已滤、已排序 */
|
||||
readonly roles: readonly string[];
|
||||
};
|
||||
|
||||
export class StaffNotFoundError extends Error {
|
||||
@@ -28,86 +50,6 @@ export class PositionNotFoundError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountTakenError extends Error {
|
||||
constructor(readonly account: string) {
|
||||
super(`账号已存在:${account}`);
|
||||
this.name = 'AccountTakenError';
|
||||
}
|
||||
}
|
||||
|
||||
/** 停用前置检查未过:在途任务与待答卡须先改派完毕(OS 02) */
|
||||
export class DisableBlockedError extends Error {
|
||||
constructor(readonly check: DisableCheck) {
|
||||
super(`员工停用前置检查未过:在途任务 ${check.openTasks.length} 件、待答卡 ${check.openCards.length} 张`);
|
||||
this.name = 'DisableBlockedError';
|
||||
}
|
||||
}
|
||||
|
||||
/** 登录结果 —— token 交给 app 层种 cookie;失败一律 undefined,不区分原因 */
|
||||
export type IssuedSession = {
|
||||
readonly token: string;
|
||||
readonly expiresAt: string;
|
||||
};
|
||||
|
||||
export type OrgService = {
|
||||
listStaff(): Promise<{ staff: StaffWithPosition[]; positions: Position[] }>;
|
||||
getStaff(staffId: StaffId): Promise<StaffWithPosition>;
|
||||
findByAccount(account: string): Promise<StaffWithPosition | undefined>;
|
||||
createStaff(input: CreateStaffInput): Promise<Staff>;
|
||||
/** 停用前置检查 —— 只读,治理台先看再点 */
|
||||
checkDisable(staffId: StaffId): Promise<DisableCheck>;
|
||||
/** 停用 —— 前置检查不过即抛 DisableBlockedError,不做部分执行 */
|
||||
disableStaff(staffId: StaffId): Promise<void>;
|
||||
listGrants(positionId: string): Promise<Grant[]>;
|
||||
setGrants(positionId: string, grants: readonly Grant[]): Promise<Grant[]>;
|
||||
/** 岗位权限校验 —— app 层鉴权中间件读它 */
|
||||
authorize(staffId: StaffId, object: GrantObject, action: GrantAction): Promise<boolean>;
|
||||
loadSession(account: string): Promise<Session | undefined>;
|
||||
|
||||
/* ── 登录会话(ADR-010)—— 中间件在 app 层,身份判定读这里 ───────────── */
|
||||
|
||||
/** 口令核验通过即签发会话;账号不存在 / 已停用 / 未设口令 / 口令错,一律 undefined */
|
||||
login(account: string, password: string): Promise<IssuedSession | undefined>;
|
||||
/** 登出:该 token 立刻作废(服务端表,不是签名 cookie) */
|
||||
logout(token: string): Promise<void>;
|
||||
/** 解析会话 —— 命中即滑动续期;过期或已作废返回 undefined */
|
||||
resolveSession(token: string): Promise<Session | undefined>;
|
||||
/** 设定口令 —— 明文只在此处停留一瞬,落库前即散列 */
|
||||
setPassword(staffId: StaffId, password: string): Promise<void>;
|
||||
|
||||
/* ── 工作组(ADR-011)—— 起单时的岗位绑定,不是第五种策略 ────────────── */
|
||||
|
||||
/** 建组 / 改组;id 留空即新建 */
|
||||
saveWorkGroup(input: {
|
||||
id?: string;
|
||||
name: string;
|
||||
ownerId: StaffId;
|
||||
members: Readonly<Record<string, StaffId>>;
|
||||
}): Promise<WorkGroup>;
|
||||
/** 某人的工作组 —— 起单时按来源人取,取不到即不装载(全走四策略) */
|
||||
workGroupOf(staffId: StaffId): Promise<WorkGroup | undefined>;
|
||||
/** 装进工作流的快照:改组不改已起的单 */
|
||||
bindingOf(staffId: StaffId): Promise<WorkGroupBinding | undefined>;
|
||||
|
||||
/** 岗位在岗人员 —— 负责人四策略解析的候选集来源(由 process 调用) */
|
||||
staffOfPosition(positionCode: string): Promise<StaffWithPosition[]>;
|
||||
ensureSeeded(): Promise<void>;
|
||||
};
|
||||
|
||||
/* ── 统一鉴权中心(ADR-012) ──────────────────────────────────────────── */
|
||||
|
||||
/** 从 token 或 Admin API 归一出来的一个人 —— `sub` 即 Staff.id */
|
||||
export type Identity = {
|
||||
readonly sub: string;
|
||||
/** preferred_username */
|
||||
readonly account: string;
|
||||
/** name claim,缺则回退 account */
|
||||
readonly name: string;
|
||||
readonly email?: string;
|
||||
/** realm 角色,噪声角色已滤、已排序 */
|
||||
readonly roles: readonly string[];
|
||||
};
|
||||
|
||||
/** 验签没过 —— message 是手册 4.4 的人话,中间件原样回 401 */
|
||||
export class TokenRejectedError extends Error {
|
||||
constructor(message: string) {
|
||||
@@ -131,3 +73,52 @@ export class AccessLockoutError extends Error {
|
||||
this.name = 'AccessLockoutError';
|
||||
}
|
||||
}
|
||||
|
||||
export type OrgService = {
|
||||
/* ── 身份(ADR-012)—— 中间件在 app 层,身份判定读这里 ─────────────── */
|
||||
|
||||
/** 验签 → 名册即时更新 → 角色→端。抛 TokenRejectedError(401)/ AccountDisabledError(403) */
|
||||
authenticate(token: string): Promise<Principal>;
|
||||
/** 票据那条路:按 sub 从库现算;停用抛 AccountDisabledError;不存在回 undefined */
|
||||
principalOf(staffId: StaffId): Promise<Principal | undefined>;
|
||||
/** 名册即时更新的入口 —— 没有就建(NONE 岗、active),有就把变了的列写回;测试也用它造人 */
|
||||
registerIdentity(identity: Identity): Promise<Staff>;
|
||||
|
||||
/* ── 名册与岗位 ───────────────────────────────────────────────────── */
|
||||
|
||||
listStaff(): Promise<{ staff: StaffWithPosition[]; positions: Position[] }>;
|
||||
getStaff(staffId: StaffId): Promise<StaffWithPosition>;
|
||||
/** 岗位在岗人员 —— 负责人四策略解析的候选集来源(由 process 调用);只回 active;NONE 恒回 [] */
|
||||
staffOfPosition(positionCode: string): Promise<StaffWithPosition[]>;
|
||||
setPosition(staffId: StaffId, positionId: PositionId): Promise<StaffWithPosition>;
|
||||
/** Admin API 同步;未配 provisioning 回 configured:false */
|
||||
syncRoster(): Promise<RosterSyncOutcome>;
|
||||
/** 此人身上还挂着什么(只读;OS 02 改派先于停用) */
|
||||
inFlightOf(staffId: StaffId): Promise<InFlightWork>;
|
||||
|
||||
/* ── 角色 → 端 ───────────────────────────────────────────────────── */
|
||||
|
||||
listRoleAccess(): Promise<{ roles: RoleAccess[]; knownRoles: string[] }>;
|
||||
/** 抛 AccessLockoutError(409):改完没有任何在册活跃员工能进运营中心 */
|
||||
setRoleAccess(role: string, ends: readonly Surface[]): Promise<RoleAccess>;
|
||||
deleteRoleAccess(role: string): Promise<void>;
|
||||
/** 能进该端的活跃员工 —— 治理台通知的收件人 */
|
||||
staffWithEnd(surface: Surface): Promise<StaffWithPosition[]>;
|
||||
|
||||
/* ── 工作组(ADR-011)—— 起单时的岗位绑定,不是第五种策略 ────────────── */
|
||||
|
||||
/** 建组 / 改组;id 留空即新建 */
|
||||
saveWorkGroup(input: {
|
||||
id?: string;
|
||||
name: string;
|
||||
ownerId: StaffId;
|
||||
members: Readonly<Record<string, StaffId>>;
|
||||
}): Promise<WorkGroup>;
|
||||
/** 某人的工作组 —— 起单时按来源人取,取不到即不装载(全走四策略) */
|
||||
workGroupOf(staffId: StaffId): Promise<WorkGroup | undefined>;
|
||||
/** 装进工作流的快照:改组不改已起的单 */
|
||||
bindingOf(staffId: StaffId): Promise<WorkGroupBinding | undefined>;
|
||||
|
||||
/** 岗位(含 NONE / TEST)+ 缺省角色→端(表空时)+ 已在册测试账号的策略;幂等 */
|
||||
ensureSeeded(): Promise<void>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user