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 { 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

View File

@@ -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");