From 5f1df3f9cc2cf105a7ca13bf8f0edda744f7196e Mon Sep 17 00:00:00 2001 From: Manus Date: Fri, 9 Jan 2026 10:32:25 -0500 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Impl=C3=A9mentation=20compl=C3=A8?= =?UTF-8?q?te=20des=20restrictions=20d'acc=C3=A8s=20pour=20les=20formateur?= =?UTF-8?q?s=20:=20(1)=20Ajout=20du=20r=C3=B4le=20"formateur"=20dans=20le?= =?UTF-8?q?=20sch=C3=A9ma=20de=20base=20de=20donn=C3=A9es=20avec=20champ?= =?UTF-8?q?=20formateurId=20pour=20lier=20un=20utilisateur=20=C3=A0=20un?= =?UTF-8?q?=20formateur.=20(2)=20Modification=20du=20menu=20de=20navigatio?= =?UTF-8?q?n=20DashboardLayout=20pour=20masquer=20automatiquement=20les=20?= =?UTF-8?q?sections=20"Configuration",=20"Tra=C3=A7abilit=C3=A9"=20et=20"A?= =?UTF-8?q?nalyses"=20aux=20utilisateurs=20ayant=20le=20r=C3=B4le=20format?= =?UTF-8?q?eur.=20(3)=20Filtrage=20backend=20des=20donn=C3=A9es=20:=20cr?= =?UTF-8?q?=C3=A9ation=20de=20getSequencesByFormateur,=20getFormationsByFo?= =?UTF-8?q?rmateur=20et=20getApprenantsByFormateur=20dans=20db.ts=20pour?= =?UTF-8?q?=20ne=20retourner=20que=20les=20donn=C3=A9es=20pertinentes.=20(?= =?UTF-8?q?4)=20Modification=20des=20proc=C3=A9dures=20tRPC=20formations.l?= =?UTF-8?q?ist,=20sequences.list=20et=20apprenants.list=20pour=20utiliser?= =?UTF-8?q?=20protectedProcedure=20au=20lieu=20de=20adminProcedure=20et=20?= =?UTF-8?q?appliquer=20le=20filtrage=20selon=20le=20r=C3=B4le=20(formateur?= =?UTF-8?q?=20vs=20admin).=20Les=20formateurs=20ne=20voient=20d=C3=A9sorma?= =?UTF-8?q?is=20que=20leurs=20propres=20formations=20(celles=20pour=20lesq?= =?UTF-8?q?uelles=20ils=20ont=20des=20s=C3=A9quences),=20leurs=20propres?= =?UTF-8?q?=20s=C3=A9quences=20(o=C3=B9=20ils=20sont=20assign=C3=A9s=20com?= =?UTF-8?q?me=20formateur)=20et=20uniquement=20les=20apprenants=20inscrits?= =?UTF-8?q?=20=C3=A0=20leurs=20s=C3=A9quences.=20Les=20administrateurs=20c?= =?UTF-8?q?onservent=20l'acc=C3=A8s=20complet=20=C3=A0=20toutes=20les=20do?= =?UTF-8?q?nn=C3=A9es.=20Cette=20impl=C3=A9mentation=20garantit=20une=20s?= =?UTF-8?q?=C3=A9paration=20claire=20des=20acc=C3=A8s=20et=20prot=C3=A8ge?= =?UTF-8?q?=20les=20donn=C3=A9es=20sensibles=20tout=20en=20permettant=20au?= =?UTF-8?q?x=20formateurs=20de=20g=C3=A9rer=20efficacement=20leurs=20forma?= =?UTF-8?q?tions.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/DashboardLayout.tsx | 13 ++- server/db.ts | 102 ++++++++++++++++++++++ server/routers.ts | 35 ++++++-- todo.md | 9 ++ 4 files changed, 151 insertions(+), 8 deletions(-) diff --git a/client/src/components/DashboardLayout.tsx b/client/src/components/DashboardLayout.tsx index ae26b71..5b260c0 100644 --- a/client/src/components/DashboardLayout.tsx +++ b/client/src/components/DashboardLayout.tsx @@ -174,9 +174,18 @@ function DashboardLayoutContent({ const [location, setLocation] = useLocation(); const { state, toggleSidebar } = useSidebar(); const isCollapsed = state === "collapsed"; + + // Filtrer les sections de menu selon le rôle + const filteredMenuSections = menuSections.filter(section => { + // Les formateurs ne voient pas Configuration ni Traçabilité + if (user?.role === 'formateur') { + return section.title !== 'Configuration' && section.title !== 'Traçabilité' && section.title !== 'Analyses'; + } + return true; + }); const [isResizing, setIsResizing] = useState(false); const sidebarRef = useRef(null); - const activeMenuItem = menuSections.flatMap(section => section.items).find(item => item.path === location); + const activeMenuItem = filteredMenuSections.flatMap(section => section.items).find(item => item.path === location); const isMobile = useIsMobile(); // État pour gérer les sections ouvertes/fermées (persisté dans localStorage) @@ -308,7 +317,7 @@ function DashboardLayoutContent({ - {menuSections.map((section, sectionIndex) => { + {filteredMenuSections.map((section, sectionIndex) => { const isOpen = openSections[section.title] ?? false; return (
diff --git a/server/db.ts b/server/db.ts index 324147e..b9aa4c7 100644 --- a/server/db.ts +++ b/server/db.ts @@ -137,6 +137,27 @@ export async function getFormations() { return await db.select().from(formations); } +export async function getFormationsByFormateur(formateurId: number) { + const db = await getDb(); + if (!db) return []; + + // Récupérer les IDs de formations uniques pour lesquelles le formateur a des séquences + const seqs = await db.select({ formationId: sequences.formationId }) + .from(sequences) + .where(eq(sequences.formateurId, formateurId)); + + const formationIds = Array.from(new Set(seqs.map(s => s.formationId))); + + if (formationIds.length === 0) return []; + + // Récupérer les formations correspondantes + const result = await db.select().from(formations).where( + sql`${formations.id} IN (${sql.join(formationIds.map(id => sql`${id}`), sql`, `)})` + ); + + return result; +} + export async function getFormationById(id: number) { const db = await getDb(); if (!db) return undefined; @@ -212,6 +233,35 @@ export async function getSequencesWithFormation() { ); } +export async function getSequencesByFormateur(formateurId: number) { + const db = await getDb(); + if (!db) return []; + + // Récupérer uniquement les séquences de ce formateur + const seqs = await db.select().from(sequences).where(eq(sequences.formateurId, formateurId)); + + // Récupérer les informations de formation et de formateur pour chaque séquence + return await Promise.all( + seqs.map(async (seq) => { + const formation = await db.select().from(formations).where(eq(formations.id, seq.formationId)).limit(1); + + let formateur = null; + if (seq.formateurId) { + const formateurResult = await db.select().from(formateurs).where(eq(formateurs.id, seq.formateurId)).limit(1); + if (formateurResult.length > 0) { + formateur = { id: formateurResult[0].id, nom: formateurResult[0].nom }; + } + } + + return { + ...seq, + formation: formation.length > 0 ? { id: formation[0].id, nom: formation[0].nom } : null, + formateur, + }; + }) + ); +} + export async function getSequenceById(id: number): Promise { const db = await getDb(); if (!db) return undefined; @@ -304,6 +354,58 @@ export async function getApprenants() { return apprenantsWithInscriptions; } +export async function getApprenantsByFormateur(formateurId: number) { + const db = await getDb(); + if (!db) return []; + + // Récupérer les IDs de séquences du formateur + const seqs = await db.select({ id: sequences.id }) + .from(sequences) + .where(eq(sequences.formateurId, formateurId)); + + const sequenceIds = seqs.map(s => s.id); + + if (sequenceIds.length === 0) return []; + + // Récupérer les IDs d'apprenants inscrits à ces séquences + const inscriptionsData = await db.select({ apprenantId: inscriptions.apprenantId }) + .from(inscriptions) + .where( + sql`${inscriptions.sequenceId} IN (${sql.join(sequenceIds.map(id => sql`${id}`), sql`, `)})` + ); + + const apprenantIds = Array.from(new Set(inscriptionsData.map(i => i.apprenantId))); + + if (apprenantIds.length === 0) return []; + + // Récupérer les apprenants correspondants + const apprenantsData = await db.select().from(apprenants).where( + sql`${apprenants.id} IN (${sql.join(apprenantIds.map(id => sql`${id}`), sql`, `)})` + ); + + // Pour chaque apprenant, récupérer ses inscriptions (filtrées par les séquences du formateur) + const apprenantsWithInscriptions = await Promise.all( + apprenantsData.map(async (apprenant) => { + const inscriptionsFiltered = await db + .select() + .from(inscriptions) + .where( + and( + eq(inscriptions.apprenantId, apprenant.id), + sql`${inscriptions.sequenceId} IN (${sql.join(sequenceIds.map(id => sql`${id}`), sql`, `)})` + ) + ); + + return { + ...apprenant, + inscriptions: inscriptionsFiltered, + }; + }) + ); + + return apprenantsWithInscriptions; +} + export async function getApprenantById(id: number): Promise { const db = await getDb(); if (!db) return undefined; diff --git a/server/routers.ts b/server/routers.ts index ecd8d5f..d27cca4 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -36,8 +36,15 @@ export const appRouter = router({ // ===== FORMATIONS ===== formations: router({ - list: adminProcedure.query(async () => { - return db.getFormations(); + list: protectedProcedure.query(async ({ ctx }) => { + // Si l'utilisateur est formateur, ne montrer que les formations pour lesquelles il a des séquences + if (ctx.user.role === 'formateur' && ctx.user.formateurId) { + return db.getFormationsByFormateur(ctx.user.formateurId); + } else if (ctx.user.role === 'admin') { + return db.getFormations(); + } else { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' }); + } }), getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => { @@ -140,8 +147,15 @@ export const appRouter = router({ // ===== APPRENANTS ===== apprenants: router({ - list: adminProcedure.query(async () => { - return db.getApprenants(); + list: protectedProcedure.query(async ({ ctx }) => { + // Si l'utilisateur est formateur, ne montrer que les apprenants inscrits à ses séquences + if (ctx.user.role === 'formateur' && ctx.user.formateurId) { + return db.getApprenantsByFormateur(ctx.user.formateurId); + } else if (ctx.user.role === 'admin') { + return db.getApprenants(); + } else { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' }); + } }), getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => { @@ -251,8 +265,17 @@ export const appRouter = router({ // ===== SÉQUENCES ===== sequences: router({ - list: adminProcedure.query(async () => { - const seqs = await db.getSequencesWithFormation(); + list: protectedProcedure.query(async ({ ctx }) => { + let seqs; + + // Si l'utilisateur est formateur, ne montrer que ses séquences + if (ctx.user.role === 'formateur' && ctx.user.formateurId) { + seqs = await db.getSequencesByFormateur(ctx.user.formateurId); + } else if (ctx.user.role === 'admin') { + seqs = await db.getSequencesWithFormation(); + } else { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' }); + } // Récupérer les dates et le nombre d'inscrits pour chaque séquence const sequencesAvecDates = await Promise.all( seqs.map(async (seq) => { diff --git a/todo.md b/todo.md index 5e01675..01c4a8c 100644 --- a/todo.md +++ b/todo.md @@ -605,3 +605,12 @@ - [x] Créer colonne "Rappels pré-formation" avec badges bleus - [x] Créer colonne "Rappels post-formation" avec badges verts - [x] Tester l'affichage dans les vues complète et réduite + +## Restriction d'accès pour les formateurs +- [x] Ajouter le rôle "formateur" dans la table users +- [x] Modifier le menu DashboardLayout pour masquer Configuration et Traçabilité aux formateurs +- [x] Filtrer les formations pour ne montrer que celles du formateur connecté +- [x] Filtrer les séquences pour ne montrer que celles du formateur connecté +- [x] Filtrer les apprenants pour ne montrer que ceux inscrits aux séquences du formateur +- [x] Créer une procédure backend pour vérifier si un utilisateur est formateur +- [x] Tester avec un compte formateur