From de75f65d05a870f1a6310ccb00d3bfbcf7e9595a Mon Sep 17 00:00:00 2001 From: Guillaume Cauchois Date: Mon, 16 Dec 2024 19:16:04 +0100 Subject: [PATCH 1/2] feat(Messaging): add limit and slack alert on suspicious usage --- src/logging.interceptor.ts | 2 +- src/messaging/messaging.controller.ts | 9 +++ src/messaging/messaging.service.ts | 49 +++++++++++++++- src/messaging/messaging.utils.ts | 34 +++++++++++ tests/messaging/messaging.e2e-spec.ts | 84 +++++++++++++++++++++++++++ 5 files changed, 175 insertions(+), 3 deletions(-) diff --git a/src/logging.interceptor.ts b/src/logging.interceptor.ts index e039cafa..e6e99d16 100644 --- a/src/logging.interceptor.ts +++ b/src/logging.interceptor.ts @@ -17,7 +17,7 @@ export class LoggingInterceptor implements NestInterceptor { const span = tracer.scope().active(); if (span) { - span.setTag('resource.name', 'LoggingInterceptor'); // this is what will be shown in the UI beside the span name + span.setTag('resource.name', 'LoggingInterceptor'); span.setTag('http.client_ip', clientIp); } diff --git a/src/messaging/messaging.controller.ts b/src/messaging/messaging.controller.ts index a9e1ab31..387db77c 100644 --- a/src/messaging/messaging.controller.ts +++ b/src/messaging/messaging.controller.ts @@ -9,6 +9,7 @@ import { } from '@nestjs/common'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { UserPayload } from 'src/auth/guards'; +import { User } from 'src/users/models/user.model'; import { CreateMessagePipe, CreateMessageDto } from './dto'; import { ReportConversationDto } from './dto/report-conversation.dto'; import { ReportAbusePipe } from './dto/report-conversation.pipe'; @@ -49,12 +50,20 @@ export class MessagingController { @Post('messages') @UseGuards(CanParticipate) async postMessage( + @UserPayload() user: User, @UserPayload('id', new ParseUUIDPipe()) userId: string, @Body(new CreateMessagePipe()) createMessageDto: CreateMessageDto ) { // Create the conversation if needed if (!createMessageDto.conversationId && createMessageDto.participantIds) { + const countDailyConversation = + await this.messagingService.countDailyConversations(userId); + await this.messagingService.handleDailyConversationLimit( + user, + countDailyConversation, + createMessageDto.content + ); const participants = [...createMessageDto.participantIds]; // Add the current user to the participants participants.push(userId); diff --git a/src/messaging/messaging.service.ts b/src/messaging/messaging.service.ts index fec77772..cef00930 100644 --- a/src/messaging/messaging.service.ts +++ b/src/messaging/messaging.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/sequelize'; import { Op, Sequelize } from 'sequelize'; import { SlackService } from 'src/external-services/slack/slack.service'; @@ -15,7 +15,10 @@ import { messagingConversationIncludes, messagingMessageIncludes, } from './messaging.includes'; -import { generateSlackMsgConfigConversationReported } from './messaging.utils'; +import { + generateSlackMsgConfigConversationReported, + generateSlackMsgConfigUserSuspiciousUser, +} from './messaging.utils'; import { ConversationParticipant } from './models'; import { Conversation } from './models/conversation.model'; import { Message } from './models/message.model'; @@ -239,6 +242,48 @@ export class MessagingService { }); } + async countDailyConversations(userId: string) { + return this.conversationParticipantModel.count({ + where: { + userId, + createdAt: { + [Op.gte]: new Date(new Date().setHours(0, 0, 0, 0)), + }, + }, + }); + } + + async handleDailyConversationLimit( + user: User, + countDailyConversation: number, + message: string + ) { + if (countDailyConversation === 4 || countDailyConversation >= 7) { + const slackMsgConfig: SlackBlockConfig = + generateSlackMsgConfigUserSuspiciousUser( + user, + `Un utilisateur tente de créer sa ${ + countDailyConversation + 1 + }ème conversation aujourd\'hui`, + message + ); + const slackMessage = + this.slackService.generateSlackBlockMsg(slackMsgConfig); + this.slackService.sendMessage( + slackChannels.ENTOURAGE_PRO_MODERATION, + slackMessage, + 'Conversation de la messagerie signalée' + ); + } + + if (countDailyConversation >= 7) { + throw new HttpException( + 'DAILY_CONVERSATION_LIMIT_REACHED', + HttpStatus.TOO_MANY_REQUESTS + ); + } + } + private async isUserInConversation(conversationId: string, userId: string) { return this.conversationParticipantModel.findOne({ where: { diff --git a/src/messaging/messaging.utils.ts b/src/messaging/messaging.utils.ts index d477ae83..b1077717 100644 --- a/src/messaging/messaging.utils.ts +++ b/src/messaging/messaging.utils.ts @@ -34,3 +34,37 @@ export const generateSlackMsgConfigConversationReported = ( ], }; }; + +export const generateSlackMsgConfigUserSuspiciousUser = ( + user: User, + context: string, + message?: string +): SlackBlockConfig => { + const adminUserProfileUrl = `${process.env.FRONT_URL}/backoffice/admin/membres/aeb196a0-9c51-4810-ac91-6850611394cd${user.id}`; + + return { + title: '🔬 Comportement suspect detecté 👿', + context: [ + { + title: `➡️ Que se passe-t-il ?`, + content: context, + }, + { + title: '👿 Qui est-ce ?', + content: `${user.firstName} ${user.lastName} <${user.email}>`, + }, + ], + msgParts: [ + { + content: `*Message* :\n${message}`, + }, + ], + actions: [ + { + label: 'Voir le profil', + url: adminUserProfileUrl, + value: 'see-profile', + }, + ], + }; +}; diff --git a/tests/messaging/messaging.e2e-spec.ts b/tests/messaging/messaging.e2e-spec.ts index eb1fb422..02bd3d4d 100644 --- a/tests/messaging/messaging.e2e-spec.ts +++ b/tests/messaging/messaging.e2e-spec.ts @@ -326,6 +326,90 @@ describe('MESSAGING', () => { .set('authorization', `Bearer ${loggedInCandidate.token}`); expect(response.status).toBe(400); }); + + it('should return 429 when create an 8th conversation', async () => { + // Create the coachs + const coachs = await databaseHelper.createEntities(userFactory, 7, { + role: UserRoles.COACH, + }); + + // Create the conversations + const conversations = await databaseHelper.createEntities( + conversationFactory, + 7 + ); + // And link the conversations to the coachs and the logged in candidate + const linkPromises = conversations.map((conversation, idx) => + messagingHelper.associationParticipantsToConversation( + conversation.id, + [loggedInCandidate.user.id, coachs[idx].id] + ) + ); + await Promise.all(linkPromises); + + // Add messages to the conversations + const messagePromises = conversations.map((conversation) => + messagingHelper.addMessagesToConversation( + 2, + conversation.id, + loggedInCandidate.user.id + ) + ); + + await Promise.all(messagePromises); + + const response: APIResponse = + await request(server) + .post(`/messaging/messages`) + .send({ + content: 'Super message', + participantIds: [loggedInCoach.user.id], + }) + .set('authorization', `Bearer ${loggedInCandidate.token}`); + expect(response.status).toBe(429); + }); + + it('should return 201 when create a 7th conversation', async () => { + // Create the coachs + const coachs = await databaseHelper.createEntities(userFactory, 6, { + role: UserRoles.COACH, + }); + + // Create the conversations + const conversations = await databaseHelper.createEntities( + conversationFactory, + 6 + ); + // And link the conversations to the coachs and the logged in candidate + const linkPromises = conversations.map((conversation, idx) => + messagingHelper.associationParticipantsToConversation( + conversation.id, + [loggedInCandidate.user.id, coachs[idx].id] + ) + ); + await Promise.all(linkPromises); + + // Add messages to the conversations + const messagePromises = conversations.map((conversation) => + messagingHelper.addMessagesToConversation( + 2, + conversation.id, + loggedInCandidate.user.id + ) + ); + + await Promise.all(messagePromises); + + const response: APIResponse = + await request(server) + .post(`/messaging/messages`) + .send({ + content: 'Super message', + participantIds: [loggedInCoach.user.id], + }) + .set('authorization', `Bearer ${loggedInCandidate.token}`); + expect(response.status).toBe(201); + }); }); }); From dc7a513445cd80cfc6492273d0b03077f55fb758 Mon Sep 17 00:00:00 2001 From: Guillaume Cauchois Date: Tue, 17 Dec 2024 15:30:48 +0100 Subject: [PATCH 2/2] refacto: messaging service self generate countDailyConversation --- src/messaging/messaging.controller.ts | 3 --- src/messaging/messaging.service.ts | 7 ++----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/messaging/messaging.controller.ts b/src/messaging/messaging.controller.ts index 387db77c..d3876660 100644 --- a/src/messaging/messaging.controller.ts +++ b/src/messaging/messaging.controller.ts @@ -57,11 +57,8 @@ export class MessagingController { ) { // Create the conversation if needed if (!createMessageDto.conversationId && createMessageDto.participantIds) { - const countDailyConversation = - await this.messagingService.countDailyConversations(userId); await this.messagingService.handleDailyConversationLimit( user, - countDailyConversation, createMessageDto.content ); const participants = [...createMessageDto.participantIds]; diff --git a/src/messaging/messaging.service.ts b/src/messaging/messaging.service.ts index cef00930..4fadbffd 100644 --- a/src/messaging/messaging.service.ts +++ b/src/messaging/messaging.service.ts @@ -253,11 +253,8 @@ export class MessagingService { }); } - async handleDailyConversationLimit( - user: User, - countDailyConversation: number, - message: string - ) { + async handleDailyConversationLimit(user: User, message: string) { + const countDailyConversation = await this.countDailyConversations(user.id); if (countDailyConversation === 4 || countDailyConversation >= 7) { const slackMsgConfig: SlackBlockConfig = generateSlackMsgConfigUserSuspiciousUser(