Files
core/src/features/admin/README.md
T
hykocx a3aff9fa49 feat(modules): add external module system with auto-discovery and public pages support
- 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
2026-04-25 10:50:13 -04:00

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.js documenté 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 les dependencies du 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