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:
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
2
todo.md
2
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)
|
||||
|
||||
Reference in New Issue
Block a user