- add optional `permission` field to route definitions with type validation in `define.js` - check `hasPermission()` in router after `requireAdmin()` and return 403 if denied - document `permission` and `skipRateLimit` optional fields in api README - load user permissions in `AdminPage.server.js` and pass them to client via `user` prop - use `user.permissions` in `RolesPage` and `UsersPage` to conditionally render actions - expose permission-gated API routes in `auth/api.js`
Admin
Ce répertoire fournit l'interface d'administration complète : layout, navigation, tableau de bord, gestion des utilisateurs et des rôles. Il expose un registre runtime pour permettre aux projets consommateurs d'ajouter des widgets, des entrées de navigation et des pages sans modifier le core.
Structure
src/features/admin/
├── index.js protectAdmin, isAdmin, buildNavigationSections, registre
├── protect.js gardes d'accès
├── navigation.js buildNavigationSections, buildBottomNavItems
├── registry.js registre runtime d'extensions
├── AdminLayout.server.js layout RSC de l'admin
├── AdminPage.server.js page RSC racine (protège + collecte les données widgets)
├── AdminPage.client.js shell client
├── components/
│ ├── index.js re-export
│ ├── AdminHeader.js
│ ├── AdminShell.js
│ ├── AdminSidebar.js
│ ├── AdminTop.js
│ ├── RoleEditModal.client.js
│ ├── ThemeToggle.js
│ ├── UserCreateModal.client.js
│ └── UserEditModal.client.js
├── devkit/
│ ├── ComponentsPage.client.js
│ ├── DevkitPage.client.js
│ └── IconsPage.client.js
├── pages/
│ ├── ConfirmEmailChangePage.client.js
│ ├── DashboardPage.client.js
│ ├── ProfilePage.client.js
│ ├── RolesPage.client.js
│ ├── SettingsPage.client.js
│ └── UsersPage.client.js
└── widgets/
├── index.client.js auto-registration des widgets core (côté client)
├── index.server.js auto-registration des widgets core (côté serveur)
├── users.client.js widget Utilisateurs (composant)
└── users.server.js widget Utilisateurs (fetcher)
Import
import { protectAdmin, isAdmin, buildNavigationSections } from '@zen/core/features/admin';
import {
registerWidget,
registerWidgetFetcher,
registerNavItem,
registerNavSection,
registerPage,
} from '@zen/core/features/admin';
Pages intégrées
| Route | Page |
|---|---|
/admin/dashboard |
Tableau de bord avec widgets |
/admin/users |
Liste, création et gestion des utilisateurs |
/admin/roles |
Gestion des rôles et permissions |
/admin/settings |
Paramètres de l'application |
/admin/profile |
Profil de l'utilisateur connecté |
/admin/confirm-email-change |
Confirmation de changement d'email |
API
protectAdmin(options?)
Garde serveur. Redirige si l'utilisateur n'est pas connecté ou n'a pas la permission ADMIN_ACCESS. Retourne la session courante.
const session = await protectAdmin();
// session.user est disponible
| Option | Type | Défaut | Description |
|---|---|---|---|
redirectTo |
string |
'/auth/login' |
Redirection si non authentifié |
forbiddenRedirect |
string |
'/' |
Redirection si non autorisé |
isAdmin()
Vérifie si l'utilisateur courant a la permission ADMIN_ACCESS. Retourne boolean.
const admin = await isAdmin();
if (!admin) return null;
buildNavigationSections(pathname)
Construit les sections de navigation pour la sidebar à partir du registre. Marque l'entrée active selon pathname.
const sections = buildNavigationSections('/admin/users');
// [{ id, title, icon, items: [{ name, href, icon, current }] }]
Registre d'extensions
Le registre permet d'ajouter des widgets, des entrées de navigation et des pages sans toucher au core. Les enregistrements se font via des imports à effet de bord dans le layout racine du projet consommateur.
Ajouter un widget
Un widget est composé de deux parties : un fetcher serveur qui collecte les données, et un composant client qui les affiche.
// app/admin/orders/ordersWidget.server.js
import { registerWidgetFetcher } from '@zen/core/features/admin';
import { countOrders } from './orders.server.js';
registerWidgetFetcher('orders', async () => ({
total: await countOrders(),
}));
// app/admin/orders/ordersWidget.client.js
'use client';
import { registerWidget } from '@zen/core/features/admin';
import { StatCard } from '@zen/core/shared/components';
function OrdersWidget({ data, loading }) {
return (
<StatCard
title="Commandes"
value={loading ? '-' : String(data?.total ?? 0)}
loading={loading}
/>
);
}
registerWidget({ id: 'orders', Component: OrdersWidget, order: 20 });
Le composant reçoit data (retour du fetcher) et loading (booléen). Si le fetcher échoue, data est null et loading reste false.
registerWidgetFetcher(id, fetcher)
| Paramètre | Type | Description |
|---|---|---|
id |
string |
Identifiant unique du widget |
fetcher |
async () => object |
Fonction serveur qui retourne les données |
registerWidget({ id, Component, order? })
| Paramètre | Type | Description |
|---|---|---|
id |
string |
Identifiant unique (doit correspondre au fetcher) |
Component |
ReactComponent |
Composant client affiché dans le tableau de bord |
order |
number |
Position dans la grille (défaut : 0) |
Ajouter une entrée de navigation
import { registerNavSection, registerNavItem } from '@zen/core/features/admin';
registerNavSection({ id: 'commerce', title: 'Commerce', icon: 'ShoppingBag03Icon', order: 30 });
registerNavItem({
id: 'orders',
label: 'Commandes',
icon: 'ShoppingBag03Icon',
href: '/admin/orders',
sectionId: 'commerce',
order: 10,
});
registerNavSection({ id, title, icon, order? })
| Paramètre | Type | Description |
|---|---|---|
id |
string |
Identifiant unique de la section |
title |
string |
Titre affiché dans la sidebar |
icon |
string |
Nom d'icône Hugeicons |
order |
number |
Ordre d'affichage (défaut : 0) |
registerNavItem({ id, label, icon, href, sectionId?, order?, position? })
| Paramètre | Type | Description |
|---|---|---|
id |
string |
Identifiant unique de l'entrée |
label |
string |
Texte affiché |
icon |
string |
Nom d'icône Hugeicons |
href |
string |
URL de destination |
sectionId |
string |
Section parente (défaut : 'main') |
order |
number |
Ordre d'affichage (défaut : 0) |
position |
string |
'bottom' pour épingler en bas de la sidebar |
Ajouter une page
import { registerPage } from '@zen/core/features/admin';
import OrdersPage from './OrdersPage.js';
registerPage({
slug: 'orders',
Component: OrdersPage,
title: 'Commandes',
});
La page est rendue sous /admin/<slug>. AdminPage.client.js résout le composant à partir du slug dans les paramètres de route.
registerPage({ slug, Component, title?, breadcrumbLabel? })
| Paramètre | Type | Description |
|---|---|---|
slug |
string |
Segment d'URL sous /admin/ |
Component |
ReactComponent |
Composant client rendu pour cette route |
title |
string |
Titre de la page (optionnel) |
breadcrumbLabel |
string |
Label du fil d'Ariane (optionnel, défaut : title) |
Câbler les extensions dans le projet consommateur
Regrouper tous les enregistrements dans un fichier de point d'entrée unique, puis l'importer une seule fois depuis le layout racine.
// app/zen.extensions.js
import './admin/orders/ordersWidget.server.js';
import './admin/orders/ordersWidget.client.js';
import { registerNavSection, registerNavItem, registerPage } from '@zen/core/features/admin';
import OrdersPage from './admin/orders/OrdersPage.js';
registerNavSection({ id: 'commerce', title: 'Commerce', icon: 'ShoppingBag03Icon', order: 30 });
registerNavItem({ id: 'orders', label: 'Commandes', icon: 'ShoppingBag03Icon', href: '/admin/orders', sectionId: 'commerce' });
registerPage({ slug: 'orders', Component: OrdersPage, title: 'Commandes' });
// app/layout.js
import './zen.extensions'; // les side effects enregistrent tout
DevKit
Le DevKit est une section de l'admin réservée au développement. Il expose une galerie de composants et un catalogue d'icônes. Il s'active via la variable d'environnement ZEN_DEVKIT_ENABLED=true et n'est jamais rendu en production.
| Route | Contenu |
|---|---|
/admin/devkit/components |
Galerie des composants partagés |
/admin/devkit/icons |
Catalogue d'icônes Hugeicons |
Ajouter un widget core
Les widgets intégrés au core suivent le même pattern que les widgets consommateurs, avec une étape supplémentaire : déclarer les fichiers dans les index d'auto-registration.
// src/features/admin/widgets/myWidget.server.js
import { registerWidgetFetcher } from '../registry.js';
registerWidgetFetcher('myWidget', async () => ({ ... }));
// src/features/admin/widgets/index.server.js
import './myWidget.server.js'; // ajouter cette ligne
// src/features/admin/widgets/myWidget.client.js
'use client';
import { registerWidget } from '../registry.js';
// ...
registerWidget({ id: 'myWidget', Component: MyWidget, order: 20 });
// src/features/admin/widgets/index.client.js
import './myWidget.client.js'; // ajouter cette ligne