docs(core): add README files for all core framework modules

- add cron/README.md documenting the node-cron wrapper API and job registration pattern
- add email/README.md documenting the Resend wrapper, env vars, and template usage
- add payments/README.md documenting the payments module
- add pdf/README.md documenting the pdf generation module
- add themes/README.md documenting the theming system
- add toast/README.md documenting the toast notification module
- add users/README.md documenting the users module
This commit is contained in:
2026-04-24 21:48:31 -04:00
parent e1ee9ef564
commit 203bd82dd9
7 changed files with 1227 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
# Cron Framework
Ce répertoire est un **wrapper générique autour de `node-cron`**. Il ne connaît aucune tâche spécifique — les modules et features enregistrent leurs propres jobs. Ajouter un nouveau job ne nécessite jamais de modifier `src/core/cron/`.
---
## Structure
```
src/core/cron/
└── index.js schedule, stop, stopAll, trigger, validate, isRunning, getJobs, getStatus
```
---
## Import
```js
import { schedule, stop, trigger } from '@zen/core/cron';
```
---
## API
### `schedule(name, cronSchedule, handler, options?)`
Enregistre un job. Si un job du même nom existe déjà, il est stoppé et remplacé.
```js
schedule('daily-report', '0 9 * * *', async () => {
await sendReport();
});
schedule('every-5min', '*/5 * * * *', async () => {
await syncData();
}, { timezone: 'America/New_York', runOnInit: true });
```
| Paramètre | Type | Description |
|-----------|------|-------------|
| `name` | `string` | Nom unique du job |
| `cronSchedule` | `string` | Expression cron (5 ou 6 champs) |
| `handler` | `async Function` | Fonction exécutée à chaque déclenchement |
| `options.timezone` | `string` | Timezone IANA (défaut : `ZEN_TIMEZONE` ou `America/Toronto`) |
| `options.runOnInit` | `boolean` | Exécuter immédiatement à l'enregistrement (défaut : `false`) |
Retourne l'instance `node-cron` task.
---
### `stop(name)`
Stoppe et supprime un job par son nom. Retourne `true` si le job existait, `false` sinon.
### `stopAll()`
Stoppe et supprime tous les jobs enregistrés.
### `trigger(name)`
Déclenche manuellement un job sans attendre son prochain tick. Lève une `Error` si le job n'existe pas.
```js
await trigger('daily-report');
```
### `validate(expression)`
Valide une expression cron. Retourne `boolean`.
### `isRunning(name)`
Vérifie si un job est actuellement enregistré. Retourne `boolean`.
### `getJobs()`
Retourne la liste des noms de tous les jobs enregistrés (`string[]`).
### `getStatus()`
Retourne les métadonnées de tous les jobs enregistrés.
```js
{
'daily-report': {
schedule: '0 9 * * *',
timezone: 'America/Toronto',
registeredAt: '2026-04-24T09:00:00.000Z'
}
}
```
---
## Enregistrer un job depuis un module
Les jobs vivent **avec leur feature ou module**, pas dans le framework. Enregistrer un job dans `initializeZen()` (`src/shared/lib/init.js`) :
```js
// src/modules/mymodule/cron.js
import { schedule } from '@zen/core/cron';
export function registerCronJobs() {
schedule('mymodule-sync', '*/15 * * * *', async () => {
await syncMyModule();
});
}
```
```js
// src/shared/lib/init.js
import { registerCronJobs } from '../../modules/mymodule/cron.js';
registerCronJobs();
```
---
## Comportement Hot-Reload
Les jobs sont stockés dans `globalThis[Symbol.for('__ZEN_CRON_JOBS__')]` — un store partagé qui survit aux invalidations de cache de modules de Next.js. Un job enregistré deux fois (hot-reload) remplace silencieusement l'ancien plutôt que de créer un doublon.
---
## Gestion des erreurs
Les erreurs levées par un handler sont interceptées et loguées via `fail()` — elles ne font jamais crasher le processus.
```
✗ Cron daily-report: Connection timeout
```
+155
View File
@@ -0,0 +1,155 @@
# Email Framework
Ce répertoire fournit un **wrapper autour de [Resend](https://resend.com)** pour l'envoi d'emails, ainsi qu'un composant de mise en page React Email réutilisable. Il ne connaît aucun template métier — les features créent leurs propres templates et utilisent ce module pour l'envoi.
---
## Structure
```
src/core/email/
├── index.js sendEmail, sendBatchEmails
└── templates/
├── index.js re-export
└── BaseLayout.js composant de mise en page React Email
```
---
## Import
```js
import { sendEmail, sendBatchEmails } from '@zen/core/email';
import { BaseLayout } from '@zen/core/email/templates';
```
---
## Variables d'environnement
| Variable | Obligatoire | Description |
|----------|-------------|-------------|
| `ZEN_EMAIL_RESEND_APIKEY` | Oui | Clé API Resend |
| `ZEN_EMAIL_FROM_ADDRESS` | Oui | Adresse expéditeur par défaut |
| `ZEN_EMAIL_FROM_NAME` | Non | Nom affiché de l'expéditeur |
| `ZEN_EMAIL_LOGO` | Non | URL du logo affiché dans `BaseLayout` |
| `ZEN_EMAIL_LOGO_URL` | Non | URL de destination du lien autour du logo |
| `ZEN_SUPPORT_EMAIL` | Non | Email affiché dans le footer si `supportSection` est activé |
| `ZEN_NAME` | Non | Nom de l'application (fallback du nom affiché dans `BaseLayout`) |
---
## API
### `sendEmail(email)`
Envoie un email via Resend. Retourne `{ success, data, error }`.
```js
const result = await sendEmail({
to: 'user@example.com',
subject: 'Bienvenue',
html: '<p>Bonjour !</p>',
});
if (!result.success) {
console.error(result.error);
}
```
| Paramètre | Type | Description |
|-----------|------|-------------|
| `to` | `string \| string[]` | Destinataire(s) |
| `subject` | `string` | Objet de l'email |
| `html` | `string` | Corps HTML |
| `text` | `string` | Corps texte brut (optionnel) |
| `from` | `string` | Adresse expéditeur (défaut : `ZEN_EMAIL_FROM_ADDRESS`) |
| `fromName` | `string` | Nom expéditeur (défaut : `ZEN_EMAIL_FROM_NAME`) |
| `replyTo` | `string` | Adresse de réponse (optionnel) |
| `attachments` | `object[]` | Pièces jointes Resend (optionnel) |
| `tags` | `object[]` | Tags Resend (optionnel) |
---
### `sendBatchEmails(emails)`
Envoie plusieurs emails en une seule requête batch Resend. Retourne `{ success, data, error }`.
```js
await sendBatchEmails([
{ to: 'a@example.com', subject: 'Sujet A', html: '<p>A</p>' },
{ to: 'b@example.com', subject: 'Sujet B', html: '<p>B</p>' },
]);
```
Chaque objet du tableau accepte les mêmes paramètres que `sendEmail`.
---
## BaseLayout
Composant React Email (`@react-email/components`) qui fournit une structure cohérente : logo ou nom de l'app, titre optionnel, contenu, footer avec copyright et lien support.
```jsx
import { render } from '@react-email/render';
import { BaseLayout } from '@zen/core/email/templates';
const html = await render(
<BaseLayout
preview="Votre commande est confirmée"
title="Commande confirmée"
supportSection
>
<Text>Merci pour votre achat.</Text>
</BaseLayout>
);
await sendEmail({ to: 'user@example.com', subject: 'Commande confirmée', html });
```
| Prop | Type | Description |
|------|------|-------------|
| `preview` | `string` | Texte de prévisualisation (snippet email) |
| `title` | `string` | Titre affiché en haut du corps |
| `children` | `ReactNode` | Contenu de l'email |
| `companyName` | `string` | Nom affiché si pas de logo (défaut : `ZEN_NAME` ou `ZEN`) |
| `logoURL` | `string` | URL du logo (défaut : `ZEN_EMAIL_LOGO`) |
| `supportSection` | `boolean` | Afficher le lien support dans le footer (défaut : `false`) |
| `supportEmail` | `string` | Email support (défaut : `ZEN_SUPPORT_EMAIL`) |
---
## Créer un template depuis une feature
Les templates vivent **avec leur feature**, pas dans ce répertoire.
```jsx
// src/features/auth/emails/WelcomeEmail.js
import { BaseLayout } from '@zen/core/email/templates';
import { Text, Button } from '@react-email/components';
export const WelcomeEmail = ({ name, loginUrl }) => (
<BaseLayout preview={`Bienvenue, ${name}`} title="Bienvenue !">
<Text>Bonjour {name}, votre compte est prêt.</Text>
<Button href={loginUrl}>Se connecter</Button>
</BaseLayout>
);
```
```js
// src/features/auth/emails/sendWelcome.js
import { render } from '@react-email/render';
import { sendEmail } from '@zen/core/email';
import { WelcomeEmail } from './WelcomeEmail.js';
export async function sendWelcomeEmail({ to, name, loginUrl }) {
const html = await render(<WelcomeEmail name={name} loginUrl={loginUrl} />);
return sendEmail({ to, subject: 'Bienvenue !', html });
}
```
---
## Gestion des erreurs
`sendEmail` et `sendBatchEmails` ne lèvent jamais d'exception — toute erreur est capturée, loguée via `fail()`, et retournée dans `{ success: false, error }`. L'appelant vérifie `result.success`.
+225
View File
@@ -0,0 +1,225 @@
# Payments Framework
Ce répertoire fournit un **wrapper autour de [Stripe](https://stripe.com)** pour la gestion des paiements : sessions de checkout, intents, clients, remboursements et webhooks.
---
## Structure
```
src/core/payments/
├── index.js re-export
└── stripe.js wrapper Stripe
```
---
## Import
```js
import {
isEnabled,
getPublishableKey,
createCheckoutSession,
createPaymentIntent,
getCheckoutSession,
getPaymentIntent,
verifyWebhookSignature,
createCustomer,
getOrCreateCustomer,
listPaymentMethods,
createRefund,
} from '@zen/core/payments';
```
---
## Variables d'environnement
| Variable | Obligatoire | Description |
|----------|-------------|-------------|
| `STRIPE_SECRET_KEY` | Oui | Clé secrète Stripe (côté serveur) |
| `STRIPE_PUBLISHABLE_KEY` | Oui | Clé publique Stripe (côté client) |
| `STRIPE_WEBHOOK_SECRET` | Pour les webhooks | Secret de signature des webhooks Stripe |
| `ZEN_CURRENCY` | Non | Devise par défaut pour les payment intents (défaut : `cad`) |
---
## API
### `isEnabled()`
Retourne `true` si `STRIPE_SECRET_KEY` et `STRIPE_PUBLISHABLE_KEY` sont définis. Utiliser pour conditionner l'affichage des fonctionnalités de paiement.
```js
if (isEnabled()) {
// afficher le bouton de paiement
}
```
---
### `getPublishableKey()`
Retourne la clé publique Stripe, ou `null` si absente. Passer au client pour initialiser Stripe.js ou `@stripe/react-stripe-js`.
```js
const key = getPublishableKey();
```
---
### `createCheckoutSession(options)`
Crée une session Stripe Checkout. Retourne la session Stripe.
```js
const session = await createCheckoutSession({
lineItems: [{ price: 'price_xxx', quantity: 1 }],
successUrl: 'https://example.com/success',
cancelUrl: 'https://example.com/cancel',
customerEmail: 'user@example.com',
mode: 'payment',
});
// Rediriger l'utilisateur vers session.url
```
| Paramètre | Type | Description |
|-----------|------|-------------|
| `lineItems` | `object[]` | Lignes de commande Stripe |
| `successUrl` | `string` | URL de retour après paiement réussi |
| `cancelUrl` | `string` | URL de retour après annulation |
| `customerEmail` | `string` | Email pré-rempli dans le formulaire (optionnel) |
| `clientReferenceId` | `string` | Identifiant interne pour rapprochement (optionnel) |
| `metadata` | `object` | Métadonnées Stripe (optionnel) |
| `mode` | `string` | `'payment'`, `'subscription'` ou `'setup'` (défaut : `'payment'`) |
---
### `createPaymentIntent(options)`
Crée un PaymentIntent Stripe. Retourne le PaymentIntent.
```js
const intent = await createPaymentIntent({
amount: 4999, // en centimes
currency: 'eur',
metadata: { orderId: '123' },
});
```
| Paramètre | Type | Description |
|-----------|------|-------------|
| `amount` | `number` | Montant en centimes |
| `currency` | `string` | Devise ISO (défaut : `ZEN_CURRENCY` ou `cad`) |
| `metadata` | `object` | Métadonnées Stripe (optionnel) |
| `automaticPaymentMethods` | `object` | Config des méthodes de paiement (défaut : `{ enabled: true }`) |
---
### `getCheckoutSession(sessionId)`
Récupère une session Checkout par son identifiant. À utiliser dans la route `successUrl` pour confirmer le paiement.
```js
const session = await getCheckoutSession(sessionId);
```
---
### `getPaymentIntent(paymentIntentId)`
Récupère un PaymentIntent par son identifiant.
```js
const intent = await getPaymentIntent(paymentIntentId);
```
---
### `verifyWebhookSignature(payload, signature)`
Vérifie la signature d'un webhook Stripe et retourne l'événement. Lève une erreur si la signature est invalide ou si `STRIPE_WEBHOOK_SECRET` est absent.
```js
// Next.js Route Handler
export async function POST(req) {
const payload = await req.text();
const signature = req.headers.get('stripe-signature');
let event;
try {
event = await verifyWebhookSignature(payload, signature);
} catch (err) {
return new Response('Signature invalide', { status: 400 });
}
if (event.type === 'checkout.session.completed') {
// traiter la commande
}
return new Response('OK');
}
```
Le `payload` doit être le corps brut de la requête (non parsé).
---
### `createCustomer(options)`
Crée un client Stripe. Retourne le client.
```js
const customer = await createCustomer({
email: 'user@example.com',
name: 'Jean Dupont',
metadata: { userId: '42' },
});
```
---
### `getOrCreateCustomer(email, defaultData)`
Retourne le client Stripe existant pour cet email, ou en crée un nouveau. Utilise une clé d'idempotence dérivée de l'email pour limiter les doublons en cas d'appels concurrents.
```js
const customer = await getOrCreateCustomer('user@example.com', {
name: 'Jean Dupont',
metadata: { userId: '42' },
});
```
---
### `listPaymentMethods(customerId, type)`
Retourne la liste des méthodes de paiement d'un client.
```js
const methods = await listPaymentMethods(customer.id, 'card');
```
Le paramètre `type` est optionnel (défaut : `'card'`).
---
### `createRefund(options)`
Crée un remboursement. Retourne le remboursement Stripe.
```js
const refund = await createRefund({
paymentIntentId: 'pi_xxx',
amount: 1000, // partiel, en centimes (optionnel — total si absent)
reason: 'requested_by_customer', // optionnel
});
```
| Paramètre | Type | Description |
|-----------|------|-------------|
| `paymentIntentId` | `string` | Identifiant du PaymentIntent à rembourser |
| `amount` | `number` | Montant en centimes (optionnel, remboursement total si absent) |
| `reason` | `string` | Raison Stripe : `duplicate`, `fraudulent`, `requested_by_customer` (optionnel) |
+142
View File
@@ -0,0 +1,142 @@
# PDF Framework
Ce répertoire re-exporte les primitives de [`@react-pdf/renderer`](https://react-pdf.org) et fournit un utilitaire de nommage de fichiers. Il ne contient aucun template métier — les features créent leurs propres documents et utilisent ce module pour le rendu.
---
## Structure
```
src/core/pdf/
└── index.js re-exports @react-pdf/renderer + getFilename
```
---
## Import
```js
import {
renderToBuffer,
Document,
Page,
View,
Text,
Image,
Link,
StyleSheet,
Font,
getFilename,
} from '@zen/core/pdf';
```
---
## API
### `renderToBuffer(element)`
Rend un document React PDF en `Buffer`. Retourne une `Promise<Buffer>`.
```js
import { renderToBuffer, Document, Page, Text } from '@zen/core/pdf';
const buffer = await renderToBuffer(
<Document>
<Page>
<Text>Bonjour</Text>
</Page>
</Document>
);
```
Utiliser ce buffer pour servir le PDF en réponse HTTP ou l'écrire sur disque.
---
### `getFilename(prefix, identifier, date?)`
Retourne un nom de fichier normalisé pour un PDF.
```js
getFilename('invoice', '12345')
// 'invoice-12345-2024-01-15.pdf'
getFilename('receipt', 'ORD-99', new Date('2024-06-01'))
// 'receipt-ORD-99-2024-06-01.pdf'
```
| Paramètre | Type | Description |
|-----------|------|-------------|
| `prefix` | `string` | Type de document (`invoice`, `receipt`, etc.) |
| `identifier` | `string` | Identifiant unique (numéro de commande, ID, etc.) |
| `date` | `Date` | Date du document (défaut : aujourd'hui) |
---
### Primitives re-exportées
Toutes les primitives de `@react-pdf/renderer` sont disponibles directement depuis `@zen/core/pdf` :
| Export | Description |
|--------|-------------|
| `Document` | Racine d'un document PDF |
| `Page` | Page du document |
| `View` | Conteneur (équivalent `div`) |
| `Text` | Bloc de texte |
| `Image` | Image (URL ou base64) |
| `Link` | Lien hypertexte |
| `StyleSheet` | Création de styles (similaire à `StyleSheet.create` React Native) |
| `Font` | Enregistrement de polices personnalisées |
---
## Créer un template depuis une feature
Les templates vivent **avec leur feature**, pas dans ce répertoire.
```jsx
// src/features/orders/pdf/InvoiceDocument.js
import { Document, Page, View, Text, StyleSheet } from '@zen/core/pdf';
const styles = StyleSheet.create({
page: { padding: 40 },
title: { fontSize: 20, marginBottom: 16 },
});
export const InvoiceDocument = ({ order }) => (
<Document>
<Page style={styles.page}>
<Text style={styles.title}>Facture #{order.number}</Text>
<Text>{order.customerName}</Text>
</Page>
</Document>
);
```
```js
// src/features/orders/pdf/sendInvoice.js
import { renderToBuffer, getFilename } from '@zen/core/pdf';
import { InvoiceDocument } from './InvoiceDocument.js';
export async function generateInvoicePdf(order) {
const buffer = await renderToBuffer(<InvoiceDocument order={order} />);
const filename = getFilename('invoice', order.number);
return { buffer, filename };
}
```
```js
// Next.js Route Handler
export async function GET(req, { params }) {
const order = await getOrder(params.id);
const { buffer, filename } = await generateInvoicePdf(order);
return new Response(buffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${filename}"`,
},
});
}
```
+153
View File
@@ -0,0 +1,153 @@
# Themes
Ce répertoire gère le thème clair/sombre de l'interface. Il expose des utilitaires client pour lire, appliquer et réagir au thème, ainsi qu'un script d'initialisation à injecter dans `<head>` pour éviter le flash au chargement.
---
## Structure
```
src/core/themes/
└── index.js THEME_INIT_SCRIPT, getStoredTheme, applyTheme, getThemeIcon, ThemeWatcher, useTheme
```
---
## Import
```js
import {
THEME_INIT_SCRIPT,
getStoredTheme,
applyTheme,
getThemeIcon,
ThemeWatcher,
useTheme,
} from '@zen/core/themes';
```
Tous les exports sont marqués `'use client'`.
---
## API
### `THEME_INIT_SCRIPT`
Script inline à injecter dans `<head>` avant le premier rendu. Il lit `localStorage` et applique la classe `dark` sur `<html>` immédiatement, ce qui évite le flash de thème (FOUC).
```jsx
// app/layout.js
import { THEME_INIT_SCRIPT } from '@zen/core/themes';
export default function RootLayout({ children }) {
return (
<html>
<head>
<script dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }} />
</head>
<body>{children}</body>
</html>
);
}
```
---
### `getStoredTheme()`
Lit le thème enregistré dans `localStorage`. Retourne `'light'`, `'dark'` ou `'auto'`.
```js
const theme = getStoredTheme(); // 'light' | 'dark' | 'auto'
```
---
### `applyTheme(theme)`
Applique un thème en modifiant `document.documentElement` et en mettant à jour `localStorage`. En mode `'auto'`, retire la préférence stockée et suit le système.
| Valeur | Comportement |
|--------|-------------|
| `'light'` | Retire la classe `dark`, stocke `'light'` |
| `'dark'` | Ajoute la classe `dark`, stocke `'dark'` |
| `'auto'` | Retire la valeur stockée, suit `prefers-color-scheme` |
```js
applyTheme('dark');
applyTheme('auto');
```
---
### `getThemeIcon(theme, systemIsDark)`
Retourne le composant icône correspondant au thème actuel.
| Thème | `systemIsDark` | Icône retournée |
|-------|----------------|-----------------|
| `'light'` | - | `Sun01Icon` |
| `'dark'` | - | `Moon02Icon` |
| `'auto'` | `true` | `MoonCloudIcon` |
| `'auto'` | `false` | `SunCloud01Icon` |
```jsx
const Icon = getThemeIcon(theme, systemIsDark);
return <Icon />;
```
---
### `ThemeWatcher`
Composant sans rendu qui écoute les changements de `prefers-color-scheme`. Si aucune préférence n'est stockée dans `localStorage`, il met à jour la classe `dark` automatiquement quand le système change.
```jsx
// Placer une fois dans le layout racine, après THEME_INIT_SCRIPT.
import { ThemeWatcher } from '@zen/core/themes';
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeWatcher />
{children}
</body>
</html>
);
}
```
---
### `useTheme()`
Hook qui expose le thème actuel et une fonction de basculement cyclique. Synchronise l'état avec `localStorage` et le système au montage.
Retourne `{ theme, toggle, systemIsDark }`.
| Propriété | Type | Description |
|-----------|------|-------------|
| `theme` | `'light' \| 'dark' \| 'auto'` | Thème actif |
| `toggle` | `() => void` | Passe au thème suivant dans le cycle |
| `systemIsDark` | `boolean` | Indique si le système est en mode sombre |
Le cycle de basculement dépend de la préférence système :
- Système clair : `auto` -> `dark` -> `light` -> `auto`
- Système sombre : `auto` -> `light` -> `dark` -> `auto`
```jsx
import { useTheme, getThemeIcon } from '@zen/core/themes';
export function ThemeToggle() {
const { theme, toggle, systemIsDark } = useTheme();
const Icon = getThemeIcon(theme, systemIsDark);
return (
<button onClick={toggle}>
<Icon />
</button>
);
}
```
+145
View File
@@ -0,0 +1,145 @@
# Toast
Ce répertoire fournit un **système de notifications toast** basé sur un contexte React. Il expose un provider, un hook, et un conteneur à placer dans le layout. Les features utilisent le hook pour déclencher des notifications.
---
## Structure
```
src/core/toast/
├── index.js Toast, ToastProvider, useToast, ToastContainer
├── ToastContext.js contexte, provider, hook useToast
├── ToastContainer.js conteneur à monter dans le layout
└── Toast.js composant d'affichage individuel
```
---
## Import
```js
import { ToastProvider, useToast, ToastContainer } from '@zen/core/toast';
```
---
## Mise en place
Entourer le layout avec `ToastProvider` et y placer `ToastContainer`.
```jsx
import { ToastProvider, ToastContainer } from '@zen/core/toast';
export default function RootLayout({ children }) {
return (
<ToastProvider>
{children}
<ToastContainer />
</ToastProvider>
);
}
```
`useToast` lève une erreur si appelé hors du `ToastProvider`.
---
## API
### `useToast()`
Hook qui expose les méthodes et l'état courant des toasts.
```js
const { success, error, warning, info, addToast, removeToast, clearAllToasts } = useToast();
```
**Méthodes de raccourci**
```js
success('Modifications enregistrées.');
error('La connexion a échoué.');
warning('Session sur le point d'expirer.');
info('Une mise à jour est disponible.');
```
Chaque méthode accepte un message et un objet `options` optionnel pour surcharger les paramètres par défaut.
```js
success('Fichier importé.', { duration: 3000, dismissible: false });
```
Toutes retournent l'`id` du toast créé.
**`addToast(toast)`**
Crée un toast à partir d'un objet complet.
```js
const id = addToast({
type: 'success',
message: 'Profil mis à jour.',
title: 'Enregistré',
duration: 4000,
dismissible: true,
});
```
| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `type` | `'success' \| 'error' \| 'warning' \| 'info'` | `'info'` | Variante visuelle |
| `message` | `string` | — | Corps du toast |
| `title` | `string` | Selon `type` | Titre affiché (optionnel) |
| `duration` | `number` | `5000` | Durée en ms avant disparition automatique. `0` pour désactiver |
| `dismissible` | `boolean` | `true` | Afficher le bouton de fermeture |
Durées par défaut selon le type : `error` → 7000 ms, `warning` → 6000 ms, `success` / `info` → 5000 ms.
**`removeToast(id)`**
Supprime un toast immédiatement par son `id`.
**`clearAllToasts()`**
Supprime tous les toasts actifs.
---
## ToastContainer
Composant à placer une seule fois dans le layout. Affiche les toasts en bas à droite de l'écran, empilés avec une animation de survol.
```jsx
<ToastContainer maxToasts={5} />
```
| Prop | Type | Défaut | Description |
|------|------|--------|-------------|
| `maxToasts` | `number` | `5` | Nombre maximum de toasts visibles simultanément |
Au survol du toast le plus récent, la pile se déploie pour afficher tous les toasts à leur taille réelle.
---
## Déclencher un toast depuis une feature
```js
// src/features/auth/actions/login.js
import { useToast } from '@zen/core/toast';
export function useLoginActions() {
const { success, error } = useToast();
async function login(credentials) {
const result = await loginRequest(credentials);
if (result.success) {
success('Connexion réussie.');
} else {
error('Identifiants incorrects.');
}
}
return { login };
}
```
+275
View File
@@ -0,0 +1,275 @@
# Users
Ce répertoire gère les utilisateurs, l'authentification par identifiants, les sessions, les rôles et les permissions. Il constitue la couche de données auth du projet : les features l'appellent, il ne connaît pas les features.
---
## Structure
```
src/core/users/
├── index.js re-exports publics
├── auth.js register, login, mot de passe, vérification email
├── session.js création, validation, suppression de sessions
├── queries.js lecture et mise à jour des utilisateurs
├── roles.js CRUD des rôles, assignation aux utilisateurs
├── permissions.js hasPermission, getUserPermissions
├── constants.js PERMISSIONS, PERMISSION_DEFINITIONS, getPermissionGroups
├── verifications.js tokens de vérification email et de réinitialisation
├── emailChange.js tokens de changement d'adresse email
├── password.js hashPassword, verifyPassword, generateToken, generateId
└── db.js helpers internes
```
---
## Import
```js
import {
register, login, requestPasswordReset, resetPassword, verifyUserEmail, updateUser,
createSession, validateSession, deleteSession, deleteUserSessions, refreshSession,
getUserById, getUserByEmail, countUsers, listUsers, updateUserById,
createRole, updateRole, deleteRole, listRoles, getRoleById,
getUserRoles, assignUserRole, revokeUserRole,
hasPermission, getUserPermissions,
PERMISSIONS, PERMISSION_DEFINITIONS, getPermissionGroups,
hashPassword, verifyPassword, generateToken, generateId,
} from '@zen/core/users';
```
---
## API
### `register(userData, options?)`
Crée un compte utilisateur avec vérification des contraintes mot de passe. Le premier utilisateur enregistré reçoit le rôle `admin`. Retourne `{ user, verificationToken }`.
```js
const { user, verificationToken } = await register(
{ email: 'alice@example.com', password: 'Secret1', name: 'Alice' },
{
onEmailVerification: async (email, token) => {
await sendVerificationEmail({ to: email, token });
},
}
);
```
| Paramètre | Type | Description |
|-----------|------|-------------|
| `email` | `string` | Adresse email (max 254 caractères) |
| `password` | `string` | Mot de passe (8-128 caractères, au moins 1 majuscule, 1 minuscule, 1 chiffre) |
| `name` | `string` | Nom affiché (max 100 caractères) |
| `onEmailVerification` | `async (email, token) => void` | Callback pour envoyer le token de vérification |
---
### `login(credentials, sessionOptions?)`
Vérifie les identifiants et crée une session. Retourne `{ user, session }`. Lève une erreur si les identifiants sont incorrects.
```js
const { user, session } = await login(
{ email: 'alice@example.com', password: 'Secret1' },
{ ipAddress: req.ip, userAgent: req.headers['user-agent'] }
);
```
---
### `requestPasswordReset(email)`
Génère un token de réinitialisation (expire dans 1 heure). Retourne `{ success: true, token }` même si l'email est inconnu, pour éviter l'énumération.
```js
const { token } = await requestPasswordReset('alice@example.com');
```
---
### `resetPassword(data, options?)`
Valide le token et met à jour le mot de passe. Retourne `{ success: true }`.
```js
await resetPassword(
{ email: 'alice@example.com', token, newPassword: 'NewSecret1' },
{ onPasswordChanged: async (email) => { /* envoyer confirmation */ } }
);
```
---
### `verifyUserEmail(userId)`
Marque l'email de l'utilisateur comme vérifié.
```js
await verifyUserEmail(user.id);
```
---
### `updateUser(userId, data)`
Met à jour les champs autorisés du profil : `name`, `image`, `language`.
```js
await updateUser(user.id, { name: 'Alice Martin' });
```
---
### `createSession(userId, options?)`
Crée une session valide 30 jours. Retourne l'objet session.
```js
const session = await createSession(user.id, { ipAddress: '127.0.0.1', userAgent: '...' });
```
---
### `validateSession(token)`
Valide un token de session. Renouvelle automatiquement la session si elle expire dans moins de 20 jours. Retourne `{ session, user, sessionRefreshed }` ou `null`.
```js
const result = await validateSession(token);
if (!result) {
// session expirée ou invalide
}
```
---
### `deleteSession(token)` / `deleteUserSessions(userId)`
Supprime une session ou toutes les sessions d'un utilisateur.
```js
await deleteSession(token);
await deleteUserSessions(user.id);
```
---
### `getUserById(id)` / `getUserByEmail(email)`
Récupère un utilisateur par son id ou son email.
```js
const user = await getUserById('abc123');
const user = await getUserByEmail('alice@example.com');
```
---
### `listUsers(options?)`
Liste les utilisateurs avec pagination et tri. Retourne `{ users, pagination }`.
```js
const { users, pagination } = await listUsers({ page: 1, limit: 20, sortBy: 'created_at', sortOrder: 'desc' });
```
| Paramètre | Défaut | Description |
|-----------|--------|-------------|
| `page` | `1` | Page courante |
| `limit` | `10` | Résultats par page (max 100) |
| `sortBy` | `'created_at'` | Colonne de tri (`id`, `email`, `name`, `role`, `email_verified`, `created_at`) |
| `sortOrder` | `'desc'` | `'asc'` ou `'desc'` |
---
### `updateUserById(id, fields)`
Met à jour les champs autorisés d'un utilisateur : `name`, `role`, `email_verified`, `image`, `language`.
```js
await updateUserById(user.id, { role: 'editor', email_verified: true });
```
---
### Rôles
```js
const roles = await listRoles();
const role = await getRoleById(id);
const role = await createRole({ name: 'Éditeur', description: 'Peut publier du contenu', color: '#3b82f6' });
await updateRole(roleId, {
name: 'Éditeur senior',
permissionKeys: [PERMISSIONS.CONTENT_EDIT, PERMISSIONS.CONTENT_PUBLISH],
});
await deleteRole(roleId); // impossible sur les rôles système
const userRoles = await getUserRoles(userId);
await assignUserRole(userId, roleId);
await revokeUserRole(userId, roleId);
```
Les rôles système (`is_system = true`) ne peuvent pas être renommés ni supprimés.
---
### Permissions
```js
const canEdit = await hasPermission(userId, PERMISSIONS.CONTENT_EDIT);
const keys = await getUserPermissions(userId);
```
`PERMISSIONS` contient toutes les clés disponibles. `PERMISSION_DEFINITIONS` expose le label, la description et le groupe de chaque permission. `getPermissionGroups()` retourne les permissions regroupées par `group_name`.
| Groupe | Clés |
|--------|------|
| Administration | `admin.access` |
| Contenu | `content.view`, `content.create`, `content.edit`, `content.delete`, `content.publish` |
| Médias | `media.view`, `media.upload`, `media.delete` |
| Utilisateurs | `users.view`, `users.edit`, `users.delete` |
| Rôles | `roles.view`, `roles.manage` |
| Paramètres | `settings.view`, `settings.manage` |
---
### Changement d'email
```js
import { createEmailChangeToken, verifyEmailChangeToken, applyEmailChange } from '@zen/core/users/emailChange';
const token = await createEmailChangeToken(userId, 'new@example.com');
// Plus tard, lors de la confirmation :
const result = await verifyEmailChangeToken(token);
if (result) {
await applyEmailChange(result.userId, result.newEmail);
}
```
Le token expire dans 24 heures. `verifyEmailChangeToken` retourne `null` si le token est invalide ou expiré.
---
## Gestion des erreurs
`register`, `login`, `resetPassword` lèvent des erreurs typées (`Error`) avec des messages en français. Les fonctions de requête (`getUserById`, etc.) retournent `null` si l'entrée n'existe pas. Les callbacks `onEmailVerification` et `onPasswordChanged` sont exécutés sans bloquer le flux principal : une erreur dans le callback est loguée mais n'interrompt pas l'opération.
---
## Tables utilisées
| Table | Description |
|-------|-------------|
| `zen_auth_users` | Comptes utilisateurs |
| `zen_auth_accounts` | Identifiants par provider (`credential`) |
| `zen_auth_sessions` | Sessions actives |
| `zen_auth_verifications` | Tokens de vérification email, reset mot de passe, changement email |
| `zen_auth_roles` | Rôles |
| `zen_auth_role_permissions` | Permissions associées aux rôles |
| `zen_auth_user_roles` | Rôles assignés aux utilisateurs |