Archived
feat(org): 成员目录 —— Keycloak 读写面穿透、治理台成员/组织/权限三页、姓在前 personName、Staff.kind
后端:`KeycloakDirectory` 端口(users / groups / roles 读写;写直接穿透 Keycloak —— 这里建的人立刻能登录、 删的人立刻消失,停用与收权之后 logout 踢掉会话)、`keycloak-directory.ts` + `keycloak-admin-http.ts` 适配器、 org-directory 契约与 /api/gov 路由、`sync-roster` CLI(ops 路由够不着时整册照过来), 容器按 provisioning 服务账号有无决定注不注入(没配就如实说不可用)。 `personName` 姓在前:汉字直拼「柴浩」、否则留一个空格「Admin Amy」,token 与名册同步两条路同一把尺; `staffKindOf` 按 Keycloak 固定前缀 service-account- 认服务账号,员工那一屏不摆它(`Staff.kind` 不落库,按账号名现判)。 前端:员工那一屏重做成成员 / 组织 / 权限三页签(member-page 下钻),页签栏右端一句话说清这一格管什么。 契约变更:contracts org.ts / api/http.ts / api/org-directory.ts;api-report 已刷新;org CONTRACT.md 与 ADR-012 同步。 2026-08-26 做的,一直留在工作树没入库。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
This commit is contained in:
@@ -11,7 +11,7 @@ import { createTicketIssuer } from '../ticket.ts';
|
||||
|
||||
const position: Position = { id: positionIdSchema.parse('none'), code: 'NONE', name: '未分配' };
|
||||
const principal = (ends: Surface[], roles: string[]): Principal => ({
|
||||
staff: { id: staffIdSchema.parse('sub-1'), account: 'alice', name: '爱丽丝', positionId: position.id, state: 'active', roles },
|
||||
staff: { id: staffIdSchema.parse('sub-1'), account: 'alice', name: '爱丽丝', positionId: position.id, state: 'active', roles, kind: 'person' },
|
||||
position,
|
||||
roles,
|
||||
ends,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 成员与组织的路由归属(ADR-012):15 条全部进契约、全部注册、全部归运营中心。
|
||||
*
|
||||
* 归属写在契约的 `access` 上,鉴权中间件一处按注册表判 —— 所以**漏标一条**
|
||||
* 就会落进 `?? 'any'` 兜底:只要登录过的人都能调。这条测试就是拦那个的。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import * as contracts from '@contracts';
|
||||
import { ORG_DIRECTORY_ROUTES } from '@contracts';
|
||||
import { appRouteRegistry } from '../server.ts';
|
||||
import { buildRouteRegistry, routesOf } from '../route-registry.ts';
|
||||
|
||||
describe('成员与组织路由契约', () => {
|
||||
it('15 条全在契约命名空间里,注册表建得起来', () => {
|
||||
const registry = buildRouteRegistry(routesOf(Object.values(contracts)));
|
||||
expect(ORG_DIRECTORY_ROUTES).toHaveLength(15);
|
||||
for (const route of ORG_DIRECTORY_ROUTES) {
|
||||
expect(registry.has(route.method, route.path)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('读写一律归运营中心 —— 成员目录没有设计端的消费方', () => {
|
||||
for (const route of ORG_DIRECTORY_ROUTES) {
|
||||
expect([route.path, route.access]).toEqual([route.path, 'ops']);
|
||||
}
|
||||
});
|
||||
|
||||
it('查得到的归属就是 ops,不落兜底 —— 兜底等于「登录了就能调」', () => {
|
||||
const registry = appRouteRegistry();
|
||||
for (const route of ORG_DIRECTORY_ROUTES) {
|
||||
// 拿一条**具体**路径去查(带 :param 的模版要真能匹配上,不然线上每次都 miss)
|
||||
const concrete = route.path.replace(/:[A-Za-z_][\w]*/g, 'X');
|
||||
expect([concrete, registry.lookup(route.method, concrete)]).toEqual([concrete, 'ops']);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user