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 { 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
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
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] 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user