- add `src/core/modules/` with registry, discovery (server), and public index - add `src/core/public-pages/` with registry, server component, and public index - add `src/core/users/permissions-registry.js` for runtime permission registration - expose `./modules`, `./public-pages`, and `./public-pages/server` package exports - rename `registerFeatureRoutes` to `registerApiRoutes` with backward-compatible alias - extend `seedDefaultRolesAndPermissions` to include module-registered permissions - update `initializeZen` and shared init to wire module discovery and registration - add `docs/MODULES.md` documenting the `@zen/module-*` authoring contract - update `docs/DEV.md` with references to module system docs
10 KiB
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.
Le pattern
zen.extensions.jsdocumenté ici reste valide pour les extensions in-projet (extensions ad hoc spécifiques à une app). Pour distribuer une extension réutilisable comme un package npm, consulter docs/MODULES.md — la même API d'enregistrement s'utilise mais le module est auto-découvert via lesdependenciesdu projet consommateur.
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, userPermissions?)
Construit les sections de navigation pour la sidebar à partir du registre. Marque l'entrée active selon pathname. Les items dont le champ permission n'est pas présent dans userPermissions sont automatiquement exclus ; si tous les items d'une section sont exclus, la section disparaît également.
const permissions = await getUserPermissions(session.user.id);
const sections = buildNavigationSections('/admin/users', permissions);
// [{ 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?, permission? })
| 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) |
permission |
string |
Clé de permission requise pour voir ce widget (ex. 'users.view'). Le widget est masqué si l'utilisateur ne possède pas cette permission. |
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,
permission: 'orders.view', // optionnel — masqué si l'utilisateur n'a pas cette permission
});
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?, permission? })
| 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 |
permission |
string |
Clé de permission requise pour voir cette entrée (ex. 'orders.view'). L'entrée est masquée si l'utilisateur ne possède pas cette permission. |
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', permission: 'orders.view' });
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