From a93d48cadad7542cdd0205e39ed37d6a45258c63 Mon Sep 17 00:00:00 2001 From: julien vdb Date: Mon, 27 Jul 2026 16:33:49 +0200 Subject: [PATCH] 0.13.23 : return the caller's conversations without opening private messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La messagerie affichait des conversations vides : ni nom, ni interlocuteur, ni message. Le front peuplait `conversation` et ses `messages` via l'API REST, dont le sanitizer retire toute relation vers un content-type que le rôle ne peut pas lire — or ni `chat-conversation.find` ni `chat-message.find` ne sont accordées. Les accorder aurait suffi, mais aurait ouvert la lecture de TOUTES les conversations et de TOUS les messages privés à n'importe quel compte connecté. On ajoute donc une action dédiée : - GET /api/chat-conversation-members/mine — conversations du seul appelant, avec interlocuteurs et messages, renvoyées via ctx.send - mot de passe et jetons retirés des utilisateurs peuplés - permission `mine` déclarée dans permissions-sync, avec la raison pour laquelle les deux `find` restent volontairement absentes Co-Authored-By: Claude Opus 5 --- package.json | 2 +- .../controllers/chat-conversation-member.ts | 83 ++++++++++++++++++- .../chat-conversation-member/routes/custom.ts | 13 +++ src/permissions-sync.ts | 8 ++ 4 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 src/api/chat-conversation-member/routes/custom.ts diff --git a/package.json b/package.json index 0cb00d6..848a7d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "harmony-back", - "version": "0.13.22", + "version": "0.13.23", "private": true, "description": "A Strapi application", "scripts": { diff --git a/src/api/chat-conversation-member/controllers/chat-conversation-member.ts b/src/api/chat-conversation-member/controllers/chat-conversation-member.ts index 118e64f..57a9cd3 100644 --- a/src/api/chat-conversation-member/controllers/chat-conversation-member.ts +++ b/src/api/chat-conversation-member/controllers/chat-conversation-member.ts @@ -2,6 +2,85 @@ * chat-conversation-member controller */ -import { factories } from '@strapi/strapi' +import { factories } from "@strapi/strapi"; -export default factories.createCoreController('api::chat-conversation-member.chat-conversation-member'); +export default factories.createCoreController( + "api::chat-conversation-member.chat-conversation-member", + ({ strapi }) => ({ + /** + * Conversations de l'utilisateur courant, avec leurs interlocuteurs et + * leurs messages. + * + * Pourquoi une action dédiée plutôt que le `find` générique : le front + * peuplait `conversation` et ses `messages` à travers l'API REST, dont le + * sanitizer retire toute relation vers un content-type que le rôle n'a pas + * le droit de lire. Les conversations revenaient donc vides — ni nom, ni + * interlocuteur, ni message. + * + * Accorder `chat-conversation.find` et `chat-message.find` aurait suffi à + * lever le filtrage, mais aurait ouvert la lecture de **toutes** les + * conversations et de **tous** les messages privés à n'importe quel compte + * connecté. Cette action retourne les données via `ctx.send`, en les + * limitant à l'appelant. + */ + async mine(ctx) { + const userId = ctx.state.user?.id; + + if (!userId) { + return ctx.unauthorized(); + } + + const members = await strapi.db + .query("api::chat-conversation-member.chat-conversation-member") + .findMany({ + where: { user: { id: userId } }, + populate: { + user: true, + conversation: { + populate: { + users: { populate: { avatar: true } }, + messages: { + populate: { + sender: { populate: { avatar: true } }, + media: true, + }, + }, + }, + }, + }, + }); + + // Les entités portent le mot de passe et les jetons des utilisateurs + // peuplés : on ne renvoie jamais ces champs au navigateur. + const stripUser = (user: any) => { + if (!user) return user; + const { + password, + resetPasswordToken, + confirmationToken, + ...safeUser + } = user; + return safeUser; + }; + + const sanitized = members.map((member: any) => ({ + ...member, + user: stripUser(member.user), + conversation: member.conversation + ? { + ...member.conversation, + users: (member.conversation.users ?? []).map(stripUser), + messages: (member.conversation.messages ?? []).map( + (message: any) => ({ + ...message, + sender: stripUser(message.sender), + }) + ), + } + : null, + })); + + ctx.send(sanitized); + }, + }) +); diff --git a/src/api/chat-conversation-member/routes/custom.ts b/src/api/chat-conversation-member/routes/custom.ts new file mode 100644 index 0000000..96e482b --- /dev/null +++ b/src/api/chat-conversation-member/routes/custom.ts @@ -0,0 +1,13 @@ +/** + * Routes personnalisées de chat-conversation-member. + */ + +export default { + routes: [ + { + method: "GET", + path: "/chat-conversation-members/mine", + handler: "chat-conversation-member.mine", + }, + ], +}; diff --git a/src/permissions-sync.ts b/src/permissions-sync.ts index 30a119b..09d8df6 100644 --- a/src/permissions-sync.ts +++ b/src/permissions-sync.ts @@ -68,6 +68,14 @@ const ROLE_PERMISSIONS: Record = { "api::chat-conversation.chat-conversation.create", "api::chat-conversation-member.chat-conversation-member.find", "api::chat-conversation-member.chat-conversation-member.update", + // Conversations de l'appelant, avec interlocuteurs et messages. + // ⚠️ VOLONTAIREMENT ABSENTES : chat-conversation.find et + // chat-message.find. Elles lèveraient le filtrage du sanitizer sur les + // relations peuplées, mais ouvriraient la lecture de toutes les + // conversations et de tous les messages privés à n'importe quel compte + // connecté. L'action `mine` retourne les mêmes données, limitées à + // l'utilisateur courant. + "api::chat-conversation-member.chat-conversation-member.mine", "api::chat-message.chat-message.create", // --- Social (posts)