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:
9
.manus/db/db-query-1769704175404.json
Normal file
9
.manus/db/db-query-1769704175404.json
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2269
drizzle/meta/0009_snapshot.json
Normal file
2269
drizzle/meta/0009_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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) */
|
||||||
|
|||||||
@@ -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
21
todo.md
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user