Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@
# production
/nextjs/matcha/build

.github
.github

node_modules
.DS_Store
1 change: 1 addition & 0 deletions fastify/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"image-dimensions": "^2.5.0",
"pg": "^8.16.3",
"pump": "^3.0.3",
"sharp": "^0.34.5",
"text-case": "^1.2.9"
},
"devDependencies": {
Expand Down
3 changes: 2 additions & 1 deletion fastify/assets/srcs/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export const buildApp = () => {
files: 1,
headerPairs: 2000,
parts: 1000
}
},
attachFieldsToBody: true
});

app.register(websocket, {
Expand Down
49 changes: 47 additions & 2 deletions fastify/assets/srcs/controllers/private/me/profilePictures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 });
Expand All @@ -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);
Copy link

Copilot AI Dec 1, 2025

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.

Suggested change
console.log('Saving new profile picture to', newFilePath);

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Dec 18, 2025

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.

Suggested change
console.log('Saving new profile picture to', newFilePath);
if (process.env.NODE_ENV !== 'production') {
console.log('Saving new profile picture to', newFilePath);
}

Copilot uses AI. Check for mistakes.
dest.write(newBuffer);
dest.end();

// dest.on('finish', () => {
Expand Down
1 change: 1 addition & 0 deletions fastify/assets/srcs/models/User/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.log statements should be removed from production code.

Suggested change
console.log("Updating user ID:", id, "with data:", user, "and location:", location);

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Dec 18, 2025

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.

Suggested change
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 uses AI. Check for mistakes.
Copy link

Copilot AI Dec 18, 2025

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.

Suggested change
console.log("Updating user ID:", id, "with data:", user, "and location:", location);

Copilot uses AI. Check for mistakes.
user = this.fixPropertiesCase(user);
if (user.bornAt instanceof Date) {
user.bornAt = user.bornAt.toISOString();
Expand Down
61 changes: 47 additions & 14 deletions fastify/assets/srcs/plugins/checkImageConformity.ts
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'));

Expand All @@ -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
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value assigned to rotation here is unused.

Suggested change
let rotation = Number((request.body as { rotation?: { value: string } }).rotation?.value) || 0;
if (rotation > 180) rotation = 180;
if (rotation < -180) rotation = -180;

Copilot uses AI. Check for mistakes.
const ratio = (width / height).toPrecision(2);
const expectedRatio = (ratiox / ratioy).toPrecision(2);
// console.log('width', width);
// console.log('width > maxWidth', width > maxWidth);
// console.log('width < minWidth', width < minWidth);
// console.log(ratio)
// console.log(expectedRatio)
Comment on lines +61 to +65
Copy link

Copilot AI Dec 18, 2025

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.

Suggested change
// console.log('width', width);
// console.log('width > maxWidth', width > maxWidth);
// console.log('width < minWidth', width < minWidth);
// console.log(ratio)
// console.log(expectedRatio)

Copilot uses AI. Check for mistakes.

if (width > maxWidth || width < minWidth || ratio != expectedRatio)
throw new Error('Wrong file dimensions after crop/rotation');
return; // Skip other checks
}

if (!currentSize)
throw new Error('Unrecognized file format');

const ratio = (currentSize.width / currentSize.height).toPrecision(2);
const expectedRatio = (ratiox / ratioy).toPrecision(2);
if (currentSize.width > maxWidth || currentSize.width < minWidth || ratio != expectedRatio)
throw new Error('Wrong file dimensions');
} catch (err) {
if (err instanceof Error && err.message)
return reply.status(400).send({ error: err.message });
Expand Down
5 changes: 3 additions & 2 deletions fastify/assets/srcs/routes/private/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export default async function privateRoutes(fastify: FastifyInstance, options: F
fastify.register(wsRoutes, { prefix: '/ws', preHandler: fastify.checkIsCompleted });
fastify.register(statics, {
root: path.join(__dirname, '../../../uploads'),
prefix: '/uploads/',
decorateReply: false
decorateReply: false,
prefix: '/uploads', // optional: default '/'
// constraints: { host: process.env.HOST || 'localhost' }, // optional: default {}
});
}
8 changes: 4 additions & 4 deletions fastify/assets/srcs/routes/private/user/me/completeProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex patterns in the schema definitions are missing delimiters. Patterns like '[a-zA-Z-' ]' should be '^[a-zA-Z-' ]+$' to properly validate the entire string. Without anchors (^ and $), the pattern will match if any part of the string contains valid characters, not the entire string.

Suggested change
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 },

Copilot uses AI. Check for mistakes.
gender: { type: 'string', enum: ['men', 'women'] },
orientation: { type: 'string', enum: ['heterosexual', 'homosexual', 'bisexual', 'other'] },
bornAt: { type: 'string', format: 'date-time' },
Expand Down
4 changes: 2 additions & 2 deletions fastify/assets/srcs/routes/private/user/me/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ const meRoutes = async (fastify: FastifyInstance) => {
'GET /profile', 'PUT /profile', 'DELETE /profile',
]};
});
fastify.register(profilePictureRoutes, { prefix: '/profile-picture', preHandler: fastify.checkIsCompleted });
fastify.register(profilePictureRoutes, { prefix: '/profile-picture' });
fastify.register(profileRoutes, { prefix: '/profile', preHandler: fastify.checkIsCompleted });
fastify.register(completeProfileRoutes, { prefix: '/complete-profile', preHandler: fastify.checkIsVerified });
fastify.register(completeProfileRoutes, { prefix: '/complete-profile'});
}

export default meRoutes;
15 changes: 7 additions & 8 deletions fastify/assets/srcs/routes/private/user/me/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex patterns in the schema definitions are missing delimiters. Patterns like '[a-zA-Z-' ]' and '[a-zA-Z_]' should be '^[a-zA-Z-' ]+$' and '^[a-zA-Z_]+$' respectively to properly validate the entire string. Without anchors, these patterns will match if any part of the string contains valid characters, allowing invalid input to pass validation.

Copilot uses AI. Check for mistakes.
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
}
Expand Down Expand Up @@ -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' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const profilePictureRoutes = async (fastify: FastifyInstance) => {
});
fastify.post('/', {
schema: {
consumes: ['multipart/form-data'],
response: {
200: {
type: 'object',
Expand Down
2 changes: 1 addition & 1 deletion fastify/assets/srcs/routes/private/user/view/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const viewRoutes = 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'] },
fameRate: { type: 'number' },
location: {
Expand Down
7 changes: 3 additions & 4 deletions fastify/assets/srcs/services/UserService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,14 +166,15 @@ class UserService {
createdAt: Date;
}> {
const user = await this.getUser(id);
console.log("Retrieved user:", user);
Copy link

Copilot AI Dec 18, 2025

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.

Suggested change
console.log("Retrieved user:", user);
if (process.env.NODE_ENV !== 'production') {
console.log("Retrieved user:", user);
}

Copilot uses AI. Check for mistakes.
if (!user || !user.isProfileCompleted)
throw new NotFoundError();
return {
id: user.id,
email: user.email,
username: user.username,
firstName: user.firstName || '',
lastName: user.lastName || '',
firstName: user.firstName as string,
lastName: user.lastName as string,
profilePictureIndex: user.profilePictureIndex,
profilePictures: user.profilePictures || [],
bio: user.bio || '',
Expand Down Expand Up @@ -208,8 +209,6 @@ class UserService {
throw new NotFoundError();
if (user.isProfileCompleted)
throw new BadRequestError('Profile already completed');
if (user.isVerified === false)
throw new BadRequestError('Email not verified');
await this.userModel.update(id, {
...profile,
isProfileCompleted: true
Expand Down
2 changes: 2 additions & 0 deletions nextjs/matcha/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

package-lock.json
1 change: 1 addition & 0 deletions nextjs/matcha/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const nextConfig: NextConfig = {
pathname: "/api/private/uploads/**",
},
],
domains: ["localhost", "matcha.fr", "mduvey.matcha.fr"]
},
};

Expand Down
4 changes: 3 additions & 1 deletion nextjs/matcha/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"framer-motion": "^12.23.24",
"next": "16.0.0",
"react": "19.2.0",
"react-dom": "19.2.0"
"react-dom": "19.2.0",
"react-easy-crop": "^5.5.6",
"react-image-crop": "^11.0.10"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
Expand Down
Loading
Loading