fix(server): 路由 id 使用 ParseIntPipe,限制分页与批量数组上限
This commit is contained in:
@@ -131,8 +131,8 @@ export class AttendanceQueryService {
|
|||||||
},
|
},
|
||||||
accessibleClassIds?: number[],
|
accessibleClassIds?: number[],
|
||||||
) {
|
) {
|
||||||
const page = query.page || 1;
|
const page = Math.max(1, Math.floor(Number(query.page) || 1));
|
||||||
const pageSize = query.pageSize || 20;
|
const pageSize = Math.min(200, Math.max(1, Math.floor(Number(query.pageSize) || 20)));
|
||||||
|
|
||||||
const qb = this.attendanceRepo.createQueryBuilder('ar');
|
const qb = this.attendanceRepo.createQueryBuilder('ar');
|
||||||
|
|
||||||
@@ -233,8 +233,8 @@ export class AttendanceQueryService {
|
|||||||
|
|
||||||
// ── DingAttendance raw records ──
|
// ── DingAttendance raw records ──
|
||||||
async getDingRaw(query: QueryDingRawDto, accessibleClassIds?: number[]) {
|
async getDingRaw(query: QueryDingRawDto, accessibleClassIds?: number[]) {
|
||||||
const page = query.page || 1;
|
const page = Math.max(1, Math.floor(Number(query.page) || 1));
|
||||||
const pageSize = query.pageSize || 20;
|
const pageSize = Math.min(200, Math.max(1, Math.floor(Number(query.pageSize) || 20)));
|
||||||
const qb = this.dingRawRepo.createQueryBuilder('ar');
|
const qb = this.dingRawRepo.createQueryBuilder('ar');
|
||||||
|
|
||||||
qb.leftJoinAndSelect('ar.matchedStudent', 'matchedStudent');
|
qb.leftJoinAndSelect('ar.matchedStudent', 'matchedStudent');
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Delete,
|
Delete,
|
||||||
Body,
|
Body,
|
||||||
Param,
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
UsePipes,
|
UsePipes,
|
||||||
@@ -85,7 +86,7 @@ export class ClassesController {
|
|||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@RequirePermission('class:view')
|
@RequirePermission('class:view')
|
||||||
async findOne(@Param('id') id: string, @Request() req: AuthenticatedRequest) {
|
async findOne(@Param('id', ParseIntPipe) id: number, @Request() req: AuthenticatedRequest) {
|
||||||
await this.assertReadAccess(req, +id);
|
await this.assertReadAccess(req, +id);
|
||||||
return this.service.findOne(+id);
|
return this.service.findOne(+id);
|
||||||
}
|
}
|
||||||
@@ -93,7 +94,7 @@ export class ClassesController {
|
|||||||
@Get(':id/schedule')
|
@Get(':id/schedule')
|
||||||
@RequirePermission('class:view')
|
@RequirePermission('class:view')
|
||||||
async getSchedule(
|
async getSchedule(
|
||||||
@Param('id') id: string,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Query() query: QueryClassScheduleDto,
|
@Query() query: QueryClassScheduleDto,
|
||||||
@Request() req: AuthenticatedRequest,
|
@Request() req: AuthenticatedRequest,
|
||||||
) {
|
) {
|
||||||
@@ -104,7 +105,7 @@ export class ClassesController {
|
|||||||
@Get(':id/attendance-summary')
|
@Get(':id/attendance-summary')
|
||||||
@RequirePermission('class:view')
|
@RequirePermission('class:view')
|
||||||
async getAttendanceSummary(
|
async getAttendanceSummary(
|
||||||
@Param('id') id: string,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Query() query: QueryClassAttendanceSummaryDto,
|
@Query() query: QueryClassAttendanceSummaryDto,
|
||||||
@Request() req: AuthenticatedRequest,
|
@Request() req: AuthenticatedRequest,
|
||||||
) {
|
) {
|
||||||
@@ -125,27 +126,27 @@ export class ClassesController {
|
|||||||
/** 批量导入学生到班级(通过钉钉用户ID) */
|
/** 批量导入学生到班级(通过钉钉用户ID) */
|
||||||
@Post(':id/students/import')
|
@Post(':id/students/import')
|
||||||
@RequirePermission('class:edit')
|
@RequirePermission('class:edit')
|
||||||
async batchImportStudents(@Param('id') id: string, @Body() dto: BatchImportStudentsDto) {
|
async batchImportStudents(@Param('id', ParseIntPipe) id: number, @Body() dto: BatchImportStudentsDto) {
|
||||||
return this.service.batchImportStudents(+id, dto.users);
|
return this.service.batchImportStudents(+id, dto.users);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 归档班级 */
|
/** 归档班级 */
|
||||||
@Put(':id/archive')
|
@Put(':id/archive')
|
||||||
@RequirePermission('class:edit')
|
@RequirePermission('class:edit')
|
||||||
async archive(@Param('id') id: string) {
|
async archive(@Param('id', ParseIntPipe) id: number) {
|
||||||
return this.service.archive(+id);
|
return this.service.archive(+id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 取消归档 */
|
/** 取消归档 */
|
||||||
@Put(':id/restore')
|
@Put(':id/restore')
|
||||||
@RequirePermission('class:edit')
|
@RequirePermission('class:edit')
|
||||||
async restore(@Param('id') id: string) {
|
async restore(@Param('id', ParseIntPipe) id: number) {
|
||||||
return this.service.restore(+id);
|
return this.service.restore(+id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
@RequirePermission('class:edit')
|
@RequirePermission('class:edit')
|
||||||
async update(@Param('id') id: string, @Body() dto: UpdateClassDto, @Request() req: any) {
|
async update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateClassDto, @Request() req: any) {
|
||||||
const result = await this.service.update(+id, dto);
|
const result = await this.service.update(+id, dto);
|
||||||
await logAudit(this.logService, req, {
|
await logAudit(this.logService, req, {
|
||||||
module: '班级管理', action: '编辑班级', targetId: +id, targetType: 'class', detail: JSON.stringify(dto),
|
module: '班级管理', action: '编辑班级', targetId: +id, targetType: 'class', detail: JSON.stringify(dto),
|
||||||
@@ -155,7 +156,7 @@ export class ClassesController {
|
|||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@RequirePermission('class:delete')
|
@RequirePermission('class:delete')
|
||||||
async remove(@Param('id') id: string, @Request() req: any) {
|
async remove(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
|
||||||
const result = await this.service.remove(+id);
|
const result = await this.service.remove(+id);
|
||||||
await logAudit(this.logService, req, {
|
await logAudit(this.logService, req, {
|
||||||
module: '班级管理', action: '归档班级', targetId: +id, targetType: 'class',
|
module: '班级管理', action: '归档班级', targetId: +id, targetType: 'class',
|
||||||
@@ -165,7 +166,7 @@ export class ClassesController {
|
|||||||
|
|
||||||
@Delete(':id/permanent')
|
@Delete(':id/permanent')
|
||||||
@RequirePermission('class:purge')
|
@RequirePermission('class:purge')
|
||||||
async purge(@Param('id') id: string, @Request() req: any) {
|
async purge(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
|
||||||
const result = await this.service.purge(+id);
|
const result = await this.service.purge(+id);
|
||||||
await logAudit(this.logService, req, {
|
await logAudit(this.logService, req, {
|
||||||
module: '班级管理', action: '永久删除班级', targetId: +id, targetType: 'class', detail: '物理删除,不可恢复',
|
module: '班级管理', action: '永久删除班级', targetId: +id, targetType: 'class', detail: '物理删除,不可恢复',
|
||||||
@@ -176,7 +177,7 @@ export class ClassesController {
|
|||||||
@Get(':id/roster/export')
|
@Get(':id/roster/export')
|
||||||
@RequirePermission('class:view')
|
@RequirePermission('class:view')
|
||||||
async exportRoster(
|
async exportRoster(
|
||||||
@Param('id') id: string,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
@Request() req: AuthenticatedRequest,
|
@Request() req: AuthenticatedRequest,
|
||||||
) {
|
) {
|
||||||
@@ -218,14 +219,14 @@ export class ClassesController {
|
|||||||
|
|
||||||
@Get(':id/students')
|
@Get(':id/students')
|
||||||
@RequirePermission('class:view')
|
@RequirePermission('class:view')
|
||||||
async getStudents(@Param('id') id: string, @Request() req: AuthenticatedRequest) {
|
async getStudents(@Param('id', ParseIntPipe) id: number, @Request() req: AuthenticatedRequest) {
|
||||||
await this.assertReadAccess(req, +id);
|
await this.assertReadAccess(req, +id);
|
||||||
return this.service.getStudents(+id);
|
return this.service.getStudents(+id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/students')
|
@Post(':id/students')
|
||||||
@RequirePermission('class:edit')
|
@RequirePermission('class:edit')
|
||||||
async addStudents(@Param('id') id: string, @Body() dto: AddStudentsDto, @Request() req: any) {
|
async addStudents(@Param('id', ParseIntPipe) id: number, @Body() dto: AddStudentsDto, @Request() req: any) {
|
||||||
const result = await this.service.addStudents(+id, dto.studentIds);
|
const result = await this.service.addStudents(+id, dto.studentIds);
|
||||||
await logAudit(this.logService, req, {
|
await logAudit(this.logService, req, {
|
||||||
module: '班级管理', action: '添加学生', targetId: +id, targetType: 'class', detail: `新增${result.added}名学生`,
|
module: '班级管理', action: '添加学生', targetId: +id, targetType: 'class', detail: `新增${result.added}名学生`,
|
||||||
@@ -249,8 +250,8 @@ export class ClassesController {
|
|||||||
@Delete(':id/students/:studentId')
|
@Delete(':id/students/:studentId')
|
||||||
@RequirePermission('class:edit')
|
@RequirePermission('class:edit')
|
||||||
async removeStudent(
|
async removeStudent(
|
||||||
@Param('id') id: string,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Param('studentId') studentId: string,
|
@Param('studentId', ParseIntPipe) studentId: number,
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
) {
|
) {
|
||||||
const result = await this.service.removeStudent(+id, +studentId);
|
const result = await this.service.removeStudent(+id, +studentId);
|
||||||
@@ -262,14 +263,14 @@ export class ClassesController {
|
|||||||
|
|
||||||
@Get(':id/teachers')
|
@Get(':id/teachers')
|
||||||
@RequirePermission('class:view')
|
@RequirePermission('class:view')
|
||||||
async getTeachers(@Param('id') id: string, @Request() req: AuthenticatedRequest) {
|
async getTeachers(@Param('id', ParseIntPipe) id: number, @Request() req: AuthenticatedRequest) {
|
||||||
await this.assertReadAccess(req, +id);
|
await this.assertReadAccess(req, +id);
|
||||||
return this.service.getTeachers(+id);
|
return this.service.getTeachers(+id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/teachers')
|
@Post(':id/teachers')
|
||||||
@RequirePermission('class:edit')
|
@RequirePermission('class:edit')
|
||||||
async addTeacher(@Param('id') id: string, @Body() dto: AddTeacherDto, @Request() req: any) {
|
async addTeacher(@Param('id', ParseIntPipe) id: number, @Body() dto: AddTeacherDto, @Request() req: any) {
|
||||||
const result = await this.service.addTeacher(+id, dto);
|
const result = await this.service.addTeacher(+id, dto);
|
||||||
await logAudit(this.logService, req, {
|
await logAudit(this.logService, req, {
|
||||||
module: '班级管理', action: '添加教师', targetId: +id, targetType: 'class', detail: `教师${dto.userId} 角色${dto.roleType}`,
|
module: '班级管理', action: '添加教师', targetId: +id, targetType: 'class', detail: `教师${dto.userId} 角色${dto.roleType}`,
|
||||||
@@ -290,8 +291,8 @@ export class ClassesController {
|
|||||||
@Delete(':id/teacher-assignments/:assignmentId')
|
@Delete(':id/teacher-assignments/:assignmentId')
|
||||||
@RequirePermission('class:edit')
|
@RequirePermission('class:edit')
|
||||||
async removeTeacherAssignment(
|
async removeTeacherAssignment(
|
||||||
@Param('id') id: string,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Param('assignmentId') assignmentId: string,
|
@Param('assignmentId', ParseIntPipe) assignmentId: number,
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
) {
|
) {
|
||||||
const result = await this.service.removeTeacherAssignment(+id, +assignmentId);
|
const result = await this.service.removeTeacherAssignment(+id, +assignmentId);
|
||||||
@@ -304,8 +305,8 @@ export class ClassesController {
|
|||||||
@Delete(':id/teachers/:userId')
|
@Delete(':id/teachers/:userId')
|
||||||
@RequirePermission('class:edit')
|
@RequirePermission('class:edit')
|
||||||
async removeTeacher(
|
async removeTeacher(
|
||||||
@Param('id') id: string,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Param('userId') userId: string,
|
@Param('userId', ParseIntPipe) userId: number,
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
) {
|
) {
|
||||||
const result = await this.service.removeTeacher(+id, +userId);
|
const result = await this.service.removeTeacher(+id, +userId);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Delete,
|
Delete,
|
||||||
Body,
|
Body,
|
||||||
Param,
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
Request,
|
Request,
|
||||||
@@ -95,7 +96,7 @@ export class ClassroomRentalsController {
|
|||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@RequirePermission('rental:view')
|
@RequirePermission('rental:view')
|
||||||
findOne(@Param('id') id: string) {
|
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||||
return this.service.findOne(+id);
|
return this.service.findOne(+id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +112,7 @@ export class ClassroomRentalsController {
|
|||||||
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
@RequirePermission('rental:edit')
|
@RequirePermission('rental:edit')
|
||||||
async update(@Param('id') id: string, @Body() dto: UpdateRentalDto, @Request() req: any) {
|
async update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateRentalDto, @Request() req: any) {
|
||||||
const result = await this.service.update(+id, dto);
|
const result = await this.service.update(+id, dto);
|
||||||
await logAudit(this.logService, req, {
|
await logAudit(this.logService, req, {
|
||||||
module: '教室租赁', action: '编辑租赁', targetId: +id, targetType: 'classroom-rental', detail: JSON.stringify(dto),
|
module: '教室租赁', action: '编辑租赁', targetId: +id, targetType: 'classroom-rental', detail: JSON.stringify(dto),
|
||||||
@@ -121,7 +122,7 @@ export class ClassroomRentalsController {
|
|||||||
|
|
||||||
@Put(':id/cancel')
|
@Put(':id/cancel')
|
||||||
@RequirePermission('rental:edit')
|
@RequirePermission('rental:edit')
|
||||||
async cancel(@Param('id') id: string, @Request() req: any) {
|
async cancel(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
|
||||||
const result = await this.service.cancel(+id);
|
const result = await this.service.cancel(+id);
|
||||||
await logAudit(this.logService, req, {
|
await logAudit(this.logService, req, {
|
||||||
module: '教室租赁', action: '取消租赁', targetId: +id, targetType: 'classroom-rental',
|
module: '教室租赁', action: '取消租赁', targetId: +id, targetType: 'classroom-rental',
|
||||||
@@ -131,7 +132,7 @@ export class ClassroomRentalsController {
|
|||||||
|
|
||||||
@Put(':id/end')
|
@Put(':id/end')
|
||||||
@RequirePermission('rental:edit')
|
@RequirePermission('rental:edit')
|
||||||
async end(@Param('id') id: string, @Request() req: any) {
|
async end(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
|
||||||
const result = await this.service.end(+id);
|
const result = await this.service.end(+id);
|
||||||
await logAudit(this.logService, req, {
|
await logAudit(this.logService, req, {
|
||||||
module: '教室租赁', action: '结束租赁', targetId: +id, targetType: 'classroom-rental',
|
module: '教室租赁', action: '结束租赁', targetId: +id, targetType: 'classroom-rental',
|
||||||
@@ -141,7 +142,7 @@ export class ClassroomRentalsController {
|
|||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@RequirePermission('rental:delete')
|
@RequirePermission('rental:delete')
|
||||||
async remove(@Param('id') id: string, @Request() req: any) {
|
async remove(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
|
||||||
const result = await this.service.remove(+id);
|
const result = await this.service.remove(+id);
|
||||||
await logAudit(this.logService, req, {
|
await logAudit(this.logService, req, {
|
||||||
module: '教室租赁', action: '归档租赁', targetId: +id, targetType: 'classroom-rental',
|
module: '教室租赁', action: '归档租赁', targetId: +id, targetType: 'classroom-rental',
|
||||||
@@ -151,7 +152,7 @@ export class ClassroomRentalsController {
|
|||||||
|
|
||||||
@Delete(':id/permanent')
|
@Delete(':id/permanent')
|
||||||
@RequirePermission('rental:purge')
|
@RequirePermission('rental:purge')
|
||||||
async purge(@Param('id') id: string, @Request() req: any) {
|
async purge(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
|
||||||
const result = await this.service.purge(+id);
|
const result = await this.service.purge(+id);
|
||||||
await logAudit(this.logService, req, {
|
await logAudit(this.logService, req, {
|
||||||
module: '教室租赁', action: '永久删除租赁订单', targetId: +id, targetType: 'classroom-rental', detail: '物理删除,不可恢复',
|
module: '教室租赁', action: '永久删除租赁订单', targetId: +id, targetType: 'classroom-rental', detail: '物理删除,不可恢复',
|
||||||
@@ -174,7 +175,7 @@ export class ClassroomRentalsController {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
async uploadContract(
|
async uploadContract(
|
||||||
@Param('id') id: string,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@UploadedFile() file: Express.Multer.File,
|
@UploadedFile() file: Express.Multer.File,
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
) {
|
) {
|
||||||
@@ -188,7 +189,7 @@ export class ClassroomRentalsController {
|
|||||||
|
|
||||||
@Get(':id/contract')
|
@Get(':id/contract')
|
||||||
@RequirePermission('rental:view')
|
@RequirePermission('rental:view')
|
||||||
async downloadContract(@Param('id') id: string, @Res() res: Response) {
|
async downloadContract(@Param('id', ParseIntPipe) id: number, @Res() res: Response) {
|
||||||
const { fullPath, originalName } = await this.service.getContractPath(+id);
|
const { fullPath, originalName } = await this.service.getContractPath(+id);
|
||||||
res.setHeader('Content-Type', 'application/pdf');
|
res.setHeader('Content-Type', 'application/pdf');
|
||||||
res.setHeader(
|
res.setHeader(
|
||||||
@@ -201,7 +202,7 @@ export class ClassroomRentalsController {
|
|||||||
|
|
||||||
@Delete(':id/contract')
|
@Delete(':id/contract')
|
||||||
@RequirePermission('rental:edit')
|
@RequirePermission('rental:edit')
|
||||||
async deleteContract(@Param('id') id: string, @Request() req: any) {
|
async deleteContract(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
|
||||||
const result = await this.service.removeContract(+id);
|
const result = await this.service.removeContract(+id);
|
||||||
await logAudit(this.logService, req, {
|
await logAudit(this.logService, req, {
|
||||||
module: '教室租赁', action: '移除合同', targetId: +id, targetType: 'classroom-rental',
|
module: '教室租赁', action: '移除合同', targetId: +id, targetType: 'classroom-rental',
|
||||||
|
|||||||
@@ -17,4 +17,11 @@ describe('BatchIdsDto', () => {
|
|||||||
const dto = Object.assign(new BatchIdsDto(), { ids: [1, 1, 2] });
|
const dto = Object.assign(new BatchIdsDto(), { ids: [1, 1, 2] });
|
||||||
await expect(validate(dto)).resolves.toHaveLength(0);
|
await expect(validate(dto)).resolves.toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects batches with more than 500 ids', async () => {
|
||||||
|
const dto = Object.assign(new BatchIdsDto(), {
|
||||||
|
ids: Array.from({ length: 501 }, (_, i) => i + 1),
|
||||||
|
});
|
||||||
|
await expect(validate(dto)).resolves.not.toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { ArrayNotEmpty, IsArray, IsInt, Min } from 'class-validator';
|
import { ArrayMaxSize, ArrayNotEmpty, IsArray, IsInt, Min } from 'class-validator';
|
||||||
|
|
||||||
export class BatchIdsDto {
|
export class BatchIdsDto {
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ArrayNotEmpty()
|
@ArrayNotEmpty()
|
||||||
|
@ArrayMaxSize(500)
|
||||||
@IsInt({ each: true })
|
@IsInt({ each: true })
|
||||||
@Min(1, { each: true })
|
@Min(1, { each: true })
|
||||||
ids: number[];
|
ids: number[];
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards, Request } from '@nestjs/common';
|
import { Controller, Get, Post, Put, Delete, Body, Param, ParseIntPipe, UseGuards, Request } from '@nestjs/common';
|
||||||
import { ExpenseTypesService } from './expense-types.service';
|
import { ExpenseTypesService } from './expense-types.service';
|
||||||
import { CreateExpenseTypeDto, UpdateExpenseTypeDto } from './dto/expense-type.dto';
|
import { CreateExpenseTypeDto, UpdateExpenseTypeDto } from './dto/expense-type.dto';
|
||||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||||
@@ -47,7 +47,7 @@ export class ExpenseTypesController {
|
|||||||
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
@RequirePermission('expense:edit')
|
@RequirePermission('expense:edit')
|
||||||
async update(@Param('id') id: string, @Body() dto: UpdateExpenseTypeDto, @Request() req: any) {
|
async update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateExpenseTypeDto, @Request() req: any) {
|
||||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||||
const result = await this.service.update(+id, dto);
|
const result = await this.service.update(+id, dto);
|
||||||
await this.logService.log({
|
await this.logService.log({
|
||||||
@@ -66,7 +66,7 @@ export class ExpenseTypesController {
|
|||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@RequirePermission('expense:delete')
|
@RequirePermission('expense:delete')
|
||||||
async remove(@Param('id') id: string, @Request() req: any) {
|
async remove(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
|
||||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||||
await this.service.remove(+id);
|
await this.service.remove(+id);
|
||||||
await this.logService.log({
|
await this.logService.log({
|
||||||
|
|||||||
@@ -28,4 +28,17 @@ describe('BatchRoomExpenseDto boundaries', () => {
|
|||||||
});
|
});
|
||||||
await expect(validate(dto)).resolves.toHaveLength(0);
|
await expect(validate(dto)).resolves.toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects a batch with more than 500 items', async () => {
|
||||||
|
const dto = plainToInstance(BatchRoomExpenseDto, {
|
||||||
|
periodStart: '2026-07-01',
|
||||||
|
periodEnd: '2026-07-31',
|
||||||
|
expenses: Array.from({ length: 501 }, () => ({
|
||||||
|
roomId: 1,
|
||||||
|
expenseType: 'water',
|
||||||
|
amount: 10,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
await expect(validate(dto)).resolves.not.toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ArrayNotEmpty, IsArray, IsDateString, IsIn, IsInt, IsISO8601, IsString, IsNumber, IsOptional, Matches, Min, ValidateNested } from 'class-validator';
|
import { ArrayMaxSize, ArrayNotEmpty, IsArray, IsDateString, IsIn, IsInt, IsISO8601, IsString, IsNumber, IsOptional, Matches, Min, ValidateNested } from 'class-validator';
|
||||||
import { PartialType } from '@nestjs/mapped-types';
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
@@ -117,6 +117,7 @@ export class BatchRoomExpenseDto {
|
|||||||
|
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ArrayNotEmpty()
|
@ArrayNotEmpty()
|
||||||
|
@ArrayMaxSize(500)
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => BatchRoomExpenseItemDto)
|
@Type(() => BatchRoomExpenseItemDto)
|
||||||
expenses: BatchRoomExpenseItemDto[];
|
expenses: BatchRoomExpenseItemDto[];
|
||||||
|
|||||||
@@ -32,4 +32,13 @@ describe('notification DTO boundaries', () => {
|
|||||||
expect(await validate(dto)).not.toEqual([]);
|
expect(await validate(dto)).not.toEqual([]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects more than 500 recipients', async () => {
|
||||||
|
const dto = plainToInstance(CreateNotificationDto, {
|
||||||
|
recipientIds: Array.from({ length: 501 }, (_, i) => i + 1),
|
||||||
|
type: 'test',
|
||||||
|
title: '标题',
|
||||||
|
});
|
||||||
|
await expect(validate(dto)).resolves.not.toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ArrayNotEmpty,
|
ArrayNotEmpty,
|
||||||
|
ArrayMaxSize,
|
||||||
IsString,
|
IsString,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
@@ -13,6 +14,7 @@ import { Type } from 'class-transformer';
|
|||||||
export class CreateNotificationDto {
|
export class CreateNotificationDto {
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ArrayNotEmpty()
|
@ArrayNotEmpty()
|
||||||
|
@ArrayMaxSize(500)
|
||||||
@IsInt({ each: true })
|
@IsInt({ each: true })
|
||||||
recipientIds: number[];
|
recipientIds: number[];
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Get,
|
Get,
|
||||||
Put,
|
Put,
|
||||||
Param,
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
Query,
|
Query,
|
||||||
Req,
|
Req,
|
||||||
Sse,
|
Sse,
|
||||||
@@ -74,7 +75,7 @@ export class NotificationsController {
|
|||||||
|
|
||||||
@Put(':id/read')
|
@Put(':id/read')
|
||||||
async markRead(
|
async markRead(
|
||||||
@Param('id') id: string,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Req() req: AuthenticatedRequest,
|
@Req() req: AuthenticatedRequest,
|
||||||
) {
|
) {
|
||||||
await this.service.markRead(+id, req.user.id);
|
await this.service.markRead(+id, req.user.id);
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ export class OperationLogsService {
|
|||||||
if (query?.endDate)
|
if (query?.endDate)
|
||||||
qb.andWhere('log.createdAt <= :endDate', { endDate: query.endDate + ' 23:59:59' });
|
qb.andWhere('log.createdAt <= :endDate', { endDate: query.endDate + ' 23:59:59' });
|
||||||
|
|
||||||
const page = query?.page || 1;
|
const page = Math.max(1, Math.floor(Number(query?.page) || 1));
|
||||||
const pageSize = query?.pageSize || 50;
|
const pageSize = Math.min(200, Math.max(1, Math.floor(Number(query?.pageSize) || 50)));
|
||||||
const [data, total] = await qb
|
const [data, total] = await qb
|
||||||
.skip((page - 1) * pageSize)
|
.skip((page - 1) * pageSize)
|
||||||
.take(pageSize)
|
.take(pageSize)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Delete,
|
Delete,
|
||||||
Body,
|
Body,
|
||||||
Param,
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
Request,
|
Request,
|
||||||
@@ -42,7 +43,7 @@ export class OrganizationsController {
|
|||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@RequirePermission('organization:view')
|
@RequirePermission('organization:view')
|
||||||
findOne(@Param('id') id: string) {
|
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||||
return this.service.findOne(+id);
|
return this.service.findOne(+id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ export class OrganizationsController {
|
|||||||
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
@RequirePermission('organization:edit')
|
@RequirePermission('organization:edit')
|
||||||
async update(@Param('id') id: string, @Body() dto: UpdateOrganizationDto, @Request() req: any) {
|
async update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateOrganizationDto, @Request() req: any) {
|
||||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||||
const result = await this.service.update(+id, dto);
|
const result = await this.service.update(+id, dto);
|
||||||
await this.logService.log({
|
await this.logService.log({
|
||||||
@@ -86,7 +87,7 @@ export class OrganizationsController {
|
|||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@RequirePermission('organization:delete')
|
@RequirePermission('organization:delete')
|
||||||
async remove(@Param('id') id: string, @Request() req: any) {
|
async remove(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
|
||||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||||
const result = await this.service.remove(+id);
|
const result = await this.service.remove(+id);
|
||||||
await this.logService.log({
|
await this.logService.log({
|
||||||
@@ -104,7 +105,7 @@ export class OrganizationsController {
|
|||||||
|
|
||||||
@Delete(':id/permanent')
|
@Delete(':id/permanent')
|
||||||
@RequirePermission('organization:purge')
|
@RequirePermission('organization:purge')
|
||||||
async purge(@Param('id') id: string, @Request() req: any) {
|
async purge(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
|
||||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||||
const result = await this.service.purge(+id);
|
const result = await this.service.purge(+id);
|
||||||
await this.logService.log({
|
await this.logService.log({
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
Request,
|
Request,
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
|
ParseIntPipe,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { RbacService } from './rbac.service';
|
import { RbacService } from './rbac.service';
|
||||||
import {
|
import {
|
||||||
@@ -41,7 +42,7 @@ export class RbacController {
|
|||||||
|
|
||||||
@Get('roles/:id')
|
@Get('roles/:id')
|
||||||
@RequirePermission('role:view')
|
@RequirePermission('role:view')
|
||||||
findRoleById(@Param('id') id: string) {
|
findRoleById(@Param('id', ParseIntPipe) id: number) {
|
||||||
return this.rbacService.findRoleById(+id);
|
return this.rbacService.findRoleById(+id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ export class RbacController {
|
|||||||
|
|
||||||
@Put('roles/:id')
|
@Put('roles/:id')
|
||||||
@RequirePermission('role:edit')
|
@RequirePermission('role:edit')
|
||||||
async updateRole(@Param('id') id: string, @Body() dto: UpdateRoleDto, @Request() req: any) {
|
async updateRole(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateRoleDto, @Request() req: any) {
|
||||||
try {
|
try {
|
||||||
const result = await this.rbacService.updateRole(+id, dto);
|
const result = await this.rbacService.updateRole(+id, dto);
|
||||||
await logAudit(this.logService, req, {
|
await logAudit(this.logService, req, {
|
||||||
@@ -71,7 +72,7 @@ export class RbacController {
|
|||||||
|
|
||||||
@Delete('roles/:id')
|
@Delete('roles/:id')
|
||||||
@RequirePermission('role:delete')
|
@RequirePermission('role:delete')
|
||||||
async deleteRole(@Param('id') id: string, @Request() req: any) {
|
async deleteRole(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
|
||||||
try {
|
try {
|
||||||
const result = await this.rbacService.deleteRole(+id);
|
const result = await this.rbacService.deleteRole(+id);
|
||||||
await logAudit(this.logService, req, {
|
await logAudit(this.logService, req, {
|
||||||
@@ -118,7 +119,7 @@ export class RbacController {
|
|||||||
|
|
||||||
@Put('users/:id')
|
@Put('users/:id')
|
||||||
@RequirePermission('user:edit')
|
@RequirePermission('user:edit')
|
||||||
async updateUser(@Param('id') id: string, @Body() dto: UpdateUserDto, @Request() req: any) {
|
async updateUser(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateUserDto, @Request() req: any) {
|
||||||
try {
|
try {
|
||||||
const result = await this.rbacService.updateUser(+id, dto);
|
const result = await this.rbacService.updateUser(+id, dto);
|
||||||
await logAudit(this.logService, req, {
|
await logAudit(this.logService, req, {
|
||||||
@@ -132,7 +133,7 @@ export class RbacController {
|
|||||||
|
|
||||||
@Put('users/:id/password')
|
@Put('users/:id/password')
|
||||||
@RequirePermission('user:reset-password')
|
@RequirePermission('user:reset-password')
|
||||||
async resetPassword(@Param('id') id: string, @Body() dto: ResetPasswordDto, @Request() req: any) {
|
async resetPassword(@Param('id', ParseIntPipe) id: number, @Body() dto: ResetPasswordDto, @Request() req: any) {
|
||||||
try {
|
try {
|
||||||
const result = await this.rbacService.resetPassword(+id, dto.password);
|
const result = await this.rbacService.resetPassword(+id, dto.password);
|
||||||
await logAudit(this.logService, req, {
|
await logAudit(this.logService, req, {
|
||||||
@@ -146,7 +147,7 @@ export class RbacController {
|
|||||||
|
|
||||||
@Put('users/:id/archive')
|
@Put('users/:id/archive')
|
||||||
@RequirePermission('user:edit')
|
@RequirePermission('user:edit')
|
||||||
async archiveUser(@Param('id') id: string) {
|
async archiveUser(@Param('id', ParseIntPipe) id: number) {
|
||||||
try {
|
try {
|
||||||
return await this.rbacService.archiveUser(+id);
|
return await this.rbacService.archiveUser(+id);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
@@ -157,7 +158,7 @@ export class RbacController {
|
|||||||
|
|
||||||
@Put('users/:id/restore')
|
@Put('users/:id/restore')
|
||||||
@RequirePermission('user:edit')
|
@RequirePermission('user:edit')
|
||||||
async restoreUser(@Param('id') id: string) {
|
async restoreUser(@Param('id', ParseIntPipe) id: number) {
|
||||||
try {
|
try {
|
||||||
return await this.rbacService.restoreUser(+id);
|
return await this.rbacService.restoreUser(+id);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
@@ -168,7 +169,7 @@ export class RbacController {
|
|||||||
|
|
||||||
@Delete('users/:id/permanent')
|
@Delete('users/:id/permanent')
|
||||||
@RequirePermission('user:purge')
|
@RequirePermission('user:purge')
|
||||||
async purgeUser(@Param('id') id: string, @Request() req: any) {
|
async purgeUser(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
|
||||||
try {
|
try {
|
||||||
const result = await this.rbacService.purgeUser(+id, req.user?.id);
|
const result = await this.rbacService.purgeUser(+id, req.user?.id);
|
||||||
await logAudit(this.logService, req, {
|
await logAudit(this.logService, req, {
|
||||||
@@ -183,7 +184,7 @@ export class RbacController {
|
|||||||
|
|
||||||
@Put('users/:id/mark-staff')
|
@Put('users/:id/mark-staff')
|
||||||
@RequirePermission('user:edit')
|
@RequirePermission('user:edit')
|
||||||
async markAsStaff(@Param('id') id: string) {
|
async markAsStaff(@Param('id', ParseIntPipe) id: number) {
|
||||||
try {
|
try {
|
||||||
return await this.rbacService.markAsStaff(+id);
|
return await this.rbacService.markAsStaff(+id);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
@@ -194,7 +195,7 @@ export class RbacController {
|
|||||||
|
|
||||||
@Put('users/:id/mark-student')
|
@Put('users/:id/mark-student')
|
||||||
@RequirePermission('user:edit')
|
@RequirePermission('user:edit')
|
||||||
async markAsStudent(@Param('id') id: string) {
|
async markAsStudent(@Param('id', ParseIntPipe) id: number) {
|
||||||
try {
|
try {
|
||||||
return await this.rbacService.markAsStudent(+id);
|
return await this.rbacService.markAsStudent(+id);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
@@ -205,14 +206,14 @@ export class RbacController {
|
|||||||
|
|
||||||
@Get('users/:id/profile')
|
@Get('users/:id/profile')
|
||||||
@RequirePermission('user:view')
|
@RequirePermission('user:view')
|
||||||
getUserProfile(@Param('id') id: string) {
|
getUserProfile(@Param('id', ParseIntPipe) id: number) {
|
||||||
return this.rbacService.getUserProfile(+id);
|
return this.rbacService.getUserProfile(+id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put('users/:id/profile')
|
@Put('users/:id/profile')
|
||||||
@RequirePermission('user:edit')
|
@RequirePermission('user:edit')
|
||||||
async updateUserProfile(
|
async updateUserProfile(
|
||||||
@Param('id') id: string,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Body() dto: UpdateProfileDto,
|
@Body() dto: UpdateProfileDto,
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
) {
|
) {
|
||||||
@@ -250,7 +251,7 @@ export class RbacController {
|
|||||||
@Put('teachers/:id/profile')
|
@Put('teachers/:id/profile')
|
||||||
@RequirePermission('teacher:edit')
|
@RequirePermission('teacher:edit')
|
||||||
async updateTeacherProfile(
|
async updateTeacherProfile(
|
||||||
@Param('id') id: string,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Body() profile: UpdateProfileDto,
|
@Body() profile: UpdateProfileDto,
|
||||||
@Request() req: { user?: { id: number; username: string } },
|
@Request() req: { user?: { id: number; username: string } },
|
||||||
) {
|
) {
|
||||||
|
|||||||
Reference in New Issue
Block a user