fix(账号): 固化超级管理员身份
This commit is contained in:
57
apps/web/lib/auth-user.test.ts
Normal file
57
apps/web/lib/auth-user.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { resolveAuthUserFromMembers, toAuthUser, type AuthUser } from './auth-user';
|
||||
import type { Member } from './members';
|
||||
|
||||
const member: Member = {
|
||||
id: 'm-8',
|
||||
name: '超级管理员',
|
||||
departmentId: 'dept-1',
|
||||
roleId: 'role-admin',
|
||||
phone: '13200132008',
|
||||
email: 'admin@company.com',
|
||||
password: 'Ftb@2024',
|
||||
createdAt: '2024-01-01',
|
||||
};
|
||||
|
||||
test('toAuthUser removes password and keeps stable member identity', () => {
|
||||
assert.deepEqual(toAuthUser(member), {
|
||||
id: 'm-8',
|
||||
name: '超级管理员',
|
||||
departmentId: 'dept-1',
|
||||
roleId: 'role-admin',
|
||||
phone: '13200132008',
|
||||
email: 'admin@company.com',
|
||||
});
|
||||
});
|
||||
|
||||
test('resolveAuthUserFromMembers refreshes stale display names by member id', () => {
|
||||
const staleUser: AuthUser = {
|
||||
id: 'm-8',
|
||||
name: '陈十',
|
||||
departmentId: 'dept-1',
|
||||
roleId: 'role-admin',
|
||||
phone: '13200132008',
|
||||
email: 'old@company.com',
|
||||
};
|
||||
|
||||
const resolved = resolveAuthUserFromMembers(staleUser, [member]);
|
||||
|
||||
assert.equal(resolved?.id, 'm-8');
|
||||
assert.equal(resolved?.name, '超级管理员');
|
||||
assert.equal(resolved?.email, 'admin@company.com');
|
||||
});
|
||||
|
||||
test('resolveAuthUserFromMembers returns null when the account no longer exists', () => {
|
||||
const staleUser: AuthUser = {
|
||||
id: 'm-9',
|
||||
name: '离职用户',
|
||||
departmentId: 'dept-2',
|
||||
roleId: 'role-dev',
|
||||
phone: '13900139000',
|
||||
email: 'old@company.com',
|
||||
};
|
||||
|
||||
assert.equal(resolveAuthUserFromMembers(staleUser, [member]), null);
|
||||
});
|
||||
26
apps/web/lib/auth-user.ts
Normal file
26
apps/web/lib/auth-user.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { Member } from './members';
|
||||
|
||||
export interface AuthUser {
|
||||
id: string;
|
||||
name: string;
|
||||
roleId: string;
|
||||
departmentId: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export function toAuthUser(member: Member): AuthUser {
|
||||
return {
|
||||
id: member.id,
|
||||
name: member.name,
|
||||
roleId: member.roleId,
|
||||
departmentId: member.departmentId,
|
||||
phone: member.phone,
|
||||
email: member.email,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveAuthUserFromMembers(user: AuthUser, members: Member[]): AuthUser | null {
|
||||
const member = members.find((m) => m.id === user.id);
|
||||
return member ? toAuthUser(member) : null;
|
||||
}
|
||||
84
apps/web/lib/member-system.test.ts
Normal file
84
apps/web/lib/member-system.test.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import {
|
||||
SYSTEM_ADMIN_MEMBER_ID,
|
||||
SYSTEM_ADMIN_MEMBER_NAME,
|
||||
SYSTEM_ADMIN_ROLE_ID,
|
||||
ensureSystemAdminMember,
|
||||
sanitizeSystemAdminPatch,
|
||||
canDeleteSystemMember,
|
||||
resolveMemberDisplayName,
|
||||
isMemberReference,
|
||||
} from './member-system';
|
||||
import type { Member } from './members';
|
||||
|
||||
const oldAdmin: Member = {
|
||||
id: SYSTEM_ADMIN_MEMBER_ID,
|
||||
name: '陈十',
|
||||
departmentId: 'dept-2',
|
||||
roleId: 'role-dev',
|
||||
phone: '13200132008',
|
||||
email: 'chenshi@company.com',
|
||||
password: 'Ftb@2024',
|
||||
createdAt: '2024-01-01',
|
||||
};
|
||||
|
||||
test('normalizes the built-in admin account when old data named it as a regular user', () => {
|
||||
const result = ensureSystemAdminMember([oldAdmin]);
|
||||
|
||||
assert.equal(result.changed, true);
|
||||
assert.equal(result.members.length, 1);
|
||||
assert.equal(result.members[0].id, SYSTEM_ADMIN_MEMBER_ID);
|
||||
assert.equal(result.members[0].name, SYSTEM_ADMIN_MEMBER_NAME);
|
||||
assert.equal(result.members[0].roleId, SYSTEM_ADMIN_ROLE_ID);
|
||||
assert.equal(result.members[0].isSystem, true);
|
||||
assert.equal(result.members[0].phone, oldAdmin.phone);
|
||||
});
|
||||
|
||||
test('adds a built-in admin account when member data has none', () => {
|
||||
const result = ensureSystemAdminMember([]);
|
||||
|
||||
assert.equal(result.changed, true);
|
||||
assert.equal(result.members.length, 1);
|
||||
assert.equal(result.members[0].id, SYSTEM_ADMIN_MEMBER_ID);
|
||||
assert.equal(result.members[0].name, SYSTEM_ADMIN_MEMBER_NAME);
|
||||
assert.equal(result.members[0].roleId, SYSTEM_ADMIN_ROLE_ID);
|
||||
});
|
||||
|
||||
test('protects the built-in admin account from identity and role edits', () => {
|
||||
const patch = sanitizeSystemAdminPatch(SYSTEM_ADMIN_MEMBER_ID, {
|
||||
name: '陈十',
|
||||
roleId: 'role-dev',
|
||||
departmentId: 'dept-3',
|
||||
phone: '13900139000',
|
||||
});
|
||||
|
||||
assert.equal(patch.name, undefined);
|
||||
assert.equal(patch.roleId, undefined);
|
||||
assert.equal(patch.departmentId, undefined);
|
||||
assert.equal(patch.phone, '13900139000');
|
||||
});
|
||||
|
||||
test('blocks deleting the built-in admin account', () => {
|
||||
assert.equal(canDeleteSystemMember(SYSTEM_ADMIN_MEMBER_ID), false);
|
||||
assert.equal(canDeleteSystemMember('m-9'), true);
|
||||
});
|
||||
|
||||
test('resolves legacy Chen Shi labels to the built-in admin display name', () => {
|
||||
const result = ensureSystemAdminMember([oldAdmin]);
|
||||
|
||||
assert.equal(resolveMemberDisplayName('陈十', result.members), SYSTEM_ADMIN_MEMBER_NAME);
|
||||
assert.equal(resolveMemberDisplayName(SYSTEM_ADMIN_MEMBER_ID, result.members), SYSTEM_ADMIN_MEMBER_NAME);
|
||||
assert.equal(resolveMemberDisplayName('张三', result.members), '张三');
|
||||
});
|
||||
|
||||
test('matches legacy admin names as the built-in admin member', () => {
|
||||
const result = ensureSystemAdminMember([oldAdmin]);
|
||||
const admin = result.members[0];
|
||||
|
||||
assert.equal(isMemberReference('陈十', admin), true);
|
||||
assert.equal(isMemberReference(SYSTEM_ADMIN_MEMBER_ID, admin), true);
|
||||
assert.equal(isMemberReference(SYSTEM_ADMIN_MEMBER_NAME, admin), true);
|
||||
assert.equal(isMemberReference('张三', admin), false);
|
||||
});
|
||||
92
apps/web/lib/member-system.ts
Normal file
92
apps/web/lib/member-system.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { Member } from './members';
|
||||
|
||||
export const SYSTEM_ADMIN_MEMBER_ID = 'm-8';
|
||||
export const SYSTEM_ADMIN_MEMBER_NAME = '超级管理员';
|
||||
export const SYSTEM_ADMIN_ROLE_ID = 'role-admin';
|
||||
export const SYSTEM_ADMIN_DEPARTMENT_ID = 'dept-1';
|
||||
const LEGACY_SYSTEM_ADMIN_NAMES = ['陈十'];
|
||||
|
||||
export const SYSTEM_ADMIN_MEMBER: Member = {
|
||||
id: SYSTEM_ADMIN_MEMBER_ID,
|
||||
name: SYSTEM_ADMIN_MEMBER_NAME,
|
||||
departmentId: SYSTEM_ADMIN_DEPARTMENT_ID,
|
||||
roleId: SYSTEM_ADMIN_ROLE_ID,
|
||||
phone: '13200132008',
|
||||
email: 'admin@company.com',
|
||||
password: 'Ftb@2024',
|
||||
createdAt: '2024-01-01',
|
||||
isSystem: true,
|
||||
};
|
||||
|
||||
function normalizeSystemAdminMember(member: Partial<Member>): Member {
|
||||
return {
|
||||
...SYSTEM_ADMIN_MEMBER,
|
||||
...member,
|
||||
id: SYSTEM_ADMIN_MEMBER_ID,
|
||||
name: SYSTEM_ADMIN_MEMBER_NAME,
|
||||
departmentId: SYSTEM_ADMIN_DEPARTMENT_ID,
|
||||
roleId: SYSTEM_ADMIN_ROLE_ID,
|
||||
phone: member.phone || SYSTEM_ADMIN_MEMBER.phone,
|
||||
email: member.email || SYSTEM_ADMIN_MEMBER.email,
|
||||
password: member.password || SYSTEM_ADMIN_MEMBER.password,
|
||||
createdAt: member.createdAt || SYSTEM_ADMIN_MEMBER.createdAt,
|
||||
isSystem: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function ensureSystemAdminMember(members: Member[]): { members: Member[]; changed: boolean } {
|
||||
const index = members.findIndex((m) => m.id === SYSTEM_ADMIN_MEMBER_ID);
|
||||
if (index === -1) {
|
||||
return { members: [SYSTEM_ADMIN_MEMBER, ...members], changed: true };
|
||||
}
|
||||
|
||||
const current = members[index];
|
||||
const normalized = normalizeSystemAdminMember(current);
|
||||
const changed =
|
||||
current.name !== normalized.name ||
|
||||
current.departmentId !== normalized.departmentId ||
|
||||
current.roleId !== normalized.roleId ||
|
||||
current.phone !== normalized.phone ||
|
||||
current.email !== normalized.email ||
|
||||
current.password !== normalized.password ||
|
||||
current.createdAt !== normalized.createdAt ||
|
||||
current.isSystem !== true;
|
||||
|
||||
if (!changed) return { members, changed: false };
|
||||
|
||||
return {
|
||||
members: members.map((m, i) => (i === index ? normalized : m)),
|
||||
changed: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function sanitizeSystemAdminPatch(id: string, data: Partial<Member>): Partial<Member> {
|
||||
if (id !== SYSTEM_ADMIN_MEMBER_ID) return data;
|
||||
const { name, departmentId, roleId, isSystem, ...allowed } = data;
|
||||
return { ...allowed, isSystem: true };
|
||||
}
|
||||
|
||||
export function canDeleteSystemMember(id: string): boolean {
|
||||
return id !== SYSTEM_ADMIN_MEMBER_ID;
|
||||
}
|
||||
|
||||
export function isSystemAdminMember(member: Pick<Member, 'id'>): boolean {
|
||||
return member.id === SYSTEM_ADMIN_MEMBER_ID;
|
||||
}
|
||||
|
||||
export function resolveMemberDisplayName(ref: string | undefined | null, members: Member[]): string {
|
||||
if (!ref) return '-';
|
||||
const member = members.find((m) => m.id === ref || m.name === ref);
|
||||
if (member) return member.name;
|
||||
if (LEGACY_SYSTEM_ADMIN_NAMES.includes(ref)) {
|
||||
const admin = members.find((m) => m.id === SYSTEM_ADMIN_MEMBER_ID);
|
||||
return admin?.name ?? SYSTEM_ADMIN_MEMBER_NAME;
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
export function isMemberReference(ref: string | undefined | null, member: Pick<Member, 'id' | 'name'>): boolean {
|
||||
if (!ref) return false;
|
||||
if (ref === member.id || ref === member.name) return true;
|
||||
return member.id === SYSTEM_ADMIN_MEMBER_ID && LEGACY_SYSTEM_ADMIN_NAMES.includes(ref);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export interface Member {
|
||||
email: string;
|
||||
password: string;
|
||||
createdAt: string;
|
||||
isSystem?: boolean;
|
||||
}
|
||||
|
||||
export interface RoleItem {
|
||||
|
||||
Reference in New Issue
Block a user