diff --git a/backend/src/common/security/file_check.ts b/backend/src/common/security/file_check.ts new file mode 100644 index 00000000..3e89d86d --- /dev/null +++ b/backend/src/common/security/file_check.ts @@ -0,0 +1,57 @@ +import { BadRequestException } from '@nestjs/common'; +import { FileUpload } from 'graphql-upload-minimal'; +import path from 'path'; + +/** Maximum allowed file size (5MB) */ +const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB + +/** Allowed image MIME types */ +const ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp']; + +/** Allowed file extensions */ +const ALLOWED_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.webp']; + +/** + * Validates a file upload (size, type) and returns a Buffer. + * @param file - FileUpload object from GraphQL + * @returns Promise - The file data in buffer format + * @throws BadRequestException - If validation fails + */ +export async function validateAndBufferFile( + file: FileUpload, +): Promise<{ buffer: Buffer; mimetype: string }> { + const { filename, createReadStream, mimetype } = await file; + + // Extract the file extension + const extension = path.extname(filename).toLowerCase(); + + // Validate MIME type + if (!ALLOWED_MIME_TYPES.includes(mimetype)) { + throw new BadRequestException( + `Invalid file type: ${mimetype}. Only JPEG, PNG, and WebP are allowed.`, + ); + } + + // Validate file extension + if (!ALLOWED_EXTENSIONS.includes(extension)) { + throw new BadRequestException( + `Invalid file extension: ${extension}. Only .jpg, .jpeg, .png, and .webp are allowed.`, + ); + } + + const chunks: Buffer[] = []; + let fileSize = 0; + + // Read file stream and check size + for await (const chunk of createReadStream()) { + fileSize += chunk.length; + if (fileSize > MAX_FILE_SIZE) { + throw new BadRequestException( + 'File size exceeds the maximum allowed limit (5MB).', + ); + } + chunks.push(chunk); + } + + return { buffer: Buffer.concat(chunks), mimetype }; +} diff --git a/backend/src/project/project.resolver.ts b/backend/src/project/project.resolver.ts index ea1ccd34..44dbfcaf 100644 --- a/backend/src/project/project.resolver.ts +++ b/backend/src/project/project.resolver.ts @@ -22,6 +22,7 @@ import { ProjectGuard } from '../guard/project.guard'; import { GetUserIdFromToken } from '../decorator/get-auth-token.decorator'; import { Chat } from 'src/chat/chat.model'; import { User } from 'src/user/user.model'; +import { validateAndBufferFile } from 'src/common/security/file_check'; @Resolver(() => Project) export class ProjectsResolver { @@ -98,14 +99,8 @@ export class ProjectsResolver { this.logger.log(`User ${userId} uploading photo for project ${projectId}`); // Extract the file data - const { createReadStream, mimetype } = await file; - - // Buffer the file content - const chunks = []; - for await (const chunk of createReadStream()) { - chunks.push(chunk); - } - const buffer = Buffer.concat(chunks); + // Validate file and convert it to buffer + const { buffer, mimetype } = await validateAndBufferFile(file); // Call the service with the extracted buffer and mimetype return this.projectService.updateProjectPhotoUrl( @@ -115,6 +110,7 @@ export class ProjectsResolver { mimetype, ); } + @Mutation(() => Project) async updateProjectPublicStatus( @GetUserIdFromToken() userId: string,