Files
harmony-back/src/permissions-sync.ts
T
admin 3e9f5c3f60
Build release Docker image / Build Docker Images (push) Successful in 8m5s
0.13.25 : drop the dead legacy direct-message content-types
`conversation` et `direct-message` n'ont plus aucun appelant depuis la
suppression de choral-old (front 0.18.17) : aucune référence dans le
back, aucune dans le front, et 0 ligne en base.

- content-types supprimés
- leurs 4 permissions retirées de permissions-sync

⚠️ La synchro étant additive, les permissions déjà présentes en base ne
sont pas supprimées automatiquement : elles apparaîtront désormais dans
la liste des permissions non déclarées au démarrage, à décocher dans
l'admin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 17:55:09 +02:00

212 lines
7.9 KiB
TypeScript

import type { Core } from "@strapi/strapi";
/**
* Source de vérité VERSIONNÉE des permissions Users & Permissions.
*
* Synchronisée au démarrage (voir bootstrap dans src/index.ts) : toute
* permission listée ici et absente en base est créée pour le rôle.
* La synchro est ADDITIVE : elle n'en supprime jamais — les permissions en
* trop sont seulement signalées dans les logs, la suppression reste un geste
* manuel (admin ou SQL).
*
* Liste établie par audit du front (voir AUDIT_PERMISSIONS.md à la racine du
* projet parent, 24/07/2026) : chaque action correspond à un appel réel de
* harmony-web. Toute modification ici doit être répercutée dans cet audit et
* dans les CLAUDE.md.
*/
const ROLE_PERMISSIONS: Record<string, string[]> = {
authenticated: [
// --- Marketplace (annonces) — ⚠️ update/delete : ownership à verrouiller côté controller
"api::ad.ad.find",
"api::ad.ad.create",
"api::ad.ad.update",
"api::ad.ad.delete",
// --- Chorale
"api::choral.choral.find",
"api::choral.choral.create",
"api::choral.choral.update",
"api::choral-membership.choral-membership.find",
"api::choral-membership.choral-membership.create",
"api::choral-membership.choral-membership.update",
"api::announcement.announcement.find",
"api::permissions-template.permissions-template.create",
"api::permissions-template.permissions-template.delete",
"api::form-template.form-template.find",
"api::form-template.form-template.create",
// --- Boards (kanban)
"api::board.board.find",
"api::board.board.create",
"api::board.board.update",
"api::board.board.delete",
"api::board-list.board-list.find",
"api::board-list.board-list.create",
"api::board-list.board-list.update",
"api::board-list.board-list.delete",
"api::board-card.board-card.create",
"api::board-card.board-card.update",
"api::board-card.board-card.delete",
// --- Chat de chorale (salons + messages)
"api::channel.channel.create",
"api::channel.channel.update",
"api::channel.channel.delete",
"api::message.message.find",
"api::message.message.create",
"api::message.message.update",
"api::message.message.delete",
"api::message.message.addReaction",
// --- Messagerie (nouveau système chat-conversation)
"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)
"api::post.post.find",
"api::post.post.create",
"api::post.post.update",
"api::post.post.delete",
"api::post.post.feed",
"api::post.post.savePost",
"api::post.post.removeSavedPost",
"api::post.post.hidePost",
"api::post.post.removeHiddenPost",
"api::post.post.likePost",
"api::post.post.addComment",
"api::post.post.link",
"api::post-ownership.post-ownership.find",
// Lecture seule : sans `find`, le sanitizer de l'API REST retire les
// commentaires des réponses où ils sont peuplés (page de détail d'une
// publication). L'écriture reste passée par `post.addComment`, qui
// contrôle l'auteur — pas de create/update/delete ici.
"api::comment.comment.find",
"api::comment.comment.findOne",
"api::report.report.create",
// --- Contacts / amis
"api::contact.contact.find",
"api::contact.contact.create",
"api::contact.contact.update",
"api::contact.contact.delete",
"api::contact.contact.activities",
"api::contact.contact.suggestions",
// --- Événements
"api::event.event.find",
"api::event.event.create",
"api::event.event.update",
"api::event.event.delete",
"api::event.event.feed",
"api::event.event.applyEvent",
"api::event.event.unapplyEvent",
"api::event-relationship.event-relationship.find",
// --- Groupes
"api::group.group.find",
"api::group.group.create",
"api::group.group.update",
"api::group.group.delete",
"api::group.group.invite",
"api::group-membership.group-membership.find",
"api::group-membership.group-membership.create",
"api::group-membership.group-membership.update",
"api::group-membership.group-membership.delete",
// --- Invitations chorale
"api::invite.invite.find",
"api::invite.invite.create",
"api::invite.invite.update",
"api::invite.invite.delete",
// --- Notifications
"api::notification.notification.find",
"api::notification.notification.update",
"api::notification.notification.delete",
// --- Contenu / commandes
"api::page.page.find",
"api::order.order.find",
"api::order.order.findOne",
// --- Plugins
"plugin::upload.content-api.upload",
"plugin::users-permissions.auth.changePassword",
"plugin::users-permissions.user.me",
"plugin::users-permissions.user.find",
"plugin::users-permissions.user.findOne",
"plugin::users-permissions.user.updateMe",
// ⚠️ VOLONTAIREMENT ABSENT : plugin::users-permissions.user.update
// (permettrait à n'importe quel utilisateur de modifier n'importe quel
// compte). Le front doit passer par updateMe — voir AUDIT_PERMISSIONS.md.
],
public: [
"plugin::users-permissions.auth.callback",
"plugin::users-permissions.auth.connect",
"plugin::users-permissions.auth.register",
"plugin::users-permissions.auth.emailConfirmation",
"plugin::users-permissions.auth.sendEmailConfirmation",
"plugin::users-permissions.auth.forgotPassword",
"plugin::users-permissions.auth.resetPassword",
// Pages CMS publiques (landing / pages légales)
"api::page.page.find",
"api::legal-page.legal-page.find",
],
};
/**
* Crée les permissions manquantes pour chaque rôle (idempotent, additif).
* À appeler depuis bootstrap().
*/
export async function syncRolePermissions(strapi: Core.Strapi): Promise<void> {
for (const [roleType, wantedActions] of Object.entries(ROLE_PERMISSIONS)) {
const role = await strapi.db
.query("plugin::users-permissions.role")
.findOne({ where: { type: roleType } });
if (!role) {
strapi.log.warn(`[permissions-sync] Rôle "${roleType}" introuvable, ignoré.`);
continue;
}
const existing = await strapi.db
.query("plugin::users-permissions.permission")
.findMany({ where: { role: role.id }, select: ["action"] });
const existingActions = new Set(existing.map((p: { action: string }) => p.action));
const missing = wantedActions.filter((a) => !existingActions.has(a));
for (const action of missing) {
await strapi.db
.query("plugin::users-permissions.permission")
.create({ data: { action, role: role.id } });
}
const extra = [...existingActions].filter((a) => !wantedActions.includes(a));
if (missing.length > 0) {
strapi.log.info(
`[permissions-sync] ${roleType}: ${missing.length} permission(s) créée(s) → ${missing.join(", ")}`,
);
}
if (extra.length > 0) {
strapi.log.warn(
`[permissions-sync] ${roleType}: ${extra.length} permission(s) en base non déclarée(s) ici (non supprimées) → ${extra.join(", ")}`,
);
}
if (missing.length === 0 && extra.length === 0) {
strapi.log.info(`[permissions-sync] ${roleType}: à jour (${wantedActions.length} permissions).`);
}
}
}