Product UI no longer switches sessions locally; API clients attach Bearer tokens only and LocalAuthClient stays test-only outside the prod bundle.
185 lines
6.0 KiB
TypeScript
185 lines
6.0 KiB
TypeScript
/**
|
|
* 测试专用本地鉴权客户端 — 仅由单元测试 import,不进入生产 bundle 主路径。
|
|
*/
|
|
import {
|
|
UserAccount,
|
|
SystemAuthConfig,
|
|
LoginPayload,
|
|
LoginResult,
|
|
RegisterPayload,
|
|
RegisterResult,
|
|
CreateAdminUserPayload,
|
|
AccountStatus,
|
|
} from '../types/auth';
|
|
import type { AuthClientOptions } from './auth';
|
|
|
|
const AUTH_STORAGE_KEY_SESSION = 'smdf_auth_session';
|
|
|
|
export class LocalAuthClient {
|
|
private users: Map<number, UserAccount & { password?: string }> = new Map();
|
|
private nextId = 10;
|
|
private config: SystemAuthConfig = { allowRegistration: true, requireApproval: false };
|
|
private env = 'dev';
|
|
private adminUsername = 'admin';
|
|
private adminPassword = 'admin-secret-password-123';
|
|
|
|
constructor(options?: AuthClientOptions) {
|
|
if (options?.env) this.env = options.env;
|
|
if (options?.adminUsername) this.adminUsername = options.adminUsername;
|
|
if (options?.adminPassword) this.adminPassword = options.adminPassword;
|
|
if (options?.allowRegistration !== undefined) this.config.allowRegistration = options.allowRegistration;
|
|
if (options?.requireApproval !== undefined) this.config.requireApproval = options.requireApproval;
|
|
this.initDefaultUsers();
|
|
}
|
|
|
|
private initDefaultUsers() {
|
|
const now = new Date().toISOString();
|
|
this.users.set(1, {
|
|
id: 1,
|
|
username: this.adminUsername,
|
|
password: this.adminPassword,
|
|
name: '系统管理员',
|
|
roleId: 'role-admin',
|
|
position: '系统管理员',
|
|
department: '系统运维部',
|
|
employeeId: 'SYS-000',
|
|
status: 'active',
|
|
mustChangePassword: false,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
});
|
|
}
|
|
|
|
async login(payload: LoginPayload): Promise<LoginResult> {
|
|
const trimmedUser = payload.username.trim();
|
|
let matched: (UserAccount & { password?: string }) | undefined;
|
|
for (const u of this.users.values()) {
|
|
if (u.username.toLowerCase() === trimmedUser.toLowerCase()) {
|
|
matched = u;
|
|
break;
|
|
}
|
|
}
|
|
if (!matched || matched.password !== payload.password) {
|
|
throw new Error('用户名或密码错误');
|
|
}
|
|
if (matched.status === 'pending_approval') {
|
|
throw new Error('账号正在等待管理员审核中,暂无法登录');
|
|
}
|
|
const { password: _, ...safeUser } = matched;
|
|
const result: LoginResult = {
|
|
token: `token-${Date.now()}-${safeUser.id}`,
|
|
user: safeUser,
|
|
mustChangePassword: !!safeUser.mustChangePassword,
|
|
env: this.env,
|
|
};
|
|
this.saveSession(result);
|
|
return result;
|
|
}
|
|
|
|
async register(payload: RegisterPayload): Promise<RegisterResult> {
|
|
if (!this.config.allowRegistration) {
|
|
throw new Error('系统当前已暂停自主注册,请联系系统管理员分配账号');
|
|
}
|
|
const trimmedUser = payload.username.trim();
|
|
if (!trimmedUser || !payload.password || !payload.name?.trim()) {
|
|
throw new Error('用户名、密码与姓名为必填项');
|
|
}
|
|
for (const u of this.users.values()) {
|
|
if (u.username.toLowerCase() === trimmedUser.toLowerCase()) {
|
|
throw new Error(`用户名已存在: ${trimmedUser}`);
|
|
}
|
|
}
|
|
const now = new Date().toISOString();
|
|
const status: AccountStatus = this.config.requireApproval ? 'pending_approval' : 'active';
|
|
const newId = this.nextId++;
|
|
this.users.set(newId, {
|
|
id: newId,
|
|
username: trimmedUser,
|
|
password: payload.password,
|
|
name: payload.name.trim(),
|
|
roleId: 'role-guest',
|
|
position: '访客/待指派',
|
|
department: payload.department || '待分配部门',
|
|
employeeId: payload.employeeId || `EXT-${newId}`,
|
|
status,
|
|
mustChangePassword: false,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
});
|
|
return {
|
|
status,
|
|
message: status === 'pending_approval' ? '注册成功,等待审核' : '注册成功',
|
|
userId: newId,
|
|
};
|
|
}
|
|
|
|
async changePassword(userId: number, oldPass: string, newPass: string): Promise<boolean> {
|
|
const user = this.users.get(userId);
|
|
if (!user || user.password !== oldPass) throw new Error('原密码不正确');
|
|
user.password = newPass;
|
|
user.mustChangePassword = false;
|
|
return true;
|
|
}
|
|
|
|
async updateSystemAuthConfig(newConfig: SystemAuthConfig): Promise<SystemAuthConfig> {
|
|
this.config = { ...newConfig };
|
|
return { ...this.config };
|
|
}
|
|
|
|
async updateUserStatus(userId: number, status: AccountStatus, roleId?: string, position?: string): Promise<UserAccount> {
|
|
const user = this.users.get(userId);
|
|
if (!user) throw new Error('用户不存在');
|
|
user.status = status;
|
|
if (roleId) user.roleId = roleId;
|
|
if (position) user.position = position;
|
|
const { password: _, ...safe } = user;
|
|
return safe;
|
|
}
|
|
|
|
async createAdminUser(payload: CreateAdminUserPayload): Promise<UserAccount> {
|
|
const now = new Date().toISOString();
|
|
const newId = this.nextId++;
|
|
const user: UserAccount & { password?: string } = {
|
|
id: newId,
|
|
username: payload.username.trim(),
|
|
password: payload.initialPassword,
|
|
name: payload.name.trim(),
|
|
roleId: payload.roleId,
|
|
position: payload.position,
|
|
department: payload.department || '研发中心',
|
|
employeeId: payload.employeeId || `EMP-${newId}`,
|
|
status: 'active',
|
|
mustChangePassword: true,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
this.users.set(newId, user);
|
|
const { password: _, ...safe } = user;
|
|
return safe;
|
|
}
|
|
|
|
logout() {
|
|
if (typeof window !== 'undefined' && window.localStorage) {
|
|
window.localStorage.removeItem(AUTH_STORAGE_KEY_SESSION);
|
|
}
|
|
}
|
|
|
|
saveSession(session: LoginResult) {
|
|
if (typeof window !== 'undefined' && window.localStorage) {
|
|
window.localStorage.setItem(AUTH_STORAGE_KEY_SESSION, JSON.stringify(session));
|
|
}
|
|
}
|
|
|
|
loadSavedSession(): LoginResult | null {
|
|
return null;
|
|
}
|
|
|
|
getSessionToken(): string | null {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function createInMemoryAuthClient(options?: AuthClientOptions): LocalAuthClient {
|
|
return new LocalAuthClient({ ...options, localFallback: true });
|
|
}
|