工作台撤顶栏:身份菜单收进左轨底部(悬停从轨右侧弹出);姓名展示改姓在前(token claims 与名册同步两处拼装同尺)

This commit is contained in:
shanqin
2026-08-26 19:42:31 +08:00
parent 99f021f964
commit 0c029b694d
8 changed files with 59 additions and 40 deletions
@@ -27,6 +27,11 @@ describe('identityOf —— claims 归一成一个人', () => {
expect('email' in id).toBe(false);
});
it('姓/名散件在 → 姓在前拼「姓 名」,不采用全名字段的名+姓序(2026-08-26 用户定)', () => {
const id = identityOf({ sub: 's', preferred_username: 'shan', name: '亲 删', given_name: '亲', family_name: '删' });
expect(id.name).toBe('删 亲');
});
it('realm_access 有但 roles 缺 → 空角色', () => {
expect(identityOf({ sub: 's', preferred_username: 'bob', realm_access: {} }).roles).toEqual([]);
});
@@ -42,13 +42,13 @@ function fakeKeycloak(): { fetch: typeof fetch; calls: string[] } {
}
describe('Keycloak Admin API 名册', () => {
it('分页拉全(100 + 5)、姓名拼 firstName lastName、缺名回退用户名、角色滤噪声、enabled 照抄', async () => {
it('分页拉全(100 + 5)、姓名姓在前拼 lastName firstName、缺名回退用户名、角色滤噪声、enabled 照抄', async () => {
const kc = fakeKeycloak();
const roster = createKeycloakRoster({ url: 'https://kc.test/', realm: 'corp', clientId: 'provisioning', secret: 's', fetch: kc.fetch });
const users = await roster.listUsers();
expect(users).toHaveLength(105);
expect(users[0]).toEqual({ sub: 'sub-0', account: 'u0', name: 'u0', email: 'u0@x.test', roles: ['user'], enabled: true });
expect(users[1]).toEqual({ sub: 'sub-1', account: 'u1', name: '名 1', email: 'u1@x.test', roles: ['admin', 'user'], enabled: true });
expect(users[1]).toEqual({ sub: 'sub-1', account: 'u1', name: '1 名', email: 'u1@x.test', roles: ['admin', 'user'], enabled: true });
expect(users[3]?.enabled).toBe(false);
expect(kc.calls.filter((c) => c.includes('/users?')).length).toBe(2);
});
@@ -76,7 +76,8 @@ export function createKeycloakRoster(options: KeycloakRosterOptions): RosterSour
const out: RosterUser[] = [];
for (const u of users) {
const roles = await adminGet<KcRole[]>(`/users/${encodeURIComponent(u.id)}/role-mappings/realm/composite`);
const name = [u.firstName, u.lastName]
/* 姓在前(2026-08-26 用户定)—— 与 token 那条路(claims.ts `identityOf`)同一把尺 */
const name = [u.lastName, u.firstName]
.filter((s): s is string => s !== undefined && s.trim() !== '')
.join(' ')
.trim();
@@ -24,6 +24,8 @@ const claimsSchema = z.object({
sub: z.string().min(1),
preferred_username: z.string().min(1),
name: z.string().optional(),
given_name: z.string().optional(),
family_name: z.string().optional(),
email: z.string().optional(),
realm_access: z.object({ roles: z.array(z.string()).optional() }).optional(),
});
@@ -39,6 +41,8 @@ function toVerifiedClaims(claims: z.infer<typeof claimsSchema>): VerifiedClaims
sub: claims.sub,
preferred_username: claims.preferred_username,
...(claims.name === undefined ? {} : { name: claims.name }),
...(claims.given_name === undefined ? {} : { given_name: claims.given_name }),
...(claims.family_name === undefined ? {} : { family_name: claims.family_name }),
...(claims.email === undefined ? {} : { email: claims.email }),
...(claims.realm_access === undefined
? {}
@@ -21,7 +21,13 @@ export function normalizeRoles(roles: readonly string[]): string[] {
export function identityOf(claims: VerifiedClaims): Identity {
const account = claims.preferred_username;
const name = claims.name === undefined || claims.name.trim() === '' ? account : claims.name.trim();
/* 姓名展示姓在前(2026-08-26 用户定):有姓/名散件就按「姓 名」拼;
没有散件才用 Keycloak 的全名 `name`(它按「名 姓」拼,中文场景顺序是反的) */
const cjk = [claims.family_name, claims.given_name]
.filter((s): s is string => s !== undefined && s.trim() !== '')
.join(' ');
const fallback = claims.name === undefined || claims.name.trim() === '' ? account : claims.name.trim();
const name = cjk !== '' ? cjk : fallback;
const email = claims.email === undefined || claims.email.trim() === '' ? undefined : claims.email.trim();
return {
sub: claims.sub,
+4
View File
@@ -14,6 +14,10 @@ export type VerifiedClaims = {
readonly sub: string;
readonly preferred_username: string;
readonly name?: string;
/* 名/姓分开收(2026-08-26 用户定:中文姓名姓在前)—— Keycloak 的 `name` 是全名(名+姓),
展示要「姓 名」得靠这两个散件拼;没带就回退 `name` 原样 */
readonly given_name?: string;
readonly family_name?: string;
readonly email?: string;
readonly realm_access?: { readonly roles?: readonly string[] };
};