Compare commits

..

2 Commits

Author SHA1 Message Date
kevin-asprec
85aea30730 fix: bypass ValidationPipe for multipart comments, auto-create uploads dir, expand client roles
- Use @Body() body: any in comment controller to skip global ValidationPipe
  which was rejecting multipart form bodies with forbidNonWhitelisted
- Auto-create uploads/ directory on startup to prevent ENOENT on file uploads
- Restrict file uploads to images only (remove pdf)
- Allow technician/collector roles to update clients (was manager-only)
2026-05-06 18:20:14 +08:00
kevin-asprec
da9707f3fb fix(comment): Create proper DTO class for comment creation
- Create CreateCommentDto class with proper validation decorators
- Update comment controller to use the DTO for request body
- This resolves the global ValidationPipe rejection due to forbidNonWhitelisted
- Set MaxLength to 50000 to match manual validation logic

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 17:38:48 +08:00
4 changed files with 30 additions and 9 deletions

View File

@@ -54,7 +54,7 @@ export class ClientController {
}
@Patch(':id')
@Roles('manager')
@Roles('manager', 'technician', 'collector')
async update(
@CurrentUser() user: CurrentUserPayload,
@Param('id') id: string,

View File

@@ -7,11 +7,11 @@ import {
UseGuards,
UseInterceptors,
UploadedFiles,
BadRequestException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { FileFieldsInterceptor } from '@nestjs/platform-express';
import { CommentService } from './comment.service';
import { CreateCommentDto } from './dto/create-comment.dto';
import { Roles } from '../common/decorators/roles.decorator';
import { RolesGuard } from '../common/guards/roles.guard';
import { TenantGuard } from '../common/guards/tenant.guard';
@@ -38,14 +38,20 @@ export class CommentController {
async create(
@CurrentUser() user: CurrentUserPayload,
@Param('ticketId') ticketId: string,
@Body() dto: CreateCommentDto,
@Body() body: any,
@UploadedFiles() files?: { files?: Express.Multer.File[] },
) {
let content = body?.content;
if (Array.isArray(content)) content = content[0];
if (typeof content !== 'string') content = String(content ?? '');
if (!content || content.length > 50000) {
throw new BadRequestException('Content must be between 1 and 50000 characters');
}
return this.commentService.create(
user.tenantId,
ticketId,
user.sub,
dto.content,
content,
files?.files,
);
}

View File

@@ -1,7 +1,19 @@
import { IsString, MinLength } from 'class-validator';
import {
IsString,
IsOptional,
IsArray,
MinLength,
MaxLength,
} from 'class-validator';
export class CreateCommentDto {
@IsString()
@MinLength(1)
content!: string;
@MaxLength(50000)
content: string;
@IsOptional()
@IsArray()
@IsString({ each: true })
files?: string[];
}

View File

@@ -1,12 +1,16 @@
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
import { diskStorage } from 'multer';
import { extname } from 'path';
import { extname, resolve } from 'path';
import { existsSync, mkdirSync } from 'fs';
import { Request } from 'express';
const uploadDir = resolve('./uploads');
if (!existsSync(uploadDir)) mkdirSync(uploadDir, { recursive: true });
export const multerOptions: MulterOptions = {
storage: diskStorage({
destination: (_req: Request, _file: Express.Multer.File, cb: (error: Error | null, destination: string) => void) => {
cb(null, './uploads');
cb(null, uploadDir);
},
filename: (_req: Request, file: Express.Multer.File, cb: (error: Error | null, filename: string) => void) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
@@ -22,7 +26,6 @@ export const multerOptions: MulterOptions = {
'image/png',
'image/gif',
'image/webp',
'application/pdf',
];
if (allowed.includes(file.mimetype)) {
cb(null, true);