From 6478bea038ca4875c25679d5135026488b82265a Mon Sep 17 00:00:00 2001 From: Shaun Hamilton Date: Tue, 22 Apr 2025 16:28:16 +0200 Subject: [PATCH] feat(api): add user id to user report (#59816) --- api/src/routes/protected/user.test.ts | 22 +++++------ api/src/routes/protected/user.ts | 38 +++++++++++++++---- api/src/schemas/user/report-user.ts | 8 ++-- api/src/utils/email-templates.ts | 7 ++-- client/i18n/locales/english/translations.json | 1 + 5 files changed, 50 insertions(+), 26 deletions(-) diff --git a/api/src/routes/protected/user.test.ts b/api/src/routes/protected/user.test.ts index 02ba3bf62f3..0ee036d8e12 100644 --- a/api/src/routes/protected/user.test.ts +++ b/api/src/routes/protected/user.test.ts @@ -13,7 +13,8 @@ import { devLogin, setupServer, superRequest, - createSuperRequest + createSuperRequest, + defaultUsername } from '../../../jest.utils'; import { JWT_SECRET } from '../../utils/env'; import { @@ -802,29 +803,25 @@ describe('userRoutes', () => { reportDescription: 'Test Report' }); - expect(response.statusCode).toBe(400); + expect(response.statusCode).toBe(404); expect(response.body).toStrictEqual({ type: 'danger', - message: 'flash.provide-username' + message: 'flash.report-error' }); }); test('POST returns 400 for empty report', async () => { const response = await superPost('/user/report-user').send({ - username: 'darth-vader', + username: testUserData.username, reportDescription: '' }); expect(response.statusCode).toBe(400); - expect(response.body).toStrictEqual({ - type: 'danger', - message: 'flash.provide-username' - }); }); test('POST sanitises report description', async () => { await superPost('/user/report-user').send({ - username: 'darth-vader', + username: defaultUsername, reportDescription: 'Luke, I am your father' }); @@ -846,7 +843,7 @@ describe('userRoutes', () => { } ); const response = await superPost('/user/report-user').send({ - username: 'darth-vader', + username: testUser.username, reportDescription: 'Luke, I am your father' }); @@ -855,11 +852,11 @@ describe('userRoutes', () => { from: 'team@freecodecamp.org', to: 'support@freecodecamp.org', cc: 'foo@bar.com', - subject: "Abuse Report : Reporting darth-vader's profile.", + subject: `Abuse Report : Reporting ${testUser.username}'s profile.`, text: ` Hello Team, -This is to report the profile of darth-vader. +This is to report the profile of ${testUser.username}. ID: ${defaultUserId}. Report Details: @@ -867,6 +864,7 @@ Luke, I am your father Reported by: +ID: ${testUser.id} Username: ${testUser.username} Name: Email: foo@bar.com diff --git a/api/src/routes/protected/user.ts b/api/src/routes/protected/user.ts index 6728c46008c..c783599e9b5 100644 --- a/api/src/routes/protected/user.ts +++ b/api/src/routes/protected/user.ts @@ -19,7 +19,7 @@ import { normalizeTwitter, removeNulls } from '../../utils/normalize'; -import type { UpdateReqType } from '../../utils'; +import { mapErr, type UpdateReqType } from '../../utils'; import { getCalendar, getPoints, @@ -176,21 +176,45 @@ export const userRoutes: FastifyPluginCallbackTypebox = ( }); const { username, reportDescription: report } = req.body; - if (!username || !report) { - logger.warn('Missing username or reportDescription'); - void reply.code(400); + // TODO: `findUnique` once db migration forces unique usernames + const maybeReportedUsers = await mapErr( + fastify.prisma.user.findMany({ + where: { username } + }) + ); + + if (maybeReportedUsers.hasError) { + logger.error( + { error: maybeReportedUsers.error, username }, + 'Error finding reported user.' + ); + fastify.Sentry.captureException(maybeReportedUsers.error); + void reply.code(500); return { type: 'danger', - message: 'flash.provide-username' + message: 'flash.generic-error' } as const; } + const reportedUsers = maybeReportedUsers.data; + + if (reportedUsers.length !== 1) { + logger.warn({ username }, 'Reported user not found'); + void reply.code(404); + return { + type: 'danger', + message: 'flash.report-error' + } as const; + } + + const reportedUser = reportedUsers[0]!; + await fastify.sendEmail({ from: 'team@freecodecamp.org', to: 'support@freecodecamp.org', cc: user.email, - subject: `Abuse Report : Reporting ${username}'s profile.`, - text: generateReportEmail(user, username, report) + subject: `Abuse Report : Reporting ${reportedUser.username}'s profile.`, + text: generateReportEmail(user, reportedUser, report) }); return { diff --git a/api/src/schemas/user/report-user.ts b/api/src/schemas/user/report-user.ts index f2523b264cf..4c62198ebff 100644 --- a/api/src/schemas/user/report-user.ts +++ b/api/src/schemas/user/report-user.ts @@ -4,7 +4,7 @@ import { genericError } from '../types'; export const reportUser = { body: Type.Object({ username: Type.String(), - reportDescription: Type.String() + reportDescription: Type.String({ minLength: 1 }) }), response: { 200: Type.Object({ @@ -14,10 +14,10 @@ export const reportUser = { email: Type.String() }) }), - 400: Type.Object({ + 404: Type.Object({ type: Type.Literal('danger'), - message: Type.Literal('flash.provide-username') + message: Type.Literal('flash.report-error') }), - default: genericError + 500: genericError } }; diff --git a/api/src/utils/email-templates.ts b/api/src/utils/email-templates.ts index 31373c59ec9..20f502b75c1 100644 --- a/api/src/utils/email-templates.ts +++ b/api/src/utils/email-templates.ts @@ -9,13 +9,13 @@ import { user } from '@prisma/client'; */ export const generateReportEmail = ( reporter: user, - abuser: string, + abuser: user, reportDesc: string ) => { return ` Hello Team, -This is to report the profile of ${abuser}. +This is to report the profile of ${abuser.username}. ID: ${abuser.id}. Report Details: @@ -23,10 +23,11 @@ ${reportDesc} Reported by: +ID: ${reporter.id} Username: ${reporter.username} Name:${reporter.name ? ' ' + reporter.name : ''} Email: ${reporter.email} Thanks and regards, -${reporter.name ?? ''}`; +${reporter.name ?? reporter.username}`; }; diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json index a70d14dd726..d9dd18c2df1 100644 --- a/client/i18n/locales/english/translations.json +++ b/client/i18n/locales/english/translations.json @@ -905,6 +905,7 @@ "unlink-success": "You've successfully unlinked your {{website}}", "provide-username": "Check if you have provided a username and a report", "report-sent": "A report was sent to the team with {{email}} in copy", + "report-error": "Unable to report this user at this time.", "certificate-missing": "The certification you tried to view does not exist", "create-token-err": "An error occurred while creating your user token", "delete-token-err": "An error occurred while deleting your user token",