移除账号启用状态统一使用归档
This commit is contained in:
@@ -47,4 +47,28 @@ describe('AuthService — authentication boundaries', () => {
|
||||
).rejects.toThrow('账号已失效');
|
||||
expect(userRepo.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows a legacy disabled user because archive is the only account status', async () => {
|
||||
const userRepo = {
|
||||
findOne: jest.fn().mockResolvedValue({
|
||||
id: 3,
|
||||
username: 'legacy-disabled',
|
||||
name: '旧账号',
|
||||
passwordHash: await bcrypt.hash('secret', 4),
|
||||
isActive: false,
|
||||
isArchived: false,
|
||||
roles: [],
|
||||
}),
|
||||
save: jest.fn(),
|
||||
};
|
||||
const service = new AuthService(
|
||||
userRepo as never,
|
||||
{ sign: jest.fn().mockReturnValue('token') } as never,
|
||||
{ getUserPermissions: jest.fn().mockResolvedValue([]) } as never,
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.login({ username: 'legacy-disabled', password: 'secret' }, '192.0.2.11'),
|
||||
).resolves.toEqual(expect.objectContaining({ access_token: 'token' }));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ export class AuthService {
|
||||
this.recordFailedAttempt(attemptKey);
|
||||
throw new UnauthorizedException('用户名或密码错误');
|
||||
}
|
||||
if (!user.isActive || user.isArchived) {
|
||||
if (user.isArchived) {
|
||||
throw new UnauthorizedException('账号已失效,请联系管理员');
|
||||
}
|
||||
const valid = await bcrypt.compare(dto.password, user.passwordHash);
|
||||
|
||||
@@ -50,16 +50,32 @@ describe('JwtStrategy', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ id: 7, isActive: false, isArchived: false, roles: [] }],
|
||||
[{ id: 7, isActive: true, isArchived: true, roles: [] }],
|
||||
[null],
|
||||
])('rejects disabled, archived, or deleted users', async (user) => {
|
||||
const userRepo = { findOne: jest.fn().mockResolvedValue(user) };
|
||||
it.each([[{ id: 7, isArchived: true, roles: [] }], [null]])(
|
||||
'rejects archived or deleted users',
|
||||
async (user) => {
|
||||
const userRepo = { findOne: jest.fn().mockResolvedValue(user) };
|
||||
const strategy = new JwtStrategy(config as never, userRepo as never);
|
||||
|
||||
await expect(strategy.validate({ sub: 7, username: 'teacher' })).rejects.toBeInstanceOf(
|
||||
UnauthorizedException,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('accepts a legacy disabled user when the account is not archived', async () => {
|
||||
const userRepo = {
|
||||
findOne: jest.fn().mockResolvedValue({
|
||||
id: 7,
|
||||
username: 'teacher',
|
||||
isActive: false,
|
||||
isArchived: false,
|
||||
roles: [],
|
||||
}),
|
||||
};
|
||||
const strategy = new JwtStrategy(config as never, userRepo as never);
|
||||
|
||||
await expect(strategy.validate({ sub: 7, username: 'teacher' })).rejects.toBeInstanceOf(
|
||||
UnauthorizedException,
|
||||
await expect(strategy.validate({ sub: 7, username: 'teacher' })).resolves.toEqual(
|
||||
expect.objectContaining({ id: 7, username: 'teacher' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
where: { id: payload.sub },
|
||||
relations: ['roles', 'roles.permissions'],
|
||||
});
|
||||
if (!user || !user.isActive || user.isArchived) {
|
||||
if (!user || user.isArchived) {
|
||||
throw new UnauthorizedException('账号已失效,请重新登录');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
ArrayUnique,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
@@ -70,10 +69,6 @@ export class UpdateUserDto {
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayUnique()
|
||||
|
||||
@@ -92,4 +92,10 @@ describe('RBAC DTO id arrays', () => {
|
||||
])('rejects invalid, duplicate, or non-positive ids for %p', async (metatype, value) => {
|
||||
await expect(pipe.transform(value, { type: 'body', metatype })).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
it('strips the retired isActive field from account updates', async () => {
|
||||
await expect(
|
||||
pipe.transform({ name: 'Alice', isActive: false }, { type: 'body', metatype: UpdateUserDto }),
|
||||
).resolves.toEqual({ name: 'Alice' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,7 +35,12 @@ describe('RbacService seedData', () => {
|
||||
save: jest.fn(async (value: any) => value),
|
||||
find: jest.fn(async () => [systemAdminRole]),
|
||||
};
|
||||
const userRepo = { count: jest.fn(async () => 1), create: jest.fn(), save: jest.fn() };
|
||||
const userRepo = {
|
||||
update: jest.fn(async () => ({ affected: 0 })),
|
||||
count: jest.fn(async () => 1),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
};
|
||||
|
||||
const service = new RbacService(
|
||||
permRepo as never,
|
||||
@@ -101,7 +106,12 @@ describe('RbacService seedData', () => {
|
||||
save: jest.fn(async (value) => value),
|
||||
find: jest.fn(async () => [teacherRole]),
|
||||
};
|
||||
const userRepo = { count: jest.fn(async () => 1), create: jest.fn(), save: jest.fn() };
|
||||
const userRepo = {
|
||||
update: jest.fn(async () => ({ affected: 0 })),
|
||||
count: jest.fn(async () => 1),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
};
|
||||
|
||||
const service = new RbacService(
|
||||
permRepo as never,
|
||||
@@ -184,6 +194,7 @@ describe('RbacService legacy role consolidation', () => {
|
||||
remove: jest.fn(async (value) => value),
|
||||
};
|
||||
const userRepo = {
|
||||
update: jest.fn(async () => ({ affected: 0 })),
|
||||
count: jest.fn(async () => 1),
|
||||
findOne: jest.fn(async () => user),
|
||||
save: jest.fn(async (value) => value),
|
||||
|
||||
@@ -296,6 +296,13 @@ export class RbacService {
|
||||
}
|
||||
|
||||
async seedData(): Promise<void> {
|
||||
const restoredLegacyUsers = await this.userRepo.update({ isActive: false }, { isActive: true });
|
||||
if (restoredLegacyUsers.affected) {
|
||||
this.logger.log(
|
||||
`已恢复 ${restoredLegacyUsers.affected} 个旧版禁用账号,账号状态现统一由归档管理`,
|
||||
);
|
||||
}
|
||||
|
||||
// Step 1: 幂等插入所有权限点(先查后插,兼容 SQLite/MySQL)
|
||||
for (const p of PRESET_PERMISSIONS) {
|
||||
const exists = await this.permRepo.findOne({ where: { code: p.code } });
|
||||
@@ -568,7 +575,6 @@ export class RbacService {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
name: u.name,
|
||||
isActive: u.isActive,
|
||||
isArchived: u.isArchived,
|
||||
studentStatus: statusMap.get(u.id) || null,
|
||||
lastLoginAt: u.lastLoginAt,
|
||||
@@ -597,7 +603,7 @@ export class RbacService {
|
||||
|
||||
async updateUser(
|
||||
id: number,
|
||||
dto: { username?: string; name?: string; isActive?: boolean; roleIds?: number[] },
|
||||
dto: { username?: string; name?: string; roleIds?: number[] },
|
||||
) {
|
||||
const user = await this.userRepo.findOne({ where: { id }, relations: ['roles'] });
|
||||
if (!user) throw new Error('用户不存在');
|
||||
@@ -607,7 +613,6 @@ export class RbacService {
|
||||
user.username = dto.username;
|
||||
}
|
||||
if (dto.name !== undefined) user.name = dto.name;
|
||||
if (dto.isActive !== undefined) user.isActive = dto.isActive;
|
||||
if (dto.roleIds !== undefined) {
|
||||
user.roles = dto.roleIds.length > 0 ? await this.resolveRoles(dto.roleIds) : [];
|
||||
}
|
||||
@@ -634,7 +639,7 @@ export class RbacService {
|
||||
async restoreUser(id: number) {
|
||||
const user = await this.userRepo.findOne({ where: { id } });
|
||||
if (!user) throw new Error('用户不存在');
|
||||
await this.userRepo.update(id, { isArchived: false });
|
||||
await this.userRepo.update(id, { isArchived: false, isActive: true });
|
||||
return { message: '用户已恢复' };
|
||||
}
|
||||
|
||||
@@ -766,7 +771,8 @@ export class RbacService {
|
||||
.where('(role.code IN (:...roleCodes) OR role.name IN (:...roleNames))', {
|
||||
roleCodes: teacherRoleCodes,
|
||||
roleNames: teacherRoleNames,
|
||||
});
|
||||
})
|
||||
.andWhere('u.isArchived = :isArchived', { isArchived: false });
|
||||
|
||||
if (query?.search) {
|
||||
qb.andWhere('(u.name LIKE :s OR u.username LIKE :s)', { s: `%${query.search}%` });
|
||||
@@ -795,7 +801,6 @@ export class RbacService {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
name: u.name,
|
||||
isActive: u.isActive,
|
||||
profile: u.profile,
|
||||
lastLoginAt: u.lastLoginAt,
|
||||
roles: u.roles || [],
|
||||
|
||||
@@ -140,7 +140,7 @@ export class StudentsService {
|
||||
: [];
|
||||
const teacherMap = new Map<number, { id: number; name: string; username: string }>();
|
||||
for (const assignment of assignments) {
|
||||
if (!assignment.user || !assignment.user.isActive || assignment.user.isArchived) continue;
|
||||
if (!assignment.user || assignment.user.isArchived) continue;
|
||||
teacherMap.set(assignment.userId, {
|
||||
id: assignment.userId,
|
||||
name: assignment.user.name || assignment.user.username,
|
||||
|
||||
Reference in New Issue
Block a user