React · Admin · RBAC

Pages d'Administration

Vues basées sur les rôles, formulaires complexes, tableaux de données

Construire les outils des instructeurs

Utilisez les flèches, cliquez ou glissez pour naviguer

Objectifs de la leçon

1. Implémenter le RBAC frontend

Rôles, permissions et guards de navigation

2. Vues conditionnelles par rôle

Même page, contenu différent selon l'utilisateur

3. Tableaux de données

Tri, pagination, recherche client et serveur

4. Formulaires multi-step

Champs dynamiques, formulaires imbriqués

5. Patterns d'admin : bulk actions, confirmations, audit log

Plan du cours

1

RBAC frontend : rôles, permissions, guards

Comprendre qui a le droit de faire quoi — et les limites du frontend

2

Vues conditionnelles par rôle

Même URL, contenu différent — le pattern roleGate

3

Tableaux de données

Tri, pagination, recherche — client vs serveur

4

Formulaires complexes

Multi-step, champs dynamiques, formulaires imbriqués

5

Patterns d'admin

Bulk actions, confirmation dialogs, audit log

RBAC Frontend

Role-Based Access Control

Qui a le droit de voir quoi ? Le RBAC frontend guide l'UX, mais la sécurité est côté serveur.

Rôles vs Permissions

Rôle

Un groupe de permissions

const roles = {

"admin": ["read", "write", "delete"],

"instructor": ["read", "write"],

"student": ["read"]

};

Permission

Une action spécifique autorisée

const canDelete = (user) =>

user.permissions.includes("delete");

const canEdit = (user) =>

user.permissions.includes("write");

💡 Un rôle est un raccourci — on vérifie toujours la permission, pas le rôle directement.

Le hook usePermissions

function usePermissions() {

const { user } = useAuth();

const hasPermission = (perm: string) =>

user?.permissions?.includes(perm) ?? false;

const hasRole = (role: string) =>

user?.role === role;

return { hasPermission, hasRole };

}

// Utilisation

const { hasPermission } = usePermissions();

{hasPermission("delete") && (

<button onClick={handleDelete}>Supprimer</button>

)}

💡 Centraliser la logique dans un hook = un seul endroit à modifier si les règles changent.

Guards de navigation

// ProtectedRoute — empêche la navigation

function ProtectedRoute({ permission }) {

const { hasPermission } = usePermissions();

if (!hasPermission(permission)) {

return <Navigate to="/unauthorized" />;

}

return <Outlet />;

}

// Dans le router

<Route element={<ProtectedRoute permission="write" />}>

<Route path="/admin/courses" element={<CourseAdmin />} />

</Route>

⚠️ Un guard empêche la navigation, mais ne protège pas les données !

L'API doit aussi vérifier les permissions — le frontend est contournable.

RBAC frontend ≠ Sécurité

✅ Ce que le RBAC frontend fait

  • • Améliorer l'UX : cacher ce qui est inutile
  • • Éviter la confusion : pas de bouton inaccessible
  • • Guider l'utilisateur dans son parcours
  • • Réduire les appels API inutiles

❌ Ce que le RBAC frontend NE fait PAS

  • • Protéger les données
  • • Empêcher un utilisateur malveillant
  • • Remplacer la vérification côté serveur
  • • Garantir qu'une action est autorisée

Règle d'or : masquer un bouton côté frontend n'est JAMAIS une mesure de sécurité.

Le backend doit TOUJOURS vérifier les permissions.

Vues conditionnelles

Même page, contenu différent selon le rôle

Un instructeur voit les outils de gestion, un étudiant voit ses notes.

Le pattern roleGate

// Composant qui affiche selon le rôle

function DashboardPage() {

const { hasRole } = usePermissions();

return (

<div>

<StatsCards /> {/* tout le monde */}

{hasRole("instructor") && (

<CourseManagement />

)}

{hasRole("student") && (

<MyGrades />

)}

</div>

);

}

💡 Même URL, composants différents — pas besoin de routes séparées pour chaque rôle.

Composant <IfPermission>

function IfPermission({ permission, children, fallback }) {

const { hasPermission } = usePermissions();

if (!hasPermission(permission)) return fallback ?? null;

return children;

}

// Utilisation déclarative

<IfPermission permission="delete"

fallback={<span>Lecture seule</span>}>

<button onClick={handleDelete}>Supprimer</button>

</IfPermission>

💡 Le fallback permet d'afficher un message au lieu de rien — meilleure UX.

Exemple : Page Cours

👨‍🎓 Étudiant voit

  • ✅ Liste des cours inscrits
  • ✅ Sa progression
  • ✅ Ses notes
  • ❌ Pas de bouton "Éditer"
  • ❌ Pas de gestion des étudiants

👩‍🏫 Instructeur voit

  • ✅ Tous les cours (créés par lui)
  • ✅ Bouton "Nouveau cours"
  • ✅ Liste des étudiants inscrits
  • ✅ Bouton "Éditer" / "Supprimer"
  • ✅ Statistiques du cours

💡 Même composant CoursePage, mais les sous-composants changent selon le rôle.

Tableaux de données

Tri, pagination, recherche

Le composant le plus complexe d'une interface d'administration.

Structure d'un DataTable

interface DataTableProps<T> {

data: T[];

columns: Column<T>[];

sortable?: boolean;

pagination?: { page: number; pageSize: number; total: number };

onSort?: (key: string, dir: "asc" | "desc") => void;

onPageChange?: (page: number) => void;

}

Tri

Cliquer sur l'en-tête pour trier

Pagination

Naviguer page par page

Recherche

Filtrer les résultats

Tri côté client

const [sortKey, setSortKey] = useState("name");

const [sortDir, setSortDir] = useState("asc");

const sorted = useMemo(() =>

[...data].sort((a, b) => {

const dir = sortDir === "asc" ? 1 : -1;

return a[sortKey] > b[sortKey] ? dir : -dir;

}),

[data, sortKey, sortDir]

);

💡 useMemo évite de re-trier à chaque render — on ne trie que quand les données ou le critère changent.

Pagination : client vs serveur

✅ Côté client

Pour les petites listes (< 100)

// Tout est en mémoire

const pageData = sorted

.slice(start, end);

  • ✅ Rapide, pas d'appel réseau
  • ✅ Tri et filtre instantanés

✅ Côté serveur

Pour les grandes listes (> 100)

// Appel API avec params

fetch(`/api/courses?page=2&sort=name`)

  • ✅ Charge mémoire réduite
  • ✅ Données toujours à jour

⚠️ Pagination côté client pour 10 000 éléments = crash du navigateur !

Pagination côté serveur avec TanStack Query

const { data, isLoading } = useQuery({

queryKey: ["courses", page, sortKey, sortDir],

queryFn: () =>

fetch(`/api/courses?page=${page}&sort=${sortKey}&dir=${sortDir}`)

.then(r => r.json())

});

// Réponse du serveur

{

"data": [...], // 20 éléments

"page": 2,

"totalPages": 50,

"total": 1000

}

💡 Le queryKey change → TanStack Query refait l'appel automatiquement. Pas de fetch manuel !

Recherche : debounce + serveur

// Debounce : attendre 300ms après la dernière frappe

const [search, setSearch] = useState("");

const debouncedSearch = useDebounce(search, 300);

const { data } = useQuery({

queryKey: ["courses", debouncedSearch],

queryFn: () =>

fetch(`/api/courses?q=${debouncedSearch}`)

.then(r => r.json())

});

💡 Sans debounce, chaque frappe envoie une requête — "abc" = 3 appels API en 300ms !

Formulaires complexes

Multi-step, dynamiques, imbriqués

Les formulaires d'admin ne sont jamais simples — il faut les structurer.

Multi-step : le pattern

const [step, setStep] = useState(0);

const methods = useForm({ defaultValues });

const steps = [

{ title: "Infos", component: StepInfos },

{ title: "Détails", component: StepDetails },

{ title: "Confirmation", component: StepConfirm }

];

1️⃣ Infos

Titre, catégorie

2️⃣ Détails

Description, modules

3️⃣ Confirmation

Vérifier et valider

💡 Un seul useForm pour tous les steps — les données sont préservées entre les étapes.

Validation par étape

const nextStep = async () => {

const valid = await methods.trigger(stepFields[step]);

if (valid) setStep(s => s + 1);

};

// Champs à valider par étape

const stepFields = [

["title", "category"], // step 0

["description", "modules"], // step 1

];

💡 trigger() de React Hook Form valide uniquement les champs de l'étape courante — pas toute la page.

Champs dynamiques avec useFieldArray

const { fields, append, remove } = useFieldArray({

control: methods.control,

name: "modules"

});

{fields.map((field, index) => (

<div key={field.id}>

<input {...methods.register(`modules.${index}.name`)} />

<button onClick={() => remove(index)}>✕</button>

</div>

))}

<button onClick={() => append({ name: "" })}>+ Module</button>

💡 useFieldArray gère l'ajout/suppression dynamique — chaque champ a un id unique stable.

Formulaires imbriqués

// Sous-formulaire réutilisable

function AddressFields({ prefix }) {

const { register } = useFormContext();

return (

<>

<input {...register(`${prefix}.street`)} />

<input {...register(`${prefix}.city`)} />

</>

);

}

// Utilisation

<AddressFields prefix="billing" />

<AddressFields prefix="shipping" />

💡 useFormContext partage le formulaire parent — pas besoin de passer les props en cascade.

Patterns d'admin

Bulk actions, confirmations, audit log

Les actions d'administration nécessitent des précautions particulières.

Bulk actions (actions en masse)

const [selected, setSelected] = useState<string[]>([]);

const toggleAll = () =>

setSelected(prev =>

prev.length === data.length ? [] : data.map(d => d.id)

);

const bulkDelete = async () => {

await mutateAsync(selected);

setSelected([]);

};

⚠️ Bulk delete SANS confirmation = catastrophe potentielle !

Toujours afficher un dialog de confirmation avec le nombre d'éléments sélectionnés.

Confirmation dialogs

const [confirm, setConfirm] = useState(null);

const handleDelete = (id: string) => {

setConfirm({ id, message: "Supprimer ce cours ?" });

};

const confirmAction = async () => {

await deleteCourse(confirm.id);

setConfirm(null);

};

Les confirmations sont OBLIGATOIRES pour :

  • • Suppression d'un élément
  • • Bulk actions (suppression en masse)
  • • Actions irréversibles (reset, archivage)
  • • Changement de rôle d'un utilisateur

💡 Afficher le nombre d'éléments affectés dans le dialog : "Supprimer 3 cours ?" est plus clair que "Supprimer ?"

Audit log : qui a fait quoi ?

interface AuditEntry {

id: string;

action: "create" | "update" | "delete";

resource: string;

userId: string;

timestamp: Date;

details?: Record<string, unknown>;

}

Pourquoi un audit log ?

  • • Traçabilité : savoir qui a modifié quoi et quand
  • • Debugging : retrouver l'origine d'un problème
  • • Conformité : certaines réglementations l'exigent
  • • Confiance : les instructeurs savent que leurs actions sont enregistrées

💡 L'audit log est côté serveur — le frontend se contente de l'afficher en lecture seule.

Pièges courants

❌ Masquer un bouton = sécurité

// Frontend only

{!canDelete && <Btn/>}

// API non protégée ❌

// Frontend + Backend

{!canDelete && <Btn/>}

// API vérifie le token ✓

✅ Le backend doit TOUJOURS vérifier les permissions.

❌ Pagination côté client pour 10 000 éléments

Charger tout en mémoire → navigateur crash

✅ Pagination côté serveur dès que la liste dépasse ~100 éléments.

❌ Multi-step sans sauvegarde intermédiaire

L'utilisateur navigue ailleurs → toutes les données du formulaire perdues

✅ Sauvegarder en brouillon (localStorage ou API) à chaque étape.

À retenir !

🔒 RBAC frontend = UX

La vraie sécurité est côté serveur — le frontend guide l'utilisateur

📊 Pagination serveur

Nécessaire pour les grandes listes (> 100 éléments)

⚠️ Confirmations obligatoires

Pour toute action destructive — suppression, bulk actions

📝 Audit log

Qui a fait quoi et quand — traçabilité et confiance

Après cette leçon, vous savez :

RBAC · Vues conditionnelles · DataTable · Multi-step · Patterns d'admin

Questions ?

Le RBAC frontend est pour l'UX, pas pour la sécurité

Les pages d'admin sont le cœur de l'application instructeur — prenez le temps de les construire correctement.