diff --git a/src/features/admin/README.md b/src/features/admin/README.md new file mode 100644 index 0000000..92cebd3 --- /dev/null +++ b/src/features/admin/README.md @@ -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 ( + + ); +} + +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/`. `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 +``` diff --git a/src/features/auth/README.md b/src/features/auth/README.md new file mode 100644 index 0000000..32e144a --- /dev/null +++ b/src/features/auth/README.md @@ -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.