Checkpoint: Ajout des permissions de gestion des attestations pour les formateurs et super-formateurs : création de formateurProcedure, fonction canAccessSequence pour vérifier les accès par séquence, modification des procédures uploadDocument, deleteDocument, sendAttestation et sendAllAttestations avec vérifications de permissions

This commit is contained in:
Manus
2026-02-02 09:53:28 -05:00
parent fadadf0d7d
commit 42d57c6e7c
3 changed files with 83 additions and 11 deletions

View File

@@ -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 { 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<boolean> {
// 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 * Récupérer la configuration d'une formation

View File

@@ -25,6 +25,14 @@ const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
return next({ ctx }); 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({ export const appRouter = router({
system: systemRouter, system: systemRouter,
auth: router({ auth: router({
@@ -2713,14 +2721,28 @@ export const appRouter = router({
}), }),
// Uploader un document d'attestation // Uploader un document d'attestation
uploadDocument: adminProcedure uploadDocument: formateurProcedure
.input(z.object({ .input(z.object({
inscriptionId: z.number(), inscriptionId: z.number(),
documentUrl: z.string(), documentUrl: z.string(),
documentS3Key: z.string(), documentS3Key: z.string(),
})) }))
.mutation(async ({ input, ctx }) => { .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( return uploadAttestationDocument(
input.inscriptionId, input.inscriptionId,
input.documentUrl, input.documentUrl,
@@ -2730,15 +2752,29 @@ export const appRouter = router({
}), }),
// Supprimer un document d'attestation // Supprimer un document d'attestation
deleteDocument: adminProcedure deleteDocument: formateurProcedure
.input(z.object({ attestationId: z.number() })) .input(z.object({ attestationId: z.number() }))
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
const { deleteAttestationDocument } = await import("./gestionAttestationsDb"); 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); return deleteAttestationDocument(input.attestationId);
}), }),
// Envoyer une attestation par email // Envoyer une attestation par email
sendAttestation: adminProcedure sendAttestation: formateurProcedure
.input(z.object({ .input(z.object({
attestationId: z.number(), attestationId: z.number(),
sequenceId: z.number(), sequenceId: z.number(),
@@ -2749,11 +2785,15 @@ export const appRouter = router({
formationNom: z.string(), formationNom: z.string(),
pdfUrl: z.string(), pdfUrl: z.string(),
})) }))
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
const { sendAttestationEmail } = await import("./emailService"); const { sendAttestationEmail } = await import("./emailService");
const { markAttestationAsSent } = await import("./gestionAttestationsDb"); const { markAttestationAsSent, canAccessSequence } = await import("./gestionAttestationsDb");
const { enregistrerEnvoiAttestation } = await import("./attestationsDb"); 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 { try {
// Envoyer l'email // Envoyer l'email
await sendAttestationEmail({ await sendAttestationEmail({
@@ -2790,7 +2830,7 @@ export const appRouter = router({
}), }),
// Envoyer toutes les attestations d'une formation en masse // Envoyer toutes les attestations d'une formation en masse
sendAllAttestations: adminProcedure sendAllAttestations: formateurProcedure
.input(z.object({ formationId: z.number() })) .input(z.object({ formationId: z.number() }))
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const { getApprenantsWithAttestationStatus } = await import("./gestionAttestationsDb"); const { getApprenantsWithAttestationStatus } = await import("./gestionAttestationsDb");

View File

@@ -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] 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] 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)