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:
@@ -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
|
||||
```
|
||||
@@ -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`.
|
||||
@@ -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) |
|
||||
@@ -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}"`,
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -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 };
|
||||
}
|
||||
```
|
||||
@@ -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 |
|
||||
Reference in New Issue
Block a user