-
Notifications
You must be signed in to change notification settings - Fork 0
57 image scroping in inscription form image upload error management #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6cb7958
4987c14
aef87ab
5f76118
bc982aa
caf5ea1
9415c2e
0aaec99
8bf7c8a
4afdee0
ece69cb
b1ea979
36cf974
7655afa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,4 +7,7 @@ | |
| # production | ||
| /nextjs/matcha/build | ||
|
|
||
| .github | ||
| .github | ||
|
|
||
| node_modules | ||
| .DS_Store | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,15 @@ import { FastifyRequest, FastifyReply, FastifyRequestUser } from 'fastify'; | |||||||||
| import { AppError, UnauthorizedError, ForbiddenError, NotFoundError, BadRequestError } from '../../../utils/error'; | ||||||||||
| import path from 'path'; | ||||||||||
| import fs from 'fs'; | ||||||||||
| import sharp from 'sharp'; | ||||||||||
| import { imageDimensionsFromData } from 'image-dimensions'; | ||||||||||
|
|
||||||||||
| type Crop = { | ||||||||||
| x: number; | ||||||||||
| y: number; | ||||||||||
| width: number; | ||||||||||
| height: number; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| export const setProfilePictureIndexHandler = async ( | ||||||||||
| request: FastifyRequest, | ||||||||||
|
|
@@ -33,12 +42,30 @@ export const addProfilePictureHandler = async ( | |||||||||
| const user = request.user; | ||||||||||
| const userId: number = (user as any)?.id; | ||||||||||
| if (!userId) | ||||||||||
| throw new UnauthorizedError(); | ||||||||||
| throw new UnauthorizedError(); | ||||||||||
|
|
||||||||||
| const rotation = Number((request.body as { rotation?: { value: string } }).rotation?.value) || 0; | ||||||||||
| const rawCrop = (request.body as { crop?: { value: string } })?.crop?.value; | ||||||||||
| let crop: Crop | undefined; | ||||||||||
| if (typeof rawCrop === 'string') { | ||||||||||
| try { | ||||||||||
| const parsed = JSON.parse(rawCrop); | ||||||||||
| crop = parsed && typeof parsed === 'object' ? parsed : undefined; | ||||||||||
| } catch (e) { | ||||||||||
| throw new BadRequestError('Invalid crop JSON'); | ||||||||||
| } | ||||||||||
| } else if (rawCrop && typeof rawCrop === 'object') { | ||||||||||
| crop = rawCrop as Crop; | ||||||||||
| } | ||||||||||
|
|
||||||||||
|
|
||||||||||
| const file = request.fileMeta; | ||||||||||
| if (!file) | ||||||||||
| throw (new BadRequestError()); | ||||||||||
|
|
||||||||||
| if (!request.fileBuffer) | ||||||||||
| throw (new BadRequestError()); | ||||||||||
|
|
||||||||||
| const picturesDir = path.join(__dirname, '..', '..', '..', '..', 'uploads', userId.toString()); | ||||||||||
| if (!fs.existsSync(picturesDir)) { | ||||||||||
| fs.mkdirSync(picturesDir, { recursive: true }); | ||||||||||
|
|
@@ -48,7 +75,25 @@ export const addProfilePictureHandler = async ( | |||||||||
| const newFilePath = path.join(picturesDir, newFileName); | ||||||||||
| const newFileURL = `https://${process.env.DOMAIN || 'localhost'}/api/private/uploads/${userId}/${newFileName}`; | ||||||||||
| const dest = fs.createWriteStream(newFilePath); | ||||||||||
| dest.write(request.fileBuffer); | ||||||||||
|
|
||||||||||
| const currentSize = await imageDimensionsFromData(request.fileBuffer); | ||||||||||
| if (!currentSize) | ||||||||||
| throw new BadRequestError('Unrecognized file format'); | ||||||||||
|
|
||||||||||
| let newBuffer; | ||||||||||
| try { | ||||||||||
| newBuffer = await sharp(request.fileBuffer).rotate(rotation || 0).extract({ | ||||||||||
| left: Math.round(crop?.x || 0), | ||||||||||
| top: Math.round(crop?.y || 0), | ||||||||||
| width: Math.round(crop?.width || currentSize.width), | ||||||||||
| height: Math.round(crop?.height || currentSize.height) | ||||||||||
| }).toBuffer(); | ||||||||||
| } catch (error) { | ||||||||||
| throw new BadRequestError('Failed to process image with given crop/rotation'); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| console.log('Saving new profile picture to', newFilePath); | ||||||||||
|
||||||||||
| console.log('Saving new profile picture to', newFilePath); | |
| if (process.env.NODE_ENV !== 'production') { | |
| console.log('Saving new profile picture to', newFilePath); | |
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -132,6 +132,7 @@ export default class UserModel { | |||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| update = async (id: number, user: UserProfile, location?: UserLocation) => { | ||||||||||||||
| console.log("Updating user ID:", id, "with data:", user, "and location:", location); | ||||||||||||||
|
||||||||||||||
| console.log("Updating user ID:", id, "with data:", user, "and location:", location); |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Debug console.log statement left in production code. This should be removed or wrapped in a development-only check.
| console.log("Updating user ID:", id, "with data:", user, "and location:", location); | |
| if (process.env.NODE_ENV !== 'production') { | |
| console.debug("Updating user ID:", id, "with data:", user, "and location:", location); | |
| } |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Console.log statement should be removed before merging to production. This debug log on line 135 exposes potentially sensitive user data and should not be in production code.
| console.log("Updating user ID:", id, "with data:", user, "and location:", location); |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,11 +1,12 @@ | ||||||||||||
| import fp from 'fastify-plugin' | ||||||||||||
| import { FastifyRequest, FastifyReply } from 'fastify' | ||||||||||||
| import { imageDimensionsFromData } from 'image-dimensions' | ||||||||||||
| import { MultipartFile } from '@fastify/multipart' | ||||||||||||
|
|
||||||||||||
| export default fp(async function(fastify, opts) { | ||||||||||||
| fastify.decorate("checkImageConformity", async function(request: FastifyRequest, reply: FastifyReply) { | ||||||||||||
| try { | ||||||||||||
| const file = await request.file(); | ||||||||||||
| const { file } = request.body as { file?: MultipartFile }; | ||||||||||||
| if (!file) | ||||||||||||
| throw (new Error('No file uploaded')); | ||||||||||||
|
|
||||||||||||
|
|
@@ -15,34 +16,66 @@ export default fp(async function(fastify, opts) { | |||||||||||
|
|
||||||||||||
| // read once into a Buffer and reuse | ||||||||||||
| const buffer = await file.toBuffer(); | ||||||||||||
| if (!buffer || buffer.length === 0) | ||||||||||||
| throw (new Error('Failed to read uploaded file')); | ||||||||||||
|
|
||||||||||||
| const maxSizeInBytes = 50 * 1024 * 1024; // 50 MB | ||||||||||||
| if (buffer.length > maxSizeInBytes) | ||||||||||||
| throw (new Error('File size exceeds limit')); | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| const ratiox = 9; | ||||||||||||
| const ratioy = 16; | ||||||||||||
| const minWidth = 150; | ||||||||||||
| const maxWidth = 1080; | ||||||||||||
|
|
||||||||||||
| const currentSize = await imageDimensionsFromData(buffer); | ||||||||||||
| if (!currentSize) | ||||||||||||
| throw new Error('Unrecognized file format'); | ||||||||||||
|
|
||||||||||||
| // const ratio = (currentSize.width / currentSize.height).toPrecision(3); | ||||||||||||
| const expectedRatio = (ratiox / ratioy).toPrecision(3); | ||||||||||||
| if (currentSize.width > maxWidth || currentSize.width < minWidth | ||||||||||||
| // || ratio != expectedRatio | ||||||||||||
| ) | ||||||||||||
| throw new Error('Wrong file dimensions'); | ||||||||||||
|
|
||||||||||||
| // attach buffer + meta to the request so later handlers can reuse it | ||||||||||||
| const currentSize = imageDimensionsFromData(buffer); | ||||||||||||
| request.fileBuffer = buffer; | ||||||||||||
| request.fileMeta = { | ||||||||||||
| filename: file.filename, | ||||||||||||
| mimetype: file.mimetype, | ||||||||||||
| fields: file.fields // if you need multipart fields | ||||||||||||
| fields: file.fields, // if you need multipart fields | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| const rawCrop = (request.body as { crop?: { value: string } })?.crop?.value; | ||||||||||||
| let crop: { width?: number; height?: number; x?: number; y?: number } | undefined; | ||||||||||||
| if (typeof rawCrop === 'string') { | ||||||||||||
| try { | ||||||||||||
| const parsed = JSON.parse(rawCrop); | ||||||||||||
| crop = parsed && typeof parsed === 'object' ? parsed : undefined; | ||||||||||||
| } catch (e) { | ||||||||||||
| throw new Error('Invalid crop JSON'); | ||||||||||||
| } | ||||||||||||
| } else if (rawCrop && typeof rawCrop === 'object') { | ||||||||||||
| crop = rawCrop; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| const width = crop?.width != null ? Number(crop.width) : undefined; | ||||||||||||
| const height = crop?.height != null ? Number(crop.height) : undefined; | ||||||||||||
| if (width && height) { | ||||||||||||
| let rotation = Number((request.body as { rotation?: { value: string } }).rotation?.value) || 0; | ||||||||||||
| if (rotation > 180) rotation = 180; | ||||||||||||
| if (rotation < -180) rotation = -180; | ||||||||||||
|
Comment on lines
+56
to
+58
|
||||||||||||
| let rotation = Number((request.body as { rotation?: { value: string } }).rotation?.value) || 0; | |
| if (rotation > 180) rotation = 180; | |
| if (rotation < -180) rotation = -180; |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Commented out console.log statements should be removed rather than left in the code. This clutters the codebase.
| // console.log('width', width); | |
| // console.log('width > maxWidth', width > maxWidth); | |
| // console.log('width < minWidth', width < minWidth); | |
| // console.log(ratio) | |
| // console.log(expectedRatio) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -29,10 +29,10 @@ const completeProfileRoutes = async (fastify: FastifyInstance) => { | |||||||||||||||||
| body: { | ||||||||||||||||||
| type: 'object', | ||||||||||||||||||
| properties: { | ||||||||||||||||||
| firstName: { type: 'string', minLength: 2, maxLength: 50 }, | ||||||||||||||||||
| lastName: { type: 'string', minLength: 2, maxLength: 50 }, | ||||||||||||||||||
| bio: { type: 'string', minLength: 2, maxLength: 100 }, | ||||||||||||||||||
| tags: { type: 'array', items: { type: 'string' }, minItems: 1 }, | ||||||||||||||||||
| firstName: { type: 'string', minLength: 1, maxLength: 50, pattern: '[a-zA-Z-\' ]' }, | ||||||||||||||||||
| lastName: { type: 'string', minLength: 1, maxLength: 50, pattern: '[a-zA-Z-\' ]' }, | ||||||||||||||||||
| bio: { type: 'string', minLength: 50, maxLength: 500 }, | ||||||||||||||||||
| tags: { type: 'array', items: { type: 'string', minLength: 1, maxLength: 30, pattern: '[a-zA-Z_]' }, minItems: 3 }, | ||||||||||||||||||
|
Comment on lines
+32
to
+35
|
||||||||||||||||||
| firstName: { type: 'string', minLength: 1, maxLength: 50, pattern: '[a-zA-Z-\' ]' }, | |
| lastName: { type: 'string', minLength: 1, maxLength: 50, pattern: '[a-zA-Z-\' ]' }, | |
| bio: { type: 'string', minLength: 50, maxLength: 500 }, | |
| tags: { type: 'array', items: { type: 'string', minLength: 1, maxLength: 30, pattern: '[a-zA-Z_]' }, minItems: 3 }, | |
| firstName: { type: 'string', minLength: 1, maxLength: 50, pattern: '^[a-zA-Z-\' ]+$' }, | |
| lastName: { type: 'string', minLength: 1, maxLength: 50, pattern: '^[a-zA-Z-\' ]+$' }, | |
| bio: { type: 'string', minLength: 50, maxLength: 500 }, | |
| tags: { type: 'array', items: { type: 'string', minLength: 1, maxLength: 30, pattern: '^[a-zA-Z_]+' + '$' }, minItems: 3 }, |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,23 +4,22 @@ import { setProfileHandler, getProfileHandler } from "../../../../controllers/pr | |
| const profileRoutes = async (fastify: FastifyInstance) => { | ||
| fastify.put('/', { | ||
| schema: { | ||
|
|
||
| body: { | ||
| type: 'object', | ||
| properties: { | ||
| firstName: { type: 'string' }, | ||
| lastName: { type: 'string' }, | ||
| firstName: { type: 'string', minLength: 1, maxLength: 50, pattern: '[a-zA-Z-\' ]' }, | ||
| lastName: { type: 'string', minLength: 1, maxLength: 50, pattern: '[a-zA-Z-\' ]' }, | ||
| email: { type: 'string', format: 'email' }, | ||
| bio: { type: 'string', minLength: 50, maxLength: 100 }, | ||
| tags: { type: 'array', items: { type: 'string' }, minItems: 1 }, | ||
| bio: { type: 'string', minLength: 50, maxLength: 500 }, | ||
| tags: { type: 'array', items: { type: 'string', minLength: 1, maxLength: 30, pattern: '[a-zA-Z_]' }, minItems: 3 }, | ||
|
Comment on lines
+10
to
+14
|
||
| gender: { type: 'string', enum: ['men', 'women'] }, | ||
| orientation: { type: 'string', enum: ['heterosexual', 'homosexual', 'bisexual', 'other'] }, | ||
| bornAt: { type: 'string', format: 'date-time' }, | ||
| location: { | ||
| type: 'object', | ||
| properties: { | ||
| latitude: { type: 'number' }, | ||
| longitude: { type: 'number' } | ||
| latitude: { type: 'number', minimum: -90, maximum: 90 }, | ||
| longitude: { type: 'number', minimum: -180, maximum: 180 } | ||
| }, | ||
| additionalProperties: false | ||
| } | ||
|
|
@@ -69,7 +68,7 @@ const profileRoutes = async (fastify: FastifyInstance) => { | |
| bio: { type: 'string', maxLength: 100 }, | ||
| tags: { type: 'array', items: { type: 'string' } }, | ||
| bornAt: { type: 'string', format: 'date-time' }, | ||
| gender: { type: 'string', enum: ['male', 'female'] }, | ||
| gender: { type: 'string', enum: ['men', 'women'] }, | ||
| orientation: { type: 'string', enum: ['heterosexual', 'homosexual', 'bisexual'] }, | ||
| isVerified: { type: 'boolean' }, | ||
| isProfileCompleted: { type: 'boolean' }, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -166,14 +166,15 @@ class UserService { | |||||||||
| createdAt: Date; | ||||||||||
| }> { | ||||||||||
| const user = await this.getUser(id); | ||||||||||
| console.log("Retrieved user:", user); | ||||||||||
|
||||||||||
| console.log("Retrieved user:", user); | |
| if (process.env.NODE_ENV !== 'production') { | |
| console.log("Retrieved user:", user); | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,3 +39,5 @@ yarn-error.log* | |
| # typescript | ||
| *.tsbuildinfo | ||
| next-env.d.ts | ||
|
|
||
| package-lock.json | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Debug console.log statement should be removed from production code.