Checkpoint: Implémentation complète des restrictions d'accès pour les formateurs : (1) Ajout du rôle "formateur" dans le schéma de base de données avec champ formateurId pour lier un utilisateur à un formateur. (2) Modification du menu de navigation DashboardLayout pour masquer automatiquement les sections "Configuration", "Traçabilité" et "Analyses" aux utilisateurs ayant le rôle formateur. (3) Filtrage backend des données : création de getSequencesByFormateur, getFormationsByFormateur et getApprenantsByFormateur dans db.ts pour ne retourner que les données pertinentes. (4) Modification des procédures tRPC formations.list, sequences.list et apprenants.list pour utiliser protectedProcedure au lieu de adminProcedure et appliquer le filtrage selon le rôle (formateur vs admin). Les formateurs ne voient désormais que leurs propres formations (celles pour lesquelles ils ont des séquences), leurs propres séquences (où ils sont assignés comme formateur) et uniquement les apprenants inscrits à leurs séquences. Les administrateurs conservent l'accès complet à toutes les données. Cette implémentation garantit une séparation claire des accès et protège les données sensibles tout en permettant aux formateurs de gérer efficacement leurs formations.
This commit is contained in:
@@ -174,9 +174,18 @@ function DashboardLayoutContent({
|
|||||||
const [location, setLocation] = useLocation();
|
const [location, setLocation] = useLocation();
|
||||||
const { state, toggleSidebar } = useSidebar();
|
const { state, toggleSidebar } = useSidebar();
|
||||||
const isCollapsed = state === "collapsed";
|
const isCollapsed = state === "collapsed";
|
||||||
|
|
||||||
|
// Filtrer les sections de menu selon le rôle
|
||||||
|
const filteredMenuSections = menuSections.filter(section => {
|
||||||
|
// Les formateurs ne voient pas Configuration ni Traçabilité
|
||||||
|
if (user?.role === 'formateur') {
|
||||||
|
return section.title !== 'Configuration' && section.title !== 'Traçabilité' && section.title !== 'Analyses';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
const [isResizing, setIsResizing] = useState(false);
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
const sidebarRef = useRef<HTMLDivElement>(null);
|
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||||
const activeMenuItem = menuSections.flatMap(section => section.items).find(item => item.path === location);
|
const activeMenuItem = filteredMenuSections.flatMap(section => section.items).find(item => item.path === location);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
// État pour gérer les sections ouvertes/fermées (persisté dans localStorage)
|
// État pour gérer les sections ouvertes/fermées (persisté dans localStorage)
|
||||||
@@ -308,7 +317,7 @@ function DashboardLayoutContent({
|
|||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
|
|
||||||
<SidebarContent className="gap-0">
|
<SidebarContent className="gap-0">
|
||||||
{menuSections.map((section, sectionIndex) => {
|
{filteredMenuSections.map((section, sectionIndex) => {
|
||||||
const isOpen = openSections[section.title] ?? false;
|
const isOpen = openSections[section.title] ?? false;
|
||||||
return (
|
return (
|
||||||
<div key={section.title}>
|
<div key={section.title}>
|
||||||
|
|||||||
102
server/db.ts
102
server/db.ts
@@ -137,6 +137,27 @@ export async function getFormations() {
|
|||||||
return await db.select().from(formations);
|
return await db.select().from(formations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getFormationsByFormateur(formateurId: number) {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) return [];
|
||||||
|
|
||||||
|
// Récupérer les IDs de formations uniques pour lesquelles le formateur a des séquences
|
||||||
|
const seqs = await db.select({ formationId: sequences.formationId })
|
||||||
|
.from(sequences)
|
||||||
|
.where(eq(sequences.formateurId, formateurId));
|
||||||
|
|
||||||
|
const formationIds = Array.from(new Set(seqs.map(s => s.formationId)));
|
||||||
|
|
||||||
|
if (formationIds.length === 0) return [];
|
||||||
|
|
||||||
|
// Récupérer les formations correspondantes
|
||||||
|
const result = await db.select().from(formations).where(
|
||||||
|
sql`${formations.id} IN (${sql.join(formationIds.map(id => sql`${id}`), sql`, `)})`
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getFormationById(id: number) {
|
export async function getFormationById(id: number) {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
if (!db) return undefined;
|
if (!db) return undefined;
|
||||||
@@ -212,6 +233,35 @@ export async function getSequencesWithFormation() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getSequencesByFormateur(formateurId: number) {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) return [];
|
||||||
|
|
||||||
|
// Récupérer uniquement les séquences de ce formateur
|
||||||
|
const seqs = await db.select().from(sequences).where(eq(sequences.formateurId, formateurId));
|
||||||
|
|
||||||
|
// Récupérer les informations de formation et de formateur pour chaque séquence
|
||||||
|
return await Promise.all(
|
||||||
|
seqs.map(async (seq) => {
|
||||||
|
const formation = await db.select().from(formations).where(eq(formations.id, seq.formationId)).limit(1);
|
||||||
|
|
||||||
|
let formateur = null;
|
||||||
|
if (seq.formateurId) {
|
||||||
|
const formateurResult = await db.select().from(formateurs).where(eq(formateurs.id, seq.formateurId)).limit(1);
|
||||||
|
if (formateurResult.length > 0) {
|
||||||
|
formateur = { id: formateurResult[0].id, nom: formateurResult[0].nom };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...seq,
|
||||||
|
formation: formation.length > 0 ? { id: formation[0].id, nom: formation[0].nom } : null,
|
||||||
|
formateur,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getSequenceById(id: number): Promise<Sequence | undefined> {
|
export async function getSequenceById(id: number): Promise<Sequence | undefined> {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
if (!db) return undefined;
|
if (!db) return undefined;
|
||||||
@@ -304,6 +354,58 @@ export async function getApprenants() {
|
|||||||
return apprenantsWithInscriptions;
|
return apprenantsWithInscriptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getApprenantsByFormateur(formateurId: number) {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) return [];
|
||||||
|
|
||||||
|
// Récupérer les IDs de séquences du formateur
|
||||||
|
const seqs = await db.select({ id: sequences.id })
|
||||||
|
.from(sequences)
|
||||||
|
.where(eq(sequences.formateurId, formateurId));
|
||||||
|
|
||||||
|
const sequenceIds = seqs.map(s => s.id);
|
||||||
|
|
||||||
|
if (sequenceIds.length === 0) return [];
|
||||||
|
|
||||||
|
// Récupérer les IDs d'apprenants inscrits à ces séquences
|
||||||
|
const inscriptionsData = await db.select({ apprenantId: inscriptions.apprenantId })
|
||||||
|
.from(inscriptions)
|
||||||
|
.where(
|
||||||
|
sql`${inscriptions.sequenceId} IN (${sql.join(sequenceIds.map(id => sql`${id}`), sql`, `)})`
|
||||||
|
);
|
||||||
|
|
||||||
|
const apprenantIds = Array.from(new Set(inscriptionsData.map(i => i.apprenantId)));
|
||||||
|
|
||||||
|
if (apprenantIds.length === 0) return [];
|
||||||
|
|
||||||
|
// Récupérer les apprenants correspondants
|
||||||
|
const apprenantsData = await db.select().from(apprenants).where(
|
||||||
|
sql`${apprenants.id} IN (${sql.join(apprenantIds.map(id => sql`${id}`), sql`, `)})`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pour chaque apprenant, récupérer ses inscriptions (filtrées par les séquences du formateur)
|
||||||
|
const apprenantsWithInscriptions = await Promise.all(
|
||||||
|
apprenantsData.map(async (apprenant) => {
|
||||||
|
const inscriptionsFiltered = await db
|
||||||
|
.select()
|
||||||
|
.from(inscriptions)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(inscriptions.apprenantId, apprenant.id),
|
||||||
|
sql`${inscriptions.sequenceId} IN (${sql.join(sequenceIds.map(id => sql`${id}`), sql`, `)})`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...apprenant,
|
||||||
|
inscriptions: inscriptionsFiltered,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return apprenantsWithInscriptions;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getApprenantById(id: number): Promise<Apprenant | undefined> {
|
export async function getApprenantById(id: number): Promise<Apprenant | undefined> {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
if (!db) return undefined;
|
if (!db) return undefined;
|
||||||
|
|||||||
@@ -36,8 +36,15 @@ export const appRouter = router({
|
|||||||
|
|
||||||
// ===== FORMATIONS =====
|
// ===== FORMATIONS =====
|
||||||
formations: router({
|
formations: router({
|
||||||
list: adminProcedure.query(async () => {
|
list: protectedProcedure.query(async ({ ctx }) => {
|
||||||
return db.getFormations();
|
// Si l'utilisateur est formateur, ne montrer que les formations pour lesquelles il a des séquences
|
||||||
|
if (ctx.user.role === 'formateur' && ctx.user.formateurId) {
|
||||||
|
return db.getFormationsByFormateur(ctx.user.formateurId);
|
||||||
|
} else if (ctx.user.role === 'admin') {
|
||||||
|
return db.getFormations();
|
||||||
|
} else {
|
||||||
|
throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' });
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
|
getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
|
||||||
@@ -140,8 +147,15 @@ export const appRouter = router({
|
|||||||
|
|
||||||
// ===== APPRENANTS =====
|
// ===== APPRENANTS =====
|
||||||
apprenants: router({
|
apprenants: router({
|
||||||
list: adminProcedure.query(async () => {
|
list: protectedProcedure.query(async ({ ctx }) => {
|
||||||
return db.getApprenants();
|
// Si l'utilisateur est formateur, ne montrer que les apprenants inscrits à ses séquences
|
||||||
|
if (ctx.user.role === 'formateur' && ctx.user.formateurId) {
|
||||||
|
return db.getApprenantsByFormateur(ctx.user.formateurId);
|
||||||
|
} else if (ctx.user.role === 'admin') {
|
||||||
|
return db.getApprenants();
|
||||||
|
} else {
|
||||||
|
throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' });
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
|
getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
|
||||||
@@ -251,8 +265,17 @@ export const appRouter = router({
|
|||||||
|
|
||||||
// ===== SÉQUENCES =====
|
// ===== SÉQUENCES =====
|
||||||
sequences: router({
|
sequences: router({
|
||||||
list: adminProcedure.query(async () => {
|
list: protectedProcedure.query(async ({ ctx }) => {
|
||||||
const seqs = await db.getSequencesWithFormation();
|
let seqs;
|
||||||
|
|
||||||
|
// Si l'utilisateur est formateur, ne montrer que ses séquences
|
||||||
|
if (ctx.user.role === 'formateur' && ctx.user.formateurId) {
|
||||||
|
seqs = await db.getSequencesByFormateur(ctx.user.formateurId);
|
||||||
|
} else if (ctx.user.role === 'admin') {
|
||||||
|
seqs = await db.getSequencesWithFormation();
|
||||||
|
} else {
|
||||||
|
throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' });
|
||||||
|
}
|
||||||
// Récupérer les dates et le nombre d'inscrits pour chaque séquence
|
// Récupérer les dates et le nombre d'inscrits pour chaque séquence
|
||||||
const sequencesAvecDates = await Promise.all(
|
const sequencesAvecDates = await Promise.all(
|
||||||
seqs.map(async (seq) => {
|
seqs.map(async (seq) => {
|
||||||
|
|||||||
9
todo.md
9
todo.md
@@ -605,3 +605,12 @@
|
|||||||
- [x] Créer colonne "Rappels pré-formation" avec badges bleus
|
- [x] Créer colonne "Rappels pré-formation" avec badges bleus
|
||||||
- [x] Créer colonne "Rappels post-formation" avec badges verts
|
- [x] Créer colonne "Rappels post-formation" avec badges verts
|
||||||
- [x] Tester l'affichage dans les vues complète et réduite
|
- [x] Tester l'affichage dans les vues complète et réduite
|
||||||
|
|
||||||
|
## Restriction d'accès pour les formateurs
|
||||||
|
- [x] Ajouter le rôle "formateur" dans la table users
|
||||||
|
- [x] Modifier le menu DashboardLayout pour masquer Configuration et Traçabilité aux formateurs
|
||||||
|
- [x] Filtrer les formations pour ne montrer que celles du formateur connecté
|
||||||
|
- [x] Filtrer les séquences pour ne montrer que celles du formateur connecté
|
||||||
|
- [x] Filtrer les apprenants pour ne montrer que ceux inscrits aux séquences du formateur
|
||||||
|
- [x] Créer une procédure backend pour vérifier si un utilisateur est formateur
|
||||||
|
- [x] Tester avec un compte formateur
|
||||||
|
|||||||
Reference in New Issue
Block a user