docs(admin): add README documentation for admin and auth features

- add comprehensive README for admin feature covering structure, API, registry, and extension points
- add comprehensive README for auth feature covering structure, API, and usage examples
This commit is contained in:
2026-04-24 21:53:47 -04:00
parent 203bd82dd9
commit f481844932
2 changed files with 523 additions and 0 deletions
+293
View File
@@ -0,0 +1,293 @@
# 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
│ └── 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
```js
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 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.
```js
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`.
```js
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`.
```js
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.
```js
// 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(),
}));
```
```js
// 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
```js
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
```js
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.
```js
// 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' });
```
```js
// 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.
```js
// 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
```
```js
// 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
```
+230
View File
@@ -0,0 +1,230 @@
# Auth
Ce répertoire gère l'authentification : inscription, connexion, sessions, réinitialisation de mot de passe, vérification d'adresse courriel et gestion du profil. Il expose des server actions Next.js, des routes API REST et des composants de pages prêts à l'emploi.
---
## Structure
```
src/features/auth/
├── index.js barrel serveur
├── actions.js server actions Next.js ('use server')
├── api.js routes API REST (users, roles)
├── auth.js register, login, resetPassword, updateUser
├── session.js createSession, validateSession, deleteSession
├── email.js tokens de vérification + envoi des e-mails
├── password.js hashPassword, verifyPassword, generateToken
├── db.js createTables, dropTables
├── storage-policies.js politiques d'accès au stockage
├── AuthPage.server.js page RSC racine (route catch-all)
├── AuthPage.client.js shell client
├── GUIDE-custom-login.md guide pour les pages personnalisées
├── components/
│ └── AuthPageHeader.js
├── pages/
│ ├── index.js re-export
│ ├── LoginPage.client.js
│ ├── RegisterPage.client.js
│ ├── ForgotPasswordPage.client.js
│ ├── ResetPasswordPage.client.js
│ ├── ConfirmEmailPage.client.js
│ └── LogoutPage.client.js
└── templates/
├── VerificationEmail.js
├── PasswordResetEmail.js
├── PasswordChangedEmail.js
├── EmailChangeConfirmEmail.js
└── EmailChangeNotifyEmail.js
```
---
## Import
```js
import { getSession, loginAction, logoutAction } from '@zen/core/features/auth/actions';
import { LoginPage, RegisterPage } from '@zen/core/features/auth/pages';
import { validateSession, createSession } from '@zen/core/features/auth';
```
---
## Pages intégrées
La route catch-all `app/auth/[...auth]/page.js` suffit pour exposer toutes les pages sans configuration supplémentaire.
```js
// app/auth/[...auth]/page.js
export { default } from '@zen/core/features/auth/server';
```
| Route | Page |
|-------|------|
| `/auth/login` | Connexion |
| `/auth/register` | Inscription |
| `/auth/forgot` | Mot de passe oublié |
| `/auth/reset` | Réinitialisation du mot de passe |
| `/auth/confirm` | Vérification de l'adresse courriel |
| `/auth/logout` | Déconnexion |
---
## Server actions
Toutes les actions sont dans `@zen/core/features/auth/actions`. Elles attendent un `FormData` sauf `getSession`, `setSessionCookie` et `refreshSessionCookie`.
### `getSession()`
Lit le cookie de session et retourne la session courante, ou `null` si l'utilisateur n'est pas connecté. Renouvelle automatiquement le cookie si la session a été rafraîchie.
```js
const session = await getSession();
if (!session?.user) redirect('/auth/login');
// session.user, session.session disponibles
```
---
### `loginAction(formData)`
Authentifie l'utilisateur et pose un cookie `HttpOnly`. Applique le rate limiting par IP et les vérifications anti-bot.
```js
const result = await loginAction(formData);
// { success: true, user } ou { success: false, error }
```
---
### `registerAction(formData)`
Crée un compte et envoie l'e-mail de vérification.
| Champ | Description |
|-------|-------------|
| `email` | Adresse courriel |
| `password` | Mot de passe |
| `name` | Nom d'affichage |
---
### `logoutAction()`
Invalide la session en base et supprime le cookie.
---
### `forgotPasswordAction(formData)`
Envoie un lien de réinitialisation si un compte existe pour l'adresse fournie. La réponse ne révèle pas si le compte existe.
---
### `resetPasswordAction(formData)`
Vérifie le token puis met à jour le mot de passe.
| Champ | Description |
|-------|-------------|
| `email` | Adresse courriel |
| `token` | Token reçu par e-mail |
| `newPassword` | Nouveau mot de passe |
---
### `verifyEmailAction(formData)`
Vérifie le token de confirmation et marque l'adresse comme vérifiée.
| Champ | Description |
|-------|-------------|
| `email` | Adresse courriel |
| `token` | Token reçu par e-mail |
---
### `setSessionCookie(token)`
Valide le token contre la base avant de l'écrire dans le cookie `HttpOnly`. À utiliser après une authentification externe ou OAuth.
---
### `refreshSessionCookie(token)`
Revalide le token et prolonge la durée de vie du cookie (30 jours).
---
## Routes API REST
Les routes sont enregistrées automatiquement sous le préfixe `/zen/api`. L'authentification est appliquée par le routeur avant chaque handler.
### Utilisateurs
| Méthode | Route | Auth | Description |
|---------|-------|------|-------------|
| `GET` | `/zen/api/users` | admin | Liste paginée des utilisateurs |
| `GET` | `/zen/api/users/:id` | admin | Détail d'un utilisateur |
| `PUT` | `/zen/api/users/:id` | admin | Modifier `name`, `role`, `email_verified` |
| `PUT` | `/zen/api/users/:id/email` | admin | Changer l'adresse courriel |
| `PUT` | `/zen/api/users/:id/password` | admin | Définir un mot de passe |
| `POST` | `/zen/api/users/:id/send-password-reset` | admin | Envoyer un lien de réinitialisation |
| `GET` | `/zen/api/users/:id/roles` | admin | Lister les rôles de l'utilisateur |
| `POST` | `/zen/api/users/:id/roles` | admin | Assigner un rôle |
| `DELETE` | `/zen/api/users/:id/roles/:roleId` | admin | Révoquer un rôle |
### Profil (utilisateur connecté)
| Méthode | Route | Description |
|---------|-------|-------------|
| `PUT` | `/zen/api/users/profile` | Modifier le nom |
| `POST` | `/zen/api/users/profile/email` | Initier un changement d'adresse courriel |
| `GET` | `/zen/api/users/email/confirm` | Confirmer le changement d'adresse |
| `POST` | `/zen/api/users/profile/password` | Changer le mot de passe |
| `POST` | `/zen/api/users/profile/picture` | Téléverser une photo de profil |
| `DELETE` | `/zen/api/users/profile/picture` | Supprimer la photo de profil |
| `GET` | `/zen/api/users/profile/sessions` | Lister les sessions actives |
| `DELETE` | `/zen/api/users/profile/sessions` | Révoquer toutes les sessions |
| `DELETE` | `/zen/api/users/profile/sessions/:sessionId` | Révoquer une session |
### Rôles
| Méthode | Route | Description |
|---------|-------|-------------|
| `GET` | `/zen/api/roles` | Lister les rôles |
| `POST` | `/zen/api/roles` | Créer un rôle |
| `GET` | `/zen/api/roles/:id` | Détail d'un rôle |
| `PUT` | `/zen/api/roles/:id` | Modifier un rôle |
| `DELETE` | `/zen/api/roles/:id` | Supprimer un rôle |
---
## Sécurité
**Rate limiting par IP.** Les actions `register`, `login`, `forgot_password`, `reset_password` et `verify_email` sont limitées par adresse IP. Quand l'IP est inconnue (pas de proxy configuré), le rate limiting est suspendu et un avertissement opérateur est émis une seule fois. Activer avec `ZEN_TRUST_PROXY=true` derrière un reverse proxy vérifié.
**Champs anti-bot.** Chaque formulaire embarque un champ honeypot (`_hp`) et un timestamp de chargement (`_t`). Une soumission trop rapide (moins de 1,5 s), trop ancienne (plus de 10 min) ou avec un honeypot rempli est rejetée.
**Cookie HttpOnly.** Le token de session n'est jamais exposé à JavaScript. `setSessionCookie` et `refreshSessionCookie` valident le token en base avant d'écrire le cookie pour éviter qu'un token arbitraire soit accepté.
**Erreurs opaques.** Les erreurs internes sont loguées côté serveur et remplacées par un message générique côté client. Seules les `UserFacingError` (token expiré, etc.) remontent verbatim.
---
## Base de données
`db.js` expose `createTables()` et `dropTables()`, appelés par `initializeZen()`.
| Table | Contenu |
|-------|---------|
| `zen_auth_users` | Utilisateurs (`id`, `email`, `name`, `role`, `email_verified`, `image`) |
| `zen_auth_sessions` | Sessions actives avec IP et user-agent |
| `zen_auth_accounts` | Comptes liés à un provider (credential, OAuth) |
| `zen_auth_verifications` | Tokens de vérification d'e-mail et de réinitialisation |
---
## Pages personnalisées
Pour envelopper les pages auth dans un layout existant, voir [GUIDE-custom-login.md](./GUIDE-custom-login.md). Le guide couvre le pattern serveur/client, les props de chaque composant et la protection de route.