From 42d57c6e7c590f5b376f441a1b08eb6a7ddad772 Mon Sep 17 00:00:00 2001 From: Manus Date: Mon, 2 Feb 2026 09:53:28 -0500 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Ajout=20des=20permissions=20de=20?= =?UTF-8?q?gestion=20des=20attestations=20pour=20les=20formateurs=20et=20s?= =?UTF-8?q?uper-formateurs=20:=20cr=C3=A9ation=20de=20formateurProcedure,?= =?UTF-8?q?=20fonction=20canAccessSequence=20pour=20v=C3=A9rifier=20les=20?= =?UTF-8?q?acc=C3=A8s=20par=20s=C3=A9quence,=20modification=20des=20proc?= =?UTF-8?q?=C3=A9dures=20uploadDocument,=20deleteDocument,=20sendAttestati?= =?UTF-8?q?on=20et=20sendAllAttestations=20avec=20v=C3=A9rifications=20de?= =?UTF-8?q?=20permissions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/gestionAttestationsDb.ts | 34 +++++++++++++++++-- server/routers.ts | 58 ++++++++++++++++++++++++++++----- todo.md | 2 ++ 3 files changed, 83 insertions(+), 11 deletions(-) diff --git a/server/gestionAttestationsDb.ts b/server/gestionAttestationsDb.ts index ff4ab71..5b9d29b 100644 --- a/server/gestionAttestationsDb.ts +++ b/server/gestionAttestationsDb.ts @@ -1,6 +1,36 @@ -import { eq, desc, and } from "drizzle-orm"; +import { eq, desc, and, isNotNull, sql } from "drizzle-orm"; import { getDb } from "./db"; -import { formations, attestations, inscriptions, apprenants, sequences, datesFormation } from "../drizzle/schema"; +import { formations, attestations, inscriptions, apprenants, sequences, datesFormation, users } from "../drizzle/schema"; + +/** + * Vérifier si un utilisateur a accès à une séquence + * - Admins et super_formateurs : accès à tout + * - Formateurs : accès uniquement à leurs séquences assignées + */ +export async function canAccessSequence(userId: number, userRole: string, sequenceId: number): Promise { + // Admins et super-formateurs ont accès à tout + if (userRole === 'admin' || userRole === 'super_formateur') { + return true; + } + + // Pour les formateurs, vérifier s'ils sont assignés à cette séquence + if (userRole === 'formateur') { + const db = await getDb(); + if (!db) return false; + + // Récupérer le formateurId de l'utilisateur + const user = await db.select({ formateurId: users.formateurId }).from(users).where(eq(users.id, userId)).limit(1); + if (user.length === 0 || !user[0].formateurId) return false; + + // Vérifier si le formateur est assigné à cette séquence + const sequence = await db.select({ formateurId: sequences.formateurId }).from(sequences).where(eq(sequences.id, sequenceId)).limit(1); + if (sequence.length === 0) return false; + + return sequence[0].formateurId === user[0].formateurId; + } + + return false; +} /** * Récupérer la configuration d'une formation diff --git a/server/routers.ts b/server/routers.ts index 8ae22ae..881ea28 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -25,6 +25,14 @@ const adminProcedure = protectedProcedure.use(({ ctx, next }) => { return next({ ctx }); }); +// Procédure pour formateurs, super-formateurs et admins +const formateurProcedure = protectedProcedure.use(({ ctx, next }) => { + if (!['admin', 'formateur', 'super_formateur'].includes(ctx.user.role)) { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès réservé aux formateurs et administrateurs' }); + } + return next({ ctx }); +}); + export const appRouter = router({ system: systemRouter, auth: router({ @@ -2713,14 +2721,28 @@ export const appRouter = router({ }), // Uploader un document d'attestation - uploadDocument: adminProcedure + uploadDocument: formateurProcedure .input(z.object({ inscriptionId: z.number(), documentUrl: z.string(), documentS3Key: z.string(), })) .mutation(async ({ input, ctx }) => { - const { uploadAttestationDocument } = await import("./gestionAttestationsDb"); + const { uploadAttestationDocument, canAccessSequence } = await import("./gestionAttestationsDb"); + const { getDb } = await import("./db"); + const { inscriptions } = await import("../drizzle/schema"); + const { eq } = await import("drizzle-orm"); + + // Récupérer la séquence de l'inscription + const db = await getDb(); + if (!db) throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Database not available' }); + const inscription = await db.select({ sequenceId: inscriptions.sequenceId }).from(inscriptions).where(eq(inscriptions.id, input.inscriptionId)).limit(1); + if (inscription.length === 0) throw new TRPCError({ code: 'NOT_FOUND', message: 'Inscription not found' }); + + // Vérifier l'accès + const hasAccess = await canAccessSequence(ctx.user.id, ctx.user.role, inscription[0].sequenceId); + if (!hasAccess) throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès refusé à cette séquence' }); + return uploadAttestationDocument( input.inscriptionId, input.documentUrl, @@ -2730,15 +2752,29 @@ export const appRouter = router({ }), // Supprimer un document d'attestation - deleteDocument: adminProcedure + deleteDocument: formateurProcedure .input(z.object({ attestationId: z.number() })) - .mutation(async ({ input }) => { - const { deleteAttestationDocument } = await import("./gestionAttestationsDb"); + .mutation(async ({ input, ctx }) => { + const { deleteAttestationDocument, canAccessSequence } = await import("./gestionAttestationsDb"); + const { getDb } = await import("./db"); + const { attestations } = await import("../drizzle/schema"); + const { eq } = await import("drizzle-orm"); + + // Récupérer la séquence de l'attestation + const db = await getDb(); + if (!db) throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Database not available' }); + const attestation = await db.select({ sequenceId: attestations.sequenceId }).from(attestations).where(eq(attestations.id, input.attestationId)).limit(1); + if (attestation.length === 0) throw new TRPCError({ code: 'NOT_FOUND', message: 'Attestation not found' }); + + // Vérifier l'accès + const hasAccess = await canAccessSequence(ctx.user.id, ctx.user.role, attestation[0].sequenceId); + if (!hasAccess) throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès refusé à cette séquence' }); + return deleteAttestationDocument(input.attestationId); }), // Envoyer une attestation par email - sendAttestation: adminProcedure + sendAttestation: formateurProcedure .input(z.object({ attestationId: z.number(), sequenceId: z.number(), @@ -2749,11 +2785,15 @@ export const appRouter = router({ formationNom: z.string(), pdfUrl: z.string(), })) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { const { sendAttestationEmail } = await import("./emailService"); - const { markAttestationAsSent } = await import("./gestionAttestationsDb"); + const { markAttestationAsSent, canAccessSequence } = await import("./gestionAttestationsDb"); const { enregistrerEnvoiAttestation } = await import("./attestationsDb"); + // Vérifier l'accès à la séquence + const hasAccess = await canAccessSequence(ctx.user.id, ctx.user.role, input.sequenceId); + if (!hasAccess) throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès refusé à cette séquence' }); + try { // Envoyer l'email await sendAttestationEmail({ @@ -2790,7 +2830,7 @@ export const appRouter = router({ }), // Envoyer toutes les attestations d'une formation en masse - sendAllAttestations: adminProcedure + sendAllAttestations: formateurProcedure .input(z.object({ formationId: z.number() })) .mutation(async ({ input }) => { const { getApprenantsWithAttestationStatus } = await import("./gestionAttestationsDb"); diff --git a/todo.md b/todo.md index 3d91363..c66f308 100644 --- a/todo.md +++ b/todo.md @@ -1489,3 +1489,5 @@ - [x] Modifier la feuille de présence pour ajouter deux colonnes de signature pour le formateur (Matin et Après-midi) - [x] Retirer la ligne de date en dessous des signatures du formateur dans la feuille de présence + +- [x] Créer l'interface de gestion des attestations pour formateurs et super-formateurs (charger, supprimer, modifier, envoyer)