Checkpoint: Ajout du rôle super_formateur avec accès à toutes les formations et séquences. Migration SQL appliquée en local et sur le VPS de production.

This commit is contained in:
Manus
2026-01-29 11:35:35 -05:00
parent 1a8d598339
commit c16392900c
9 changed files with 2331 additions and 14 deletions

View File

@@ -0,0 +1,9 @@
{
"query": "ALTER TABLE `users` MODIFY COLUMN `role` enum('user','admin','formateur','super_formateur') NOT NULL DEFAULT 'user';",
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute ALTER TABLE `users` MODIFY COLUMN `role` enum('user','admin','formateur','super_formateur') NOT NULL DEFAULT 'user';",
"rows": [],
"messages": [],
"stdout": "",
"stderr": "",
"execution_time_ms": 234
}

View File

@@ -148,6 +148,10 @@ export default function DashboardLayout({
if (user && user.role === 'formateur' && location === '/admin') { if (user && user.role === 'formateur' && location === '/admin') {
setLocation('/formateur'); setLocation('/formateur');
} }
// Les super formateurs vont sur /admin comme les admins
if (user && user.role === 'super_formateur' && location === '/formateur') {
setLocation('/admin');
}
}, [user, location, setLocation]); }, [user, location, setLocation]);
if (loading) { if (loading) {
@@ -224,6 +228,10 @@ function DashboardLayoutContent({
if (user?.role === 'formateur') { if (user?.role === 'formateur') {
return section.title !== 'Configuration' && section.title !== 'Traçabilité' && section.title !== 'Analyses' && section.title !== 'Gestion technique'; return section.title !== 'Configuration' && section.title !== 'Traçabilité' && section.title !== 'Analyses' && section.title !== 'Gestion technique';
} }
// Les super formateurs ne voient pas Gestion technique
if (user?.role === 'super_formateur') {
return section.title !== 'Gestion technique';
}
return true; return true;
}).map(section => { }).map(section => {
// Modifier le chemin du tableau de bord pour les formateurs // Modifier le chemin du tableau de bord pour les formateurs

View File

@@ -46,7 +46,7 @@ export default function AdminUsers() {
email: "", email: "",
username: "", username: "",
password: "", password: "",
role: "user" as "user" | "admin" | "formateur", role: "user" as "user" | "admin" | "formateur" | "super_formateur",
formateurId: undefined as number | undefined, formateurId: undefined as number | undefined,
}); });
const [editForm, setEditForm] = useState({ const [editForm, setEditForm] = useState({
@@ -54,7 +54,7 @@ export default function AdminUsers() {
email: "", email: "",
username: "", username: "",
password: "", password: "",
role: "user" as "user" | "admin" | "formateur", role: "user" as "user" | "admin" | "formateur" | "super_formateur",
formateurId: undefined as number | undefined, formateurId: undefined as number | undefined,
}); });
@@ -277,6 +277,7 @@ export default function AdminUsers() {
<SelectItem value="admin">Administrateur</SelectItem> <SelectItem value="admin">Administrateur</SelectItem>
<SelectItem value="user">Utilisateur</SelectItem> <SelectItem value="user">Utilisateur</SelectItem>
<SelectItem value="formateur">Formateur</SelectItem> <SelectItem value="formateur">Formateur</SelectItem>
<SelectItem value="super_formateur">Super Formateur</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -374,8 +375,8 @@ export default function AdminUsers() {
<TableCell>{user.email || "N/A"}</TableCell> <TableCell>{user.email || "N/A"}</TableCell>
<TableCell>{user.username || "N/A"}</TableCell> <TableCell>{user.username || "N/A"}</TableCell>
<TableCell> <TableCell>
<Badge variant={user.role === "admin" ? "default" : user.role === "formateur" ? "outline" : "secondary"}> <Badge variant={user.role === "admin" ? "default" : user.role === "formateur" ? "outline" : user.role === "super_formateur" ? "default" : "secondary"}>
{user.role === "admin" ? "Administrateur" : user.role === "formateur" ? "Formateur" : "Utilisateur"} {user.role === "admin" ? "Administrateur" : user.role === "formateur" ? "Formateur" : user.role === "super_formateur" ? "Super Formateur" : "Utilisateur"}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell> <TableCell>
@@ -512,11 +513,12 @@ export default function AdminUsers() {
<SelectItem value="user">Utilisateur</SelectItem> <SelectItem value="user">Utilisateur</SelectItem>
<SelectItem value="admin">Administrateur</SelectItem> <SelectItem value="admin">Administrateur</SelectItem>
<SelectItem value="formateur">Formateur</SelectItem> <SelectItem value="formateur">Formateur</SelectItem>
<SelectItem value="super_formateur">Super Formateur</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{createForm.role === "formateur" && ( {(createForm.role === "formateur" || createForm.role === "super_formateur") && (
<div> <div>
<Label htmlFor="createFormateur">Formateur associé</Label> <Label htmlFor="createFormateur">Formateur associé</Label>
<Select <Select
@@ -629,11 +631,12 @@ export default function AdminUsers() {
<SelectItem value="user">Utilisateur</SelectItem> <SelectItem value="user">Utilisateur</SelectItem>
<SelectItem value="admin">Administrateur</SelectItem> <SelectItem value="admin">Administrateur</SelectItem>
<SelectItem value="formateur">Formateur</SelectItem> <SelectItem value="formateur">Formateur</SelectItem>
<SelectItem value="super_formateur">Super Formateur</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{editForm.role === "formateur" && ( {(editForm.role === "formateur" || editForm.role === "super_formateur") && (
<div> <div>
<Label htmlFor="editFormateur">Formateur associé</Label> <Label htmlFor="editFormateur">Formateur associé</Label>
<Select <Select

View File

@@ -12,7 +12,7 @@ export default function Home() {
useEffect(() => { useEffect(() => {
if (!loading && isAuthenticated) { if (!loading && isAuthenticated) {
if (user?.role === 'admin') { if (user?.role === 'admin' || user?.role === 'super_formateur') {
setLocation('/admin'); setLocation('/admin');
} else if (user?.role === 'formateur') { } else if (user?.role === 'formateur') {
setLocation('/formateur'); setLocation('/formateur');
@@ -28,7 +28,7 @@ export default function Home() {
); );
} }
if (isAuthenticated && (user?.role === 'admin' || user?.role === 'formateur')) { if (isAuthenticated && (user?.role === 'admin' || user?.role === 'formateur' || user?.role === 'super_formateur')) {
return null; return null;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -64,6 +64,13 @@
"when": 1769611851240, "when": 1769611851240,
"tag": "0008_sturdy_albert_cleary", "tag": "0008_sturdy_albert_cleary",
"breakpoints": true "breakpoints": true
},
{
"idx": 9,
"version": "5",
"when": 1769704162958,
"tag": "0009_nervous_nomad",
"breakpoints": true
} }
] ]
} }

View File

@@ -20,7 +20,7 @@ export const users = mysqlTable("users", {
/** Mot de passe hashé (bcrypt) */ /** Mot de passe hashé (bcrypt) */
password: varchar("password", { length: 255 }), password: varchar("password", { length: 255 }),
loginMethod: varchar("loginMethod", { length: 64 }), loginMethod: varchar("loginMethod", { length: 64 }),
role: mysqlEnum("role", ["user", "admin", "formateur"]).default("user").notNull(), role: mysqlEnum("role", ["user", "admin", "formateur", "super_formateur"]).default("user").notNull(),
/** Statut du compte (actif/inactif) */ /** Statut du compte (actif/inactif) */
isActive: boolean("isActive").default(true).notNull(), isActive: boolean("isActive").default(true).notNull(),
/** ID du formateur associé (si role = formateur) */ /** ID du formateur associé (si role = formateur) */

View File

@@ -132,7 +132,7 @@ export const appRouter = router({
// Si l'utilisateur est formateur, ne montrer que les formations pour lesquelles il a des séquences // 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) { if (ctx.user.role === 'formateur' && ctx.user.formateurId) {
return db.getFormationsByFormateur(ctx.user.formateurId); return db.getFormationsByFormateur(ctx.user.formateurId);
} else if (ctx.user.role === 'admin') { } else if (ctx.user.role === 'admin' || ctx.user.role === 'super_formateur') {
return db.getFormations(); return db.getFormations();
} else { } else {
throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' }); throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' });
@@ -243,7 +243,7 @@ export const appRouter = router({
// Si l'utilisateur est formateur, ne montrer que les apprenants inscrits à ses séquences // Si l'utilisateur est formateur, ne montrer que les apprenants inscrits à ses séquences
if (ctx.user.role === 'formateur' && ctx.user.formateurId) { if (ctx.user.role === 'formateur' && ctx.user.formateurId) {
return db.getApprenantsByFormateur(ctx.user.formateurId); return db.getApprenantsByFormateur(ctx.user.formateurId);
} else if (ctx.user.role === 'admin') { } else if (ctx.user.role === 'admin' || ctx.user.role === 'super_formateur') {
return db.getApprenants(); return db.getApprenants();
} else { } else {
throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' }); throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' });
@@ -386,7 +386,7 @@ export const appRouter = router({
// Si l'utilisateur est formateur, ne montrer que ses séquences // Si l'utilisateur est formateur, ne montrer que ses séquences
if (ctx.user.role === 'formateur' && ctx.user.formateurId) { if (ctx.user.role === 'formateur' && ctx.user.formateurId) {
seqs = await db.getSequencesByFormateur(ctx.user.formateurId); seqs = await db.getSequencesByFormateur(ctx.user.formateurId);
} else if (ctx.user.role === 'admin') { } else if (ctx.user.role === 'admin' || ctx.user.role === 'super_formateur') {
seqs = await db.getSequencesWithFormation(); seqs = await db.getSequencesWithFormation();
} else { } else {
throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' }); throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' });
@@ -1279,7 +1279,7 @@ export const appRouter = router({
email: z.string().email().optional(), email: z.string().email().optional(),
username: z.string().min(3).optional(), username: z.string().min(3).optional(),
password: z.string().min(6).optional(), password: z.string().min(6).optional(),
role: z.enum(["user", "admin", "formateur"]).default("user"), role: z.enum(["user", "admin", "formateur", "super_formateur"]).default("user"),
isActive: z.boolean().default(true), isActive: z.boolean().default(true),
formateurId: z.number().optional(), formateurId: z.number().optional(),
})).mutation(async ({ input }) => { })).mutation(async ({ input }) => {
@@ -1293,7 +1293,7 @@ export const appRouter = router({
email: z.string().email().optional(), email: z.string().email().optional(),
username: z.string().min(3).optional(), username: z.string().min(3).optional(),
password: z.string().min(6).optional(), password: z.string().min(6).optional(),
role: z.enum(["user", "admin", "formateur"]).optional(), role: z.enum(["user", "admin", "formateur", "super_formateur"]).optional(),
isActive: z.boolean().optional(), isActive: z.boolean().optional(),
formateurId: z.number().optional(), formateurId: z.number().optional(),
})).mutation(async ({ input }) => { })).mutation(async ({ input }) => {

21
todo.md
View File

@@ -1424,3 +1424,24 @@
- [x] Déployer sur VPS - [x] Déployer sur VPS
- [x] Tester l'envoi avec 2 pièces jointes (test utilisateur validé) - [x] Tester l'envoi avec 2 pièces jointes (test utilisateur validé)
- [x] Créer un checkpoint - [x] Créer un checkpoint
## Nouveau rôle - Super formateur
- [ ] Modifier le schéma de base de données pour ajouter le rôle "super_formateur"
- [ ] Créer et appliquer la migration SQL
- [ ] Adapter les requêtes backend pour donner accès à toutes les formations au super formateur
- [ ] Modifier le frontend pour afficher toutes les formations au super formateur
- [ ] Ajouter une interface admin pour promouvoir un formateur en super formateur
- [ ] Tester localement
- [ ] Déployer sur VPS
- [ ] Créer un checkpoint
## Nouveau rôle - Super formateur
- [x] Modifier le schéma de base de données pour ajouter le rôle super_formateur
- [x] Créer et appliquer la migration SQL en local
- [x] Adapter le backend (routers.ts) pour gérer les permissions
- [x] Adapter le frontend (DashboardLayout, Home, AdminUsers)
- [x] Tester localement
- [x] Déployer sur VPS avec migration
- [x] Créer un checkpoint