refactor: strengthen client boundaries and quality gates
This commit is contained in:
@@ -1,8 +1,22 @@
|
||||
import { BadRequestException, Body, Controller, Get, Param, Post, Query, Res, UploadedFile, UseInterceptors } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
Res,
|
||||
UploadedFile,
|
||||
UseInterceptors,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { CurrentSessionUserId } from '../auth/current-session-user.decorator';
|
||||
import { FilesService } from './files.service';
|
||||
import { strictValidationPipe } from '../common/strict-validation.pipe';
|
||||
import { ClientFileUploadDto } from './client-files.dto';
|
||||
|
||||
type UploadedMultipartFile = { originalname: string; mimetype: string; size: number; buffer: Buffer };
|
||||
type DownloadResponse = { setHeader(name: string, value: number | string): void; send(content: Buffer): void };
|
||||
@@ -14,14 +28,14 @@ export class ClientFilesController {
|
||||
|
||||
@Post('upload')
|
||||
@UseInterceptors(FileInterceptor('file', { limits: { fileSize: 10 * 1024 * 1024, files: 1, fields: 4, parts: 5 } }))
|
||||
@UsePipes(strictValidationPipe)
|
||||
upload(
|
||||
@CurrentSessionUserId() userId: string | undefined,
|
||||
@UploadedFile() file: UploadedMultipartFile,
|
||||
@Body('purpose') purpose: string,
|
||||
@Body('prefix') prefix?: string,
|
||||
@Body() body: ClientFileUploadDto,
|
||||
) {
|
||||
if (!file) throw new BadRequestException('Upload file is required');
|
||||
return this.files.uploadForClient(userId, { purpose, prefix }, file);
|
||||
return this.files.uploadForClient(userId, body, file);
|
||||
}
|
||||
|
||||
@Get(':id/download')
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { strictValidationPipe } from '../common/strict-validation.pipe';
|
||||
import { ClientFileUploadDto } from './client-files.dto';
|
||||
|
||||
describe('ClientFileUploadDto', () => {
|
||||
it('accepts bounded client material paths and rejects traversal before storage', async () => {
|
||||
await expect(
|
||||
strictValidationPipe.transform(
|
||||
{ purpose: 'drainage_report_material', prefix: 'drainage-materials/item-1' },
|
||||
{
|
||||
type: 'body',
|
||||
metatype: ClientFileUploadDto,
|
||||
data: undefined,
|
||||
},
|
||||
),
|
||||
).resolves.toEqual(expect.objectContaining({ purpose: 'drainage_report_material' }));
|
||||
await expect(
|
||||
strictValidationPipe.transform(
|
||||
{ purpose: 'enterprise_certification', prefix: '../admin' },
|
||||
{
|
||||
type: 'body',
|
||||
metatype: ClientFileUploadDto,
|
||||
data: undefined,
|
||||
},
|
||||
),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { IsIn, IsOptional, IsString, Matches, MaxLength } from 'class-validator';
|
||||
|
||||
export class ClientFileUploadDto {
|
||||
@IsString()
|
||||
@IsIn(['enterprise_certification', 'signature_report_material', 'drainage_report_material'])
|
||||
purpose!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(256)
|
||||
@Matches(/^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*$/)
|
||||
prefix?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user