From a9c6578569ee819cedaebd8fe85363c394183845 Mon Sep 17 00:00:00 2001 From: xiong Date: Fri, 24 Jul 2026 09:41:37 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E7=8F=AD=E7=BA=A7=E5=8F=AF=E8=A7=81=E8=8C=83=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/classes/classes.controller.spec.ts | 96 +++++++++++++++++++ apps/server/src/classes/classes.controller.ts | 21 ++-- 2 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 apps/server/src/classes/classes.controller.spec.ts diff --git a/apps/server/src/classes/classes.controller.spec.ts b/apps/server/src/classes/classes.controller.spec.ts new file mode 100644 index 0000000..1c8d7e4 --- /dev/null +++ b/apps/server/src/classes/classes.controller.spec.ts @@ -0,0 +1,96 @@ +import { ClassesController } from './classes.controller'; +import { ClassesService } from './classes.service'; +import { OperationLogsService } from '../operation-logs/operation-logs.service'; +import { NotificationsService } from '../notifications/notifications.service'; +import { CaslAction, SubjectName } from '../authorization'; + +describe('ClassesController - class data scope', () => { + const service = { + getAccessibleClassIds: jest.fn(), + assertClassAccess: jest.fn(), + findAll: jest.fn(), + findOne: jest.fn(), + getSchedule: jest.fn(), + getAttendanceSummary: jest.fn(), + getStudents: jest.fn(), + getTeachers: jest.fn(), + }; + const authzService = { can: jest.fn() }; + const logService = {}; + const notificationsService = {}; + let controller: ClassesController; + + const request = (permissions: string[] = [], isSuperAdmin = false) => ({ + user: { + id: 21, + username: 'user', + permissions, + isSuperAdmin, + roles: [], + }, + }); + + beforeEach(() => { + jest.clearAllMocks(); + authzService.can.mockImplementation((req, action, subject) => { + if (subject !== SubjectName.Class) return false; + if (req.user.isSuperAdmin && action === CaslAction.Manage) return true; + if (action === CaslAction.Create) return req.user.permissions.includes('class:create'); + if (action === CaslAction.Update) return req.user.permissions.includes('class:edit'); + return false; + }); + service.getAccessibleClassIds.mockResolvedValue(undefined); + service.assertClassAccess.mockResolvedValue(undefined); + service.findAll.mockResolvedValue([]); + service.findOne.mockResolvedValue({ id: 8 }); + service.getSchedule.mockResolvedValue([]); + service.getAttendanceSummary.mockResolvedValue({}); + service.getStudents.mockResolvedValue([]); + service.getTeachers.mockResolvedValue([]); + controller = new ClassesController( + service as unknown as ClassesService, + logService as unknown as OperationLogsService, + notificationsService as unknown as NotificationsService, + authzService as never, + ); + }); + + it.each([ + ['class creator', request(['class:view', 'class:create'])], + ['class editor', request(['class:view', 'class:edit'])], + ['super admin', request([], true)], + ])('allows %s to list all classes', async (_label, req) => { + await controller.findAll({}, req); + + expect(service.getAccessibleClassIds).toHaveBeenCalledWith(21, true); + expect(service.findAll).toHaveBeenCalledWith({}, undefined); + }); + + it('keeps view-only users scoped to their assigned classes', async () => { + service.getAccessibleClassIds.mockResolvedValue([8]); + + await controller.findAll({}, request(['class:view'])); + + expect(service.getAccessibleClassIds).toHaveBeenCalledWith(21, false); + expect(service.findAll).toHaveBeenCalledWith({}, [8]); + }); + + it('allows a class creator to read an unassigned class through every detail endpoint', async () => { + const req = request(['class:view', 'class:create']); + + await controller.findOne('8', req); + await controller.getSchedule('8', {}, req); + await controller.getAttendanceSummary('8', {}, req); + await controller.getStudents('8', req); + await controller.getTeachers('8', req); + + expect(service.assertClassAccess).toHaveBeenCalledTimes(5); + expect(service.assertClassAccess).toHaveBeenCalledWith(21, 8, true); + }); + + it('requires a view-only user to be assigned before reading class details', async () => { + await controller.findOne('8', request(['class:view'])); + + expect(service.assertClassAccess).toHaveBeenCalledWith(21, 8, false); + }); +}); diff --git a/apps/server/src/classes/classes.controller.ts b/apps/server/src/classes/classes.controller.ts index a3e1c7e..2dc8dca 100644 --- a/apps/server/src/classes/classes.controller.ts +++ b/apps/server/src/classes/classes.controller.ts @@ -54,12 +54,20 @@ export class ClassesController { private readonly authz: AuthorizationService, ) {} - private assertReadAccess(req: AuthenticatedRequest, classId: number) { - // Legacy: Manage (super_admin) or Update (class:edit) grants broad class access - const canManageAll = + private canManageAllClasses(req: AuthenticatedRequest): boolean { + return ( this.authz.can(req, CaslAction.Manage, SubjectName.Class) || - this.authz.can(req, CaslAction.Update, SubjectName.Class); - return this.service.assertClassAccess(req.user.id, classId, canManageAll); + this.authz.can(req, CaslAction.Create, SubjectName.Class) || + this.authz.can(req, CaslAction.Update, SubjectName.Class) + ); + } + + private assertReadAccess(req: AuthenticatedRequest, classId: number) { + return this.service.assertClassAccess( + req.user.id, + classId, + this.canManageAllClasses(req), + ); } @Get() @@ -67,8 +75,7 @@ export class ClassesController { async findAll(@Query() query: QueryClassDto, @Request() req: AuthenticatedRequest) { const classIds = await this.service.getAccessibleClassIds( req.user.id, - this.authz.can(req, CaslAction.Manage, SubjectName.Class) || - this.authz.can(req, CaslAction.Update, SubjectName.Class), + this.canManageAllClasses(req), ); return this.service.findAll(query, classIds); } From 13ef35744829f0e23c7f62c171827aa100a0b9f2 Mon Sep 17 00:00:00 2001 From: xiong Date: Fri, 24 Jul 2026 09:53:04 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E5=90=AF=E7=94=A8=E7=8A=B6=E6=80=81=E7=BB=9F=E4=B8=80=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E5=BD=92=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../teacher-candidate.integration.test.ts | 8 ++--- .../src/pages/Classes/teacher-candidate.ts | 3 +- apps/admin/src/pages/Teachers/index.tsx | 8 ----- apps/admin/src/pages/Users/index.tsx | 29 +---------------- apps/server/src/auth/auth.service.spec.ts | 24 ++++++++++++++ apps/server/src/auth/auth.service.ts | 2 +- .../src/auth/strategies/jwt.strategy.spec.ts | 32 ++++++++++++++----- .../src/auth/strategies/jwt.strategy.ts | 2 +- apps/server/src/rbac/dto/rbac.dto.ts | 5 --- apps/server/src/rbac/rbac.boundary.spec.ts | 6 ++++ apps/server/src/rbac/rbac.seed.spec.ts | 15 +++++++-- apps/server/src/rbac/rbac.service.ts | 17 ++++++---- apps/server/src/students/students.service.ts | 2 +- 13 files changed, 86 insertions(+), 67 deletions(-) diff --git a/apps/admin/src/pages/Classes/teacher-candidate.integration.test.ts b/apps/admin/src/pages/Classes/teacher-candidate.integration.test.ts index 9f561d4..ebfe45b 100644 --- a/apps/admin/src/pages/Classes/teacher-candidate.integration.test.ts +++ b/apps/admin/src/pages/Classes/teacher-candidate.integration.test.ts @@ -10,7 +10,6 @@ const baseUser = (overrides: Partial = {}): TeacherCandida id: 1, username: 'teacher', name: '测试老师', - isActive: true, isArchived: false, studentStatus: null, roles: [{ code: 'teacher', name: '任课老师' }], @@ -29,13 +28,12 @@ describe('class teacher candidates', () => { expect(isTeacherCandidate(baseUser({ studentStatus: 'staff' }))).toBe(true); }); - it('excludes active students, disabled, archived, and super-admin accounts', () => { + it('excludes active students, archived, and super-admin accounts', () => { const users = [ baseUser({ id: 1, studentStatus: 'active' }), - baseUser({ id: 2, isActive: false }), - baseUser({ id: 3, isArchived: true }), + baseUser({ id: 2, isArchived: true }), baseUser({ - id: 4, + id: 3, roles: [{ code: 'super_admin', name: '超级管理员' }], }), ]; diff --git a/apps/admin/src/pages/Classes/teacher-candidate.ts b/apps/admin/src/pages/Classes/teacher-candidate.ts index 823c72f..cae2d8c 100644 --- a/apps/admin/src/pages/Classes/teacher-candidate.ts +++ b/apps/admin/src/pages/Classes/teacher-candidate.ts @@ -7,7 +7,6 @@ export interface TeacherCandidateUser { id: number; username: string; name?: string | null; - isActive: boolean; isArchived: boolean; studentStatus?: string | null; roles?: TeacherCandidateRole[]; @@ -18,7 +17,7 @@ const isSuperAdminRole = (role: TeacherCandidateRole) => role.code === 'super_admin' || role.name === '超级管理员' || role.name === '超管'; export const isTeacherCandidate = (user: TeacherCandidateUser) => { - if (!user.isActive || user.isArchived) return false; + if (user.isArchived) return false; if (user.studentStatus && user.studentStatus !== 'staff') return false; return !(user.roles || []).some(isSuperAdminRole); }; diff --git a/apps/admin/src/pages/Teachers/index.tsx b/apps/admin/src/pages/Teachers/index.tsx index add59f7..7fc003c 100644 --- a/apps/admin/src/pages/Teachers/index.tsx +++ b/apps/admin/src/pages/Teachers/index.tsx @@ -12,7 +12,6 @@ interface TeacherRow { id: number; username: string; name: string; - isActive: boolean; profile: { subjects?: string[]; joinedAt?: string; qualifications?: string } | null; lastLoginAt: string; roles: { code: string; name: string }[]; @@ -175,13 +174,6 @@ const TeachersPage: React.FC = () => { ), }, - { - title: '状态', - dataIndex: 'isActive', - key: 'status', - width: 90, - render: (v: boolean) => {v ? '在职' : '停用'}, - }, { title: '最后登录', dataIndex: 'lastLoginAt', diff --git a/apps/admin/src/pages/Users/index.tsx b/apps/admin/src/pages/Users/index.tsx index d633aac..edcf68d 100644 --- a/apps/admin/src/pages/Users/index.tsx +++ b/apps/admin/src/pages/Users/index.tsx @@ -87,7 +87,6 @@ const UsersPage: React.FC = () => { form.setFieldsValue({ username: record.username, name: record.name, - isActive: record.isActive, roleIds: record.roles?.map((r: any) => r.id) || [], }); setModalOpen(true); @@ -101,7 +100,6 @@ const UsersPage: React.FC = () => { await api.put(`/rbac/users/${editing.id}`, { username: values.username, name: values.name, - isActive: values.isActive, roleIds: values.roleIds || [], }); message.success('更新成功'); @@ -223,26 +221,6 @@ const UsersPage: React.FC = () => { ), }, - { - title: '状态', - dataIndex: 'isActive', - width: 80, - render: (v: boolean, r: any) => ( - saveCell(r, 'isActive', String(next) === 'true')} - > - {v ? '启用' : '禁用'} - - ), - }, { title: '最后登录', dataIndex: 'lastLoginAt', @@ -350,7 +328,7 @@ const UsersPage: React.FC = () => { dataSource={data} rowKey="id" loading={loading} - scroll={{ x: 1250 }} + scroll={{ x: 1150 }} pagination={false} /> @@ -382,11 +360,6 @@ const UsersPage: React.FC = () => { - {editing && ( - - - - )} { ).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' })); + }); }); diff --git a/apps/server/src/auth/auth.service.ts b/apps/server/src/auth/auth.service.ts index 3de488d..b3dd0a0 100644 --- a/apps/server/src/auth/auth.service.ts +++ b/apps/server/src/auth/auth.service.ts @@ -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); diff --git a/apps/server/src/auth/strategies/jwt.strategy.spec.ts b/apps/server/src/auth/strategies/jwt.strategy.spec.ts index 1443ab0..74da847 100644 --- a/apps/server/src/auth/strategies/jwt.strategy.spec.ts +++ b/apps/server/src/auth/strategies/jwt.strategy.spec.ts @@ -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' }), ); }); }); diff --git a/apps/server/src/auth/strategies/jwt.strategy.ts b/apps/server/src/auth/strategies/jwt.strategy.ts index ca967a1..0e9ab64 100644 --- a/apps/server/src/auth/strategies/jwt.strategy.ts +++ b/apps/server/src/auth/strategies/jwt.strategy.ts @@ -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('账号已失效,请重新登录'); } diff --git a/apps/server/src/rbac/dto/rbac.dto.ts b/apps/server/src/rbac/dto/rbac.dto.ts index c75029e..5229106 100644 --- a/apps/server/src/rbac/dto/rbac.dto.ts +++ b/apps/server/src/rbac/dto/rbac.dto.ts @@ -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() diff --git a/apps/server/src/rbac/rbac.boundary.spec.ts b/apps/server/src/rbac/rbac.boundary.spec.ts index 843e819..6e8067b 100644 --- a/apps/server/src/rbac/rbac.boundary.spec.ts +++ b/apps/server/src/rbac/rbac.boundary.spec.ts @@ -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' }); + }); }); diff --git a/apps/server/src/rbac/rbac.seed.spec.ts b/apps/server/src/rbac/rbac.seed.spec.ts index 9f0669c..42fef73 100644 --- a/apps/server/src/rbac/rbac.seed.spec.ts +++ b/apps/server/src/rbac/rbac.seed.spec.ts @@ -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), diff --git a/apps/server/src/rbac/rbac.service.ts b/apps/server/src/rbac/rbac.service.ts index 88ace4a..638f301 100644 --- a/apps/server/src/rbac/rbac.service.ts +++ b/apps/server/src/rbac/rbac.service.ts @@ -296,6 +296,13 @@ export class RbacService { } async seedData(): Promise { + 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 || [], diff --git a/apps/server/src/students/students.service.ts b/apps/server/src/students/students.service.ts index 26a2279..30c1ed5 100644 --- a/apps/server/src/students/students.service.ts +++ b/apps/server/src/students/students.service.ts @@ -140,7 +140,7 @@ export class StudentsService { : []; const teacherMap = new Map(); 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, From a272403f27254be7b6b619eaf79938161a125aaf Mon Sep 17 00:00:00 2001 From: xiong Date: Fri, 24 Jul 2026 10:05:36 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=8F=AD=E7=BA=A7?= =?UTF-8?q?=E5=BD=92=E6=A1=A3=E6=9F=A5=E8=AF=A2=E5=8F=82=E6=95=B0=E8=BD=AC?= =?UTF-8?q?=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/classes/classes.controller.spec.ts | 20 +++++++++++++++++++ apps/server/src/classes/classes.controller.ts | 3 +++ apps/server/src/classes/dto/class.dto.ts | 2 ++ 3 files changed, 25 insertions(+) diff --git a/apps/server/src/classes/classes.controller.spec.ts b/apps/server/src/classes/classes.controller.spec.ts index 1c8d7e4..102a40d 100644 --- a/apps/server/src/classes/classes.controller.spec.ts +++ b/apps/server/src/classes/classes.controller.spec.ts @@ -1,5 +1,7 @@ +import { ValidationPipe } from '@nestjs/common'; import { ClassesController } from './classes.controller'; import { ClassesService } from './classes.service'; +import { QueryClassDto } from './dto/class.dto'; import { OperationLogsService } from '../operation-logs/operation-logs.service'; import { NotificationsService } from '../notifications/notifications.service'; import { CaslAction, SubjectName } from '../authorization'; @@ -94,3 +96,21 @@ describe('ClassesController - class data scope', () => { expect(service.assertClassAccess).toHaveBeenCalledWith(21, 8, false); }); }); + +describe('QueryClassDto - query transformation', () => { + const pipe = new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }); + + it.each([ + ['false', false], + ['0', false], + ['true', true], + ['1', true], + ])('transforms isArchived=%s to %s', async (input, expected) => { + await expect( + pipe.transform( + { isArchived: input }, + { type: 'query', metatype: QueryClassDto, data: undefined }, + ), + ).resolves.toEqual({ isArchived: expected }); + }); +}); diff --git a/apps/server/src/classes/classes.controller.ts b/apps/server/src/classes/classes.controller.ts index 2dc8dca..547528b 100644 --- a/apps/server/src/classes/classes.controller.ts +++ b/apps/server/src/classes/classes.controller.ts @@ -8,6 +8,8 @@ import { Param, Query, UseGuards, + UsePipes, + ValidationPipe, Request, Res, } from '@nestjs/common'; @@ -45,6 +47,7 @@ const teacherRoleLabels: Record = { }; @UseGuards(JwtAuthGuard) +@UsePipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true })) @Controller('classes') export class ClassesController { constructor( diff --git a/apps/server/src/classes/dto/class.dto.ts b/apps/server/src/classes/dto/class.dto.ts index 9df9088..3d1e837 100644 --- a/apps/server/src/classes/dto/class.dto.ts +++ b/apps/server/src/classes/dto/class.dto.ts @@ -9,6 +9,7 @@ import { ArrayNotEmpty, ValidateNested, Min, + IsBoolean, } from 'class-validator'; import { Type, Transform } from 'class-transformer'; import { ClassType, ClassStatus, TeacherRoleType } from '../../entities'; @@ -160,6 +161,7 @@ export class QueryClassDto { if (value === 'false' || value === '0') return false; return value; }) + @IsBoolean() isArchived?: boolean; }