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
4 changes: 4 additions & 0 deletions fastify/assets/@types/fastify.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ declare module 'fastify' {
researchUsers(userId: number, username: string, limit: number = 5, offset: number = 0, radius: number = 25, filters?: BrowsingFilter, sort?: BrowsingSort): Promise<Array<BrowsingUser>>;
};

mapService: {
getNearUsers(level: number, latitude: number, longitude: number, radius: number): Promise<{users: MapUser[], clusters: MapUserCluster[]}>;
}

nodemailer: any;

facebookOAuth2: OAuth2Namespace;
Expand Down
3 changes: 3 additions & 0 deletions fastify/assets/srcs/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import mailServicePlugin from './services/MailService'
import notificationService from './services/NotificationsServices'
import reportService from './services/ReportService'
import browsingService from './services/BrowsingService'
import mapService from './services/MapService'
// import custom plugins
import authenticate from './plugins/authenticate'
import checkImageConformity from './plugins/checkImageConformity'
Expand Down Expand Up @@ -85,6 +86,8 @@ export const buildApp = () => {

app.register(browsingService);

app.register(mapService);

app.register(pg, {
connectionString: process.env.PG
});
Expand Down
27 changes: 27 additions & 0 deletions fastify/assets/srcs/controllers/private/map/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { FastifyRequest, FastifyReply, FastifyRequestUser } from 'fastify';
import { AppError, UnauthorizedError } from '../../../utils/error';

export const getNearUsersHandler = async (
request: FastifyRequest,
reply: FastifyReply
) => {
const params = request.query as { level: string, latitude: string, longitude: string, radius: string };
const level = Number(params.level);
const latitude = Number(params.latitude);
const longitude = Number(params.longitude);
let radius = Number(params.radius);
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

The variable 'radius' is declared with 'let' but is never reassigned. Use 'const' instead for better code clarity and to prevent accidental modifications.

Suggested change
let radius = Number(params.radius);
const radius = Number(params.radius);

Copilot uses AI. Check for mistakes.

try {
const user = request.user as FastifyRequestUser | undefined;
const userId = user?.id;
if (!userId)
throw new UnauthorizedError();
const data = await request.server.mapService.getNearUsers(level, latitude, longitude, radius);
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

The userId is extracted but never passed to the map service. Consider whether the current user should be excluded from the results, as is done in the browsing service. If users should be able to see themselves on the map, this is fine; otherwise, the userId should be passed to the service and used in the query to filter out the current user.

Suggested change
const data = await request.server.mapService.getNearUsers(level, latitude, longitude, radius);
const data = await request.server.mapService.getNearUsers(level, latitude, longitude, radius, userId);

Copilot uses AI. Check for mistakes.
return reply.code(200).send(data);
} catch (error) {
console.log(error);
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

The console.log statement should be removed before merging to production, or replaced with a proper logging mechanism. Console logs in production can expose sensitive information and impact performance.

Suggested change
console.log(error);
request.log.error(error);

Copilot uses AI. Check for mistakes.
if (error instanceof AppError)
return reply.status(error.statusCode).send({ error: error.message });
return reply.status(500).send({ error: 'Internal server error' });
}
}
44 changes: 44 additions & 0 deletions fastify/assets/srcs/models/User/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ type UserLocation = {
country: string;
};

export type MapUserCluster = {
latitude: number
longitude: number
count: number
}

export type MapUser = {
id: number
firstName: string
profilePictureUrl: string
latitude: number
longitude: number
}

type BlockedUser = {
id: number;
blockerId: number;
Expand Down Expand Up @@ -111,6 +125,36 @@ export default class UserModel {
return result.rows[0];
}

getUsersFromLocation = async (lat: number, lgn: number, radius: number): Promise<MapUser[]> => {
const result = await this.fastify.pg.query(
`SELECT u.id, u.first_name, u.profile_pictures[u.profile_picture_index] AS profile_picture, locations.latitude, locations.longitude
FROM locations
JOIN users AS u ON locations.user_id = u.id
WHERE locations.user_id IS NOT NULL
AND 6371 * acos(least(1, greatest(-1, cos(radians(locations.latitude)) * cos(radians($1)) * cos(radians($2) - radians(locations.longitude)) + sin(radians(locations.latitude)) * sin(radians($1))))) < $3`,
[lat, lgn, radius]
);

return result.rows.map((row: { id: number, first_name: string, profile_picture_url: string, latitude: number, longitude: number }) => { return { id: row.id, firstName: row.first_name, profilePictureUrl: row.profile_picture_url, latitude: row.latitude, longitude: row.longitude }});
Comment on lines +130 to +138
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

There is a mismatch between the SQL query column alias and the TypeScript type definition. The SQL query aliases the column as 'profile_picture' (line 130), but the TypeScript type on line 138 expects 'profile_picture_url'. This will cause the mapping to return undefined for the profilePictureUrl field. Either change the SQL alias to 'profile_picture_url' or update the type definition to use 'profile_picture'.

Copilot uses AI. Check for mistakes.
}

getUsersCountByLocation = async (level: number, lat: number, lgn: number, radius: number): Promise<MapUserCluster[]> => {
const areaName = (level == 1) ? 'city' : (level == 2) ? 'country' : null;
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

Use strict equality operators (=== and !==) instead of loose equality (==) in the ternary operator expressions.

Suggested change
const areaName = (level == 1) ? 'city' : (level == 2) ? 'country' : null;
const areaName = (level === 1) ? 'city' : (level === 2) ? 'country' : null;

Copilot uses AI. Check for mistakes.
if (!areaName)
return [];

const result = await this.fastify.pg.query(
`SELECT count(*) AS count, avg(latitude) AS latitude, avg(longitude) AS longitude
FROM locations
WHERE ${areaName} IS NOT NULL
AND user_id IS NOT NULL
AND 6371 * acos(least(1, greatest(-1, cos(radians(latitude)) * cos(radians($1)) * cos(radians($2) - radians(longitude)) + sin(radians(latitude)) * sin(radians($1))))) < $3
GROUP BY ${areaName}`,
[lat, lgn, radius]
);
return result.rows.map((row: { count: number, latitude: number, longitude: number }) => { return { count: row.count, latitude: row.latitude, longitude: row.longitude }});
Comment on lines +142 to +155
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

The SQL query uses string interpolation for the column name in the GROUP BY clause, which could potentially lead to SQL injection if the level parameter is not properly validated. Although validation exists in the service layer, it's better to use a safer approach such as a CASE statement or explicit conditional queries rather than string interpolation.

Suggested change
const areaName = (level == 1) ? 'city' : (level == 2) ? 'country' : null;
if (!areaName)
return [];
const result = await this.fastify.pg.query(
`SELECT count(${areaName}) AS count, avg(latitude) AS latitude, avg(longitude) AS longitude
FROM locations
WHERE ${areaName} IS NOT NULL
AND user_id IS NOT NULL
AND 6371 * acos(least(1, greatest(-1, cos(radians(latitude)) * cos(radians($1)) * cos(radians($2) - radians(longitude)) + sin(radians(latitude)) * sin(radians($1))))) < $3
GROUP BY ${areaName}`,
[lat, lgn, radius]
);
return result.rows.map((row: { count: number, latitude: number, longitude: number }) => { return { count: row.count, latitude: row.latitude, longitude: row.longitude }});
let query: string | null = null;
if (level === 1) {
query = `SELECT count(city) AS count, avg(latitude) AS latitude, avg(longitude) AS longitude
FROM locations
WHERE city IS NOT NULL
AND user_id IS NOT NULL
AND 6371 * acos(least(1, greatest(-1, cos(radians(latitude)) * cos(radians($1)) * cos(radians($2) - radians(longitude)) + sin(radians(latitude)) * sin(radians($1))))) < $3
GROUP BY city`;
} else if (level === 2) {
query = `SELECT count(country) AS count, avg(latitude) AS latitude, avg(longitude) AS longitude
FROM locations
WHERE country IS NOT NULL
AND user_id IS NOT NULL
AND 6371 * acos(least(1, greatest(-1, cos(radians(latitude)) * cos(radians($1)) * cos(radians($2) - radians(longitude)) + sin(radians(latitude)) * sin(radians($1))))) < $3
GROUP BY country`;
}
if (!query) {
return [];
}
const result = await this.fastify.pg.query(
query,
[lat, lgn, radius]
);
return result.rows.map((row: { count: number, latitude: number, longitude: number }) => {
return { count: row.count, latitude: row.latitude, longitude: row.longitude };
});

Copilot uses AI. Check for mistakes.
}

setVerified = async (id: number) => {
await this.fastify.pg.query(
'UPDATE users SET is_verified=true WHERE id=$1', [id]
Expand Down
2 changes: 2 additions & 0 deletions fastify/assets/srcs/routes/private/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import reportRoutes from './report';
import browsingRoutes from './browsing';
import researchRoutes from './research';
import matchRoutes from './match';
import mapRoutes from './map';
import statics from '@fastify/static';
import path from 'path';

Expand All @@ -23,6 +24,7 @@ export default async function privateRoutes(fastify: FastifyInstance, options: F
fastify.register(browsingRoutes, { prefix: '/browsing', preHandler: fastify.checkIsCompleted });
fastify.register(researchRoutes, { prefix: '/research', preHandler: fastify.checkIsCompleted });
fastify.register(matchRoutes, { prefix: '/match', preHandler: fastify.checkIsCompleted });
fastify.register(mapRoutes, { prefix: '/map', preHandler: fastify.checkIsCompleted });
fastify.register(wsRoutes, { prefix: '/ws', preHandler: fastify.checkIsCompleted });
fastify.register(statics, {
root: path.join(__dirname, '../../../uploads'),
Expand Down
64 changes: 64 additions & 0 deletions fastify/assets/srcs/routes/private/map/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { FastifyInstance } from 'fastify';
import { getNearUsersHandler } from '../../../controllers/private/map';

const mapRoutes = async (fastify: FastifyInstance) => {
fastify.get('/', {
schema: {
debug: true,
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

The debug flag is set to true in the schema. This should be removed or set to false in production code, as it may expose additional error information that could be a security concern.

Suggested change
debug: true,
debug: false,

Copilot uses AI. Check for mistakes.
querystring: {
type: 'object',
properties: {
level: { type: 'string' },
latitude: { type: 'string' },
longitude: { type: 'string' },
radius: { type: 'string' }
},
required: ['level', 'latitude', 'longitude', 'radius'],
additionalProperties: false
},
response: {
200: {
type: 'object',
properties: {
users: { type: 'array', items: {
type: 'object',
properties: {
id: { type: 'integer' },
firstName: { type: 'string' },
profilePicture: { type: 'string', nullable: true },
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

The schema property name 'profilePicture' does not match the MapUser type property name 'profilePictureUrl'. This will cause a mismatch between the returned data and the schema validation. Either update the schema to use 'profilePictureUrl' or change the MapUser type to use 'profilePicture'.

Suggested change
profilePicture: { type: 'string', nullable: true },
profilePictureUrl: { type: 'string', nullable: true },

Copilot uses AI. Check for mistakes.
latitude: { type: 'number' },
longitude: { type: 'number' },
}
}},
clusters: { type: 'array', items: {
type: 'object',
properties: {
latitude: { type: 'number' },
longitude: { type: 'number' },
count: { type: 'integer' },
}
}}
},
additionalProperties: false
},
400: {
type: 'object',
properties: {
error: { type: 'string', }
},
additionalProperties: false
},
500: {
type: 'object',
properties: {
error: { type: 'string' }
},
additionalProperties: false
}
}
},
handler: getNearUsersHandler
});
}

export default mapRoutes;
39 changes: 39 additions & 0 deletions fastify/assets/srcs/services/MapService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import UserModel from "../models/User";
import fp from 'fastify-plugin';
import { MapUser, MapUserCluster } from "../models/User";
import { FastifyInstance } from 'fastify';
import { BadRequestError } from "../utils/error";

class MapService {
private fastify: FastifyInstance;
private userModel: UserModel;

constructor(fastify: FastifyInstance) {
this.fastify = fastify;
this.userModel = new UserModel(fastify);
}

async getNearUsers(level: number, latitude: number, longitude: number, radius: number): Promise<{ users: MapUser[], clusters: MapUserCluster[] }> {
if (isNaN(level) || isNaN(latitude) || isNaN(longitude) || isNaN(radius))
throw new BadRequestError();
if (level !== 0 && level !== 1 && level !== 2)
throw new BadRequestError();
if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180 || radius < 0)
throw new BadRequestError();

const data = {
users: [] as MapUser[],
clusters: [] as MapUserCluster[]
}
if (level == 0)
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

Use strict equality operators (=== and !==) instead of loose equality (==). The same issue appears in the ternary operator on line 142.

Copilot uses AI. Check for mistakes.
data.users = await this.userModel.getUsersFromLocation(latitude, longitude, radius);
else
data.clusters = await this.userModel.getUsersCountByLocation(level, latitude, longitude, radius);
return data;
}
}

export default fp(async (fastify: FastifyInstance) => {
const mapService = new MapService(fastify);
fastify.decorate('mapService', mapService);
});
102 changes: 102 additions & 0 deletions fastify/assets/test/integration/private/map.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { expect } from 'chai';
import { app } from '../../setup';
import { FastifyInstance } from 'fastify';
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

Unused import FastifyInstance.

Suggested change
import { FastifyInstance } from 'fastify';

Copilot uses AI. Check for mistakes.

// import fixtures
import { quickUser } from '../fixtures/auth.fixtures';

// import utils
import { setLocalisation } from '../utils/browsing';

describe('Map integration tests', async () => {

it('should be able to fetch user on level 0', async () => {
const { userData: data1, token: token1 } = await quickUser(app);
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

Unused variable data1.

Copilot uses AI. Check for mistakes.
const { userData: data2, token: token2 } = await quickUser(app);
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

Unused variable data2.

Copilot uses AI. Check for mistakes.
const { userData: data3, token: token3 } = await quickUser(app);
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

Unused variable data3.

Copilot uses AI. Check for mistakes.

await setLocalisation(app, token1, 48.8566, 2.3522);
await setLocalisation(app, token2, 48.8570, 2.3530);
await setLocalisation(app, token3, 40.7128, -74.0060); // New York

const response = await app.inject({
method: 'GET',
url: `/private/map?level=0&latitude=48.8566&longitude=2.3522&radius=0.5`, // Paris coordinates
headers: {
'Cookie': `jwt=${token3}`
}
});
expect(response.statusCode).to.equal(200);
const data = JSON.parse(response.body);
expect(data).to.have.property('users');
expect(data).to.have.property('clusters');
expect(data.users).to.be.an('array').and.have.lengthOf(2);
expect(data.clusters).to.be.an('array').and.have.lengthOf(0);
});

it('should be able to fetch user on level 1', async () => {
const { userData: data1, token: token1 } = await quickUser(app);
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

Unused variable data1.

Copilot uses AI. Check for mistakes.
const { userData: data2, token: token2 } = await quickUser(app);
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

Unused variable data2.

Copilot uses AI. Check for mistakes.
const { userData: data3, token: token3 } = await quickUser(app);
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

Unused variable data3.

Copilot uses AI. Check for mistakes.

await setLocalisation(app, token1, 48.8566, 2.3522);
await setLocalisation(app, token2, 48.8570, 2.3530);
await setLocalisation(app, token3, 40.7128, -74.0060); // New York

const response = await app.inject({
method: 'GET',
url: `/private/map?level=1&latitude=48.8566&longitude=2.3522&radius=0.5`, // Paris coordinates
headers: {
'Cookie': `jwt=${token3}`
}
});
expect(response.statusCode).to.equal(200);
const data = JSON.parse(response.body);
expect(data).to.have.property('users');
expect(data).to.have.property('clusters');
expect(data.users).to.be.an('array').and.have.lengthOf(0);
expect(data.clusters).to.be.an('array').and.have.length.that.is.above(0);
expect(data.clusters[0]).to.have.property('count').that.is.above(1);
});

it('should be able to fetch user on level 2', async () => {
const { userData: data1, token: token1 } = await quickUser(app);
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

Unused variable data1.

Copilot uses AI. Check for mistakes.
const { userData: data2, token: token2 } = await quickUser(app);
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

Unused variable data2.

Copilot uses AI. Check for mistakes.
const { userData: data3, token: token3 } = await quickUser(app);
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

Unused variable data3.

Copilot uses AI. Check for mistakes.

await setLocalisation(app, token1, 48.8566, 2.3522);
await setLocalisation(app, token2, 45.7640, 4.8357);
await setLocalisation(app, token3, 40.7128, -74.0060); // New York

const response = await app.inject({
method: 'GET',
url: `/private/map?level=2&latitude=48.8566&longitude=2.3522&radius=0.5`, // Paris coordinates
headers: {
'Cookie': `jwt=${token3}`
}
});
expect(response.statusCode).to.equal(200);
const data = JSON.parse(response.body);
expect(data).to.have.property('users');
expect(data).to.have.property('clusters');
expect(data.users).to.be.an('array').and.have.lengthOf(0);
expect(data.clusters).to.be.an('array').and.have.length.that.is.above(0);
expect(data.clusters[0]).to.have.property('count').that.is.above(0)
expect(data.clusters[0]).to.have.property('latitude').that.is.a('number')
Comment on lines +84 to +85
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

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

Missing semicolon at the end of the statement. Add a semicolon for consistency with the rest of the codebase.

Suggested change
expect(data.clusters[0]).to.have.property('count').that.is.above(0)
expect(data.clusters[0]).to.have.property('latitude').that.is.a('number')
expect(data.clusters[0]).to.have.property('count').that.is.above(0);
expect(data.clusters[0]).to.have.property('latitude').that.is.a('number');

Copilot uses AI. Check for mistakes.
expect(data.clusters[0]).to.have.property('longitude').that.is.a('number');

const moreLargeResponse = await app.inject({
method: 'GET',
url: `/private/map?level=2&latitude=48.8566&longitude=2.3522&radius=15000`, // Paris coordinates
headers: {
'Cookie': `jwt=${token3}`
}
});
expect(moreLargeResponse.statusCode).to.equal(200);
const moreLargeData = JSON.parse(moreLargeResponse.body);
expect(moreLargeData).to.have.property('users');
expect(moreLargeData).to.have.property('clusters');
expect(moreLargeData.users).to.be.an('array').and.have.lengthOf(0);
expect(moreLargeData.clusters).to.be.an('array').and.have.length.that.is.above(1);
});
});
1 change: 0 additions & 1 deletion fastify/assets/test/integration/ws/like.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,6 @@ describe('Websocket like test', () => {
'Cookie': `jwt=${tokenA}`
}
});
console.log('get matches response:', res.body);
expect(res.statusCode).to.equal(200);
const resData = JSON.parse(res.body);
const matches = resData.matches;
Expand Down