refactor: remove module system integration from admin and CLI
Removes all module-related logic from the admin dashboard, CLI database initialization, and AdminPages component: - Drop `initModules` call from `db init` CLI command and simplify the completion message to only reflect core feature tables - Remove `getModuleDashboardStats` and module page routing from admin stats actions and update usage documentation accordingly - Simplify `AdminPagesClient` to remove module page loading, lazy components, and module-specific props (`moduleStats`, `modulePageInfo`, `routeInfo`, `enabledModules`)
This commit is contained in:
+387
@@ -0,0 +1,387 @@
|
||||
# ZEN — Plan du projet
|
||||
|
||||
> Un CMS Next.js construit sur l'essentiel, rien de plus, rien de moins.
|
||||
|
||||
ZEN est un système de gestion de contenu (CMS) pour Next.js. Il s'installe automatiquement dans n'importe quel projet Next.js via :
|
||||
|
||||
```bash
|
||||
npx @zen/start
|
||||
```
|
||||
|
||||
Le package principal est `@zen/core`. Il fournit toute l'infrastructure nécessaire pour gérer un site : authentification, base de données, stockage, courriels, paiements, PDF, tâches planifiées, notifications. Chaque fonctionnalité est indépendante — les cores ne se connaissent pas entre eux, mais le reste du CMS s'appuie sur chacun d'eux.
|
||||
|
||||
---
|
||||
|
||||
## Principes directeurs
|
||||
|
||||
**Core purity** — Chaque core contient uniquement du code propre à son domaine. Aucune logique métier, aucune dépendance vers un autre core.
|
||||
|
||||
**Minimal by default** — Pas de fonctionnalité superflue. Si ce n'est pas nécessaire, ça n'existe pas.
|
||||
|
||||
**Sécuritaire par défaut** — Requêtes paramétrées, protection CSRF, limitation de débit, validation en entrée, erreurs opaques vers le client.
|
||||
|
||||
**Performant** — Connexions en pool, cache HTTP, génération différée des services.
|
||||
|
||||
---
|
||||
|
||||
## Structure du projet
|
||||
|
||||
```
|
||||
src/
|
||||
core/ # Infrastructure fondamentale — la base de tout
|
||||
features/ # Fonctionnalités du CMS utilisant les cores
|
||||
shared/ # Utilitaires, composants et styles partagés
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `src/core` — Les piliers du CMS
|
||||
|
||||
Chaque core est une brique indépendante. Il n'existe qu'une seule façon de faire chaque chose dans ZEN : passer par le core concerné. L'ensemble du CMS, des features aux modules, repose sur ces cores.
|
||||
|
||||
---
|
||||
|
||||
### API
|
||||
|
||||
**`@zen/core/api`**
|
||||
|
||||
L'API est le point d'entrée unique de toutes les requêtes HTTP du CMS. Que ce soit depuis l'Admin, le front-end du site ou un module tiers, tout passe par là. Il n'existe aucune autre route API dans ZEN.
|
||||
|
||||
Toutes les requêtes arrivent via le catch-all Next.js `app/zen/api/[...path]/route.js`. Le router les dispatche vers le bon handler selon le chemin et la méthode HTTP.
|
||||
|
||||
**Ce qu'il fait :**
|
||||
- Routage dynamique avec paramètres nommés (`:id`) et wildcards (`/**`)
|
||||
- Protection CSRF automatique sur toutes les requêtes mutantes (POST, PUT, PATCH, DELETE)
|
||||
- Limitation de débit par IP avec des préréglages par action (login, register, api)
|
||||
- Injection de session dans le contexte de chaque handler
|
||||
- Trois niveaux d'accès : `public`, `user` (session requise), `admin` (rôle admin requis)
|
||||
- Enregistrement dynamique des routes par les features
|
||||
- Réponses standardisées via `apiSuccess()` et `apiError()`
|
||||
|
||||
**Règle absolue :** Toute route API du CMS doit être enregistrée dans ce router. Jamais de `route.js` parallèle.
|
||||
|
||||
---
|
||||
|
||||
### Database
|
||||
|
||||
**`@zen/core/database`**
|
||||
|
||||
La couche d'accès à la base de données PostgreSQL. Toute l'application communique avec la base de données uniquement via ce core — jamais directement.
|
||||
|
||||
**Ce qu'il fait :**
|
||||
- Pool de connexions PostgreSQL (max 20 clients, reconnexion automatique)
|
||||
- Fonctions de requête de bas niveau : `query()`, `queryOne()`, `queryAll()`, `transaction()`
|
||||
- Helpers CRUD complets : `create()`, `find()`, `findOne()`, `findById()`, `update()`, `updateById()`, `delete()`, `deleteWhere()`, `count()`, `exists()`
|
||||
- Protection contre l'injection SQL : requêtes paramétrées `$1, $2, ...`, jamais de concaténation
|
||||
- Contrôle d'accès aux colonnes via liste blanche (`allowedColumns`) — empêche le mass assignment
|
||||
- Validation et échappement des identifiants SQL (noms de tables et colonnes)
|
||||
- Gestion SSL configurable : vérification complète en production, souple en développement
|
||||
- Erreurs opaques : seul le code SQLSTATE est exposé, jamais les détails internes
|
||||
|
||||
**Règle absolue :** Aucun code ailleurs dans le CMS ne communique directement avec la base de données. Tout passe par ce core.
|
||||
|
||||
---
|
||||
|
||||
### Email
|
||||
|
||||
**`@zen/core/email`**
|
||||
|
||||
L'unique système d'envoi de courriels du CMS. Alimenté par Resend. Toute l'application envoie ses courriels via ce core.
|
||||
|
||||
**Ce qu'il fait :**
|
||||
- Envoi de courriels simples : `sendEmail({ to, subject, html })`
|
||||
- Envoi en lot : `sendBatchEmails(emailArray)`
|
||||
- Expéditeur configurable (adresse, nom d'affichage)
|
||||
- Template de base React Email (`BaseLayout`) : logo, marque, pied de page, lien de support
|
||||
- Réponses standardisées `{ success, data, error }`
|
||||
|
||||
**Templates fournis :**
|
||||
- `BaseLayout` — Enveloppe visuelle commune à tous les courriels du CMS (logo, couleurs, pied de page)
|
||||
|
||||
**Règle absolue :** Tout courriel envoyé par le CMS passe par ce core.
|
||||
|
||||
---
|
||||
|
||||
### Cron
|
||||
|
||||
**`@zen/core/cron`**
|
||||
|
||||
Le registre central de toutes les tâches planifiées. Chaque tâche récurrente de l'application s'enregistre ici — jamais en dehors.
|
||||
|
||||
**Ce qu'il fait :**
|
||||
- Planification de tâches avec expressions cron standard (`schedule(name, expression, handler)`)
|
||||
- Support des fuseaux horaires (défaut : `ZEN_TIMEZONE`)
|
||||
- Survie aux hot reloads Next.js via stockage global (`Symbol.for`)
|
||||
- Remplacement automatique si une tâche du même nom est enregistrée deux fois
|
||||
- Déclenchement manuel : `trigger(name)` pour exécuter une tâche immédiatement
|
||||
- Introspection : `getJobs()`, `getStatus()`, `isRunning(name)`
|
||||
- Gestion des erreurs : un échec de tâche ne plante pas le scheduler
|
||||
|
||||
**Règle absolue :** Toutes les tâches cron de l'application sont enregistrées via ce core.
|
||||
|
||||
---
|
||||
|
||||
### Storage
|
||||
|
||||
**`@zen/core/storage`**
|
||||
|
||||
La gestion complète du stockage de fichiers, compatible S3 (Cloudflare R2 ou Backblaze B2). Toute l'application lit et écrit des fichiers via ce core.
|
||||
|
||||
**Ce qu'il fait :**
|
||||
- Upload, téléchargement, suppression, copie et déplacement de fichiers
|
||||
- Upload d'images avec cache longue durée (`max-age=31536000`)
|
||||
- Suppression en lot optimisée (S3 batch delete)
|
||||
- URLs pré-signées pour accès direct (GET ou PUT)
|
||||
- Liste paginée des fichiers avec préfixe et continuation token
|
||||
- Signature AWS Signature V4 pour toutes les requêtes
|
||||
- Protection contre le path traversal (`..`, `.`, segments vides, null bytes)
|
||||
- Contrôle d'accès via policies : préfixes publics vs chemins protégés (session + rôle)
|
||||
- Headers de sécurité : `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`
|
||||
- Téléchargement forcé pour les fichiers non-images (prévient l'exécution en navigateur)
|
||||
- Utilitaires : validation de type, validation de taille, nommage unique, extension, MIME type
|
||||
|
||||
**Règle absolue :** Tout accès fichier passe par ce core.
|
||||
|
||||
---
|
||||
|
||||
### Payments
|
||||
|
||||
**`@zen/core/payments`**
|
||||
|
||||
L'intégration Stripe pour les paiements. Tout ce qui touche à la facturation ou aux transactions dans l'application passe par ce core.
|
||||
|
||||
**Ce qu'il fait :**
|
||||
- Sessions de checkout Stripe (`createCheckoutSession`)
|
||||
- PaymentIntents pour paiements personnalisés (`createPaymentIntent`)
|
||||
- Gestion des clients Stripe (`createCustomer`, `getOrCreateCustomer`)
|
||||
- Récupération des sessions et intentions de paiement
|
||||
- Listing des moyens de paiement d'un client
|
||||
- Vérification des webhooks Stripe (`verifyWebhookSignature`)
|
||||
- Remboursements (`createRefund`)
|
||||
- Initialisation paresseuse du client Stripe (seulement si configuré)
|
||||
- Clé publiable exposée pour le front-end (`getPublishableKey`)
|
||||
|
||||
---
|
||||
|
||||
### PDF
|
||||
|
||||
**`@zen/core/pdf`**
|
||||
|
||||
La génération de fichiers PDF à partir de composants React. Tout PDF produit par l'application passe par ce core.
|
||||
|
||||
**Ce qu'il fait :**
|
||||
- Rendu de documents PDF depuis des composants React (`renderToBuffer`)
|
||||
- Réexporte l'API complète de `@react-pdf/renderer` : `Document`, `Page`, `View`, `Text`, `Image`, `Link`, `StyleSheet`, `Font`
|
||||
- Utilitaire de nommage : `getFilename(prefix, identifier, date?)` → `"invoice-12345-2024-01-15.pdf"`
|
||||
|
||||
---
|
||||
|
||||
### Toast
|
||||
|
||||
**`@zen/core/toast`**
|
||||
|
||||
Le système de notifications visuelles de l'application. Que ce soit dans l'Admin ou sur le front-end, c'est l'unique système de toast à utiliser.
|
||||
|
||||
**Ce qu'il fait :**
|
||||
- Contexte React via `<ToastProvider>` et `useToast()`
|
||||
- Quatre types de notifications : `success`, `error`, `warning`, `info`
|
||||
- Disparition automatique avec durées configurables par type
|
||||
- Animation de sortie (fade-out 300ms)
|
||||
- Flag `dismissible` par notification
|
||||
- `<ToastContainer>` pour le rendu dans l'arbre React
|
||||
|
||||
**Règle absolue :** Un seul système de toast dans toute l'application — celui-ci.
|
||||
|
||||
---
|
||||
|
||||
## `src/features` — Les fonctionnalités du CMS
|
||||
|
||||
Les features utilisent les cores pour implémenter les fonctionnalités centrales du CMS. Elles ont accès à l'API, à la base de données, aux courriels, etc.
|
||||
|
||||
---
|
||||
|
||||
### Auth
|
||||
|
||||
**`@zen/core/auth`**
|
||||
|
||||
Le système d'authentification du CMS. Toute authentification d'utilisateur dans le site passe par ici.
|
||||
|
||||
**Ce qu'il fait :**
|
||||
- Inscription et connexion d'utilisateurs
|
||||
- Hachage des mots de passe avec `scrypt` (natif Node.js) + sel aléatoire + comparaison résistante aux timing attacks
|
||||
- Gestion de sessions : création, validation, suppression, rafraîchissement automatique (<20 jours → 30 jours)
|
||||
- Vérification d'adresse courriel par token
|
||||
- Réinitialisation de mot de passe par lien sécurisé
|
||||
- Middleware de protection de routes : `protect()`, `checkAuth()`, `requireRole()`
|
||||
- Server Actions : `loginAction`, `registerAction`, `logoutAction`, `forgotPasswordAction`, `resetPasswordAction`, `verifyEmailAction`
|
||||
- Tables gérées : `zen_auth_users`, `zen_auth_sessions`, `zen_auth_email_verifications`, `zen_auth_password_resets`
|
||||
|
||||
**Règle absolue :** Toute authentification de site passe par cette feature.
|
||||
|
||||
---
|
||||
|
||||
### Admin
|
||||
|
||||
**`@zen/core/admin`**
|
||||
|
||||
L'interface d'administration centrale. Tableau de bord visuel pour gérer le site et ses modules.
|
||||
|
||||
**Ce qu'il fait :**
|
||||
- Protection des routes admin : `protectAdmin()`, `isAdmin()`
|
||||
- Pages catch-all pour l'interface admin (`AdminPagesClient`, `AdminPagesLayout`)
|
||||
- Navigation construite côté serveur (`buildNavigationSections`)
|
||||
- Gestion des utilisateurs depuis l'interface
|
||||
|
||||
**Navigation :**
|
||||
- Tableau de bord → `/admin/dashboard`
|
||||
- Utilisateurs → `/admin/users`
|
||||
|
||||
---
|
||||
|
||||
### Provider
|
||||
|
||||
**`@zen/core/provider`**
|
||||
|
||||
Le provider React racine du CMS. Il s'insère dans le layout du site et active tout ce dont le CMS a besoin côté client.
|
||||
|
||||
**Ce qu'il fait :**
|
||||
- Enveloppe l'application dans `<ToastProvider>` avec son `<ToastContainer>`
|
||||
- Un seul composant à poser dans le layout : `<ZenProvider>`
|
||||
|
||||
---
|
||||
|
||||
## `src/shared` — Utilitaires partagés
|
||||
|
||||
Tout ce qui est utile à travers le CMS sans appartenir à un core ou une feature.
|
||||
|
||||
### Composants UI (`src/shared/components/`)
|
||||
|
||||
Bibliothèque de composants React stylisés, utilisés dans l'Admin et les pages du CMS :
|
||||
|
||||
`Badge`, `StatusBadge`, `TypeBadge`, `Button`, `Card`, `Input`, `Loading`, `LoadingState`, `Modal`, `Pagination`, `Select`, `StatCard`, `Table`, `Textarea`, `MarkdownEditor`, `PasswordStrengthIndicator`, `FilterTabs`, `Breadcrumb`
|
||||
|
||||
### Utilitaires (`src/shared/lib/`, `src/shared/utils/`)
|
||||
|
||||
- **`appConfig`** — Lecture centralisée de la configuration (`getAppName`, `getAppConfig`, `getPublicBaseUrl`)
|
||||
- **`logger`** — Console stylisée pour les logs (`step`, `done`, `warn`, `fail`, `info`)
|
||||
- **`dates`** — Manipulation de dates en UTC (`formatDateForDisplay`, `getDaysBetween`, `isOverdue`, etc.)
|
||||
- **`metadata`** — Génération de métadonnées Next.js (`generateMetadata`, `generateTitle`, `generateRobots`)
|
||||
- **`rateLimit`** — Limitation de débit partagée (`checkRateLimit`) avec préréglages par action
|
||||
- **`currency`** — Formatage monétaire (`formatCurrency`, `getCurrencySymbol`)
|
||||
|
||||
### Icons (`src/shared/Icons.js`)
|
||||
|
||||
Bibliothèque de plus de 1000 icônes (style Untitled UI).
|
||||
|
||||
### Styles (`src/shared/styles/zen.css`)
|
||||
|
||||
Feuille de style CSS de base du CMS.
|
||||
|
||||
---
|
||||
|
||||
## Initialisation
|
||||
|
||||
Dans `instrumentation.js` du projet Next.js :
|
||||
|
||||
```js
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
const { initializeZen } = await import('@zen/core');
|
||||
await initializeZen();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Dans `app/layout.js` :
|
||||
|
||||
```jsx
|
||||
import { ZenProvider } from '@zen/core/provider';
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<ZenProvider>
|
||||
{children}
|
||||
</ZenProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Dans `app/zen/api/[...path]/route.js` :
|
||||
|
||||
```js
|
||||
export { GET, POST, PUT, PATCH, DELETE } from '@zen/core/zen/api';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Variables d'environnement
|
||||
|
||||
| Variable | Description |
|
||||
|---|---|
|
||||
| `ZEN_NAME` | Nom de l'application |
|
||||
| `ZEN_TIMEZONE` | Fuseau horaire IANA (défaut : `America/Toronto`) |
|
||||
| `ZEN_CURRENCY` | Code monétaire (défaut : `CAD`) |
|
||||
| `ZEN_CURRENCY_SYMBOL` | Symbole monétaire (défaut : `$`) |
|
||||
| `NEXT_PUBLIC_URL` | URL de base (production) |
|
||||
| `NEXT_PUBLIC_URL_DEV` | URL de base (développement) |
|
||||
| `ZEN_DATABASE_URL` | Chaîne de connexion PostgreSQL (production) |
|
||||
| `ZEN_DATABASE_URL_DEV` | Chaîne de connexion PostgreSQL (développement) |
|
||||
| `ZEN_DB_SSL_DISABLED` | Désactiver TLS (local uniquement) |
|
||||
| `ZEN_EMAIL_RESEND_APIKEY` | Clé API Resend |
|
||||
| `ZEN_EMAIL_FROM_ADDRESS` | Adresse d'expédition |
|
||||
| `ZEN_EMAIL_FROM_NAME` | Nom d'affichage de l'expéditeur |
|
||||
| `ZEN_STORAGE_PROVIDER` | `r2` ou `backblaze` |
|
||||
| `ZEN_STORAGE_ENDPOINT` | Endpoint S3-compatible |
|
||||
| `ZEN_STORAGE_ACCESS_KEY` | Clé d'accès stockage |
|
||||
| `ZEN_STORAGE_SECRET_KEY` | Clé secrète stockage |
|
||||
| `ZEN_STORAGE_BUCKET` | Nom du bucket |
|
||||
| `STRIPE_SECRET_KEY` | Clé secrète Stripe |
|
||||
| `STRIPE_PUBLISHABLE_KEY` | Clé publiable Stripe |
|
||||
| `STRIPE_WEBHOOK_SECRET` | Secret webhook Stripe |
|
||||
|
||||
---
|
||||
|
||||
## CLI base de données
|
||||
|
||||
```bash
|
||||
npx zen-db init # Créer toutes les tables
|
||||
npx zen-db test # Tester la connexion
|
||||
npx zen-db drop # Supprimer toutes les tables (confirmation requise)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Flux d'une requête
|
||||
|
||||
```
|
||||
Navigateur / Client
|
||||
↓
|
||||
app/zen/api/[...path]/route.js ← catch-all Next.js
|
||||
↓
|
||||
core/api — router.js ← CSRF, rate limit, auth
|
||||
↓
|
||||
Handler (feature) ← logique métier
|
||||
↓
|
||||
core/database, core/storage, ← accès aux ressources
|
||||
core/email, core/payments…
|
||||
↓
|
||||
Réponse standardisée ← apiSuccess() / apiError()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Résumé des règles absolues
|
||||
|
||||
| Domaine | Règle |
|
||||
|---|---|
|
||||
| API | Toutes les routes HTTP passent par `core/api`. Aucune autre route API. |
|
||||
| Base de données | Tout accès DB passe par `core/database`. Jamais de requêtes directes. |
|
||||
| Courriels | Tout envoi de courriel passe par `core/email`. |
|
||||
| Cron | Toutes les tâches planifiées s'enregistrent dans `core/cron`. |
|
||||
| Stockage | Tout accès fichier passe par `core/storage`. |
|
||||
| Notifications | Un seul système de toast dans toute l'app : `core/toast`. |
|
||||
| Authentification | Toute auth de site passe par `features/auth`. |
|
||||
+1
-12
@@ -69,18 +69,7 @@ async function runCLI() {
|
||||
const { initFeatures } = await import('../features/init.js');
|
||||
const featuresResult = await initFeatures();
|
||||
|
||||
// Module tables are initialized per-module, if present
|
||||
let modulesResult = { created: [], skipped: [] };
|
||||
try {
|
||||
const { initModules } = await import('../modules/init.js');
|
||||
modulesResult = await initModules();
|
||||
} catch {
|
||||
// Modules may not be present in all project setups — silently skip
|
||||
}
|
||||
|
||||
const totalCreated = featuresResult.created.length + modulesResult.created.length;
|
||||
const totalSkipped = featuresResult.skipped.length + modulesResult.skipped.length;
|
||||
done(`DB ready — ${totalCreated} tables created, ${totalSkipped} skipped`);
|
||||
done(`DB ready — ${featuresResult.created.length} tables created, ${featuresResult.skipped.length} skipped`);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,33 +2,25 @@
|
||||
* Admin Stats Actions
|
||||
* Server-side actions for core dashboard statistics
|
||||
*
|
||||
* Module-specific stats are handled by each module's dashboard actions.
|
||||
* See src/modules/{module}/dashboard/statsActions.js
|
||||
*
|
||||
* Usage in your Next.js app:
|
||||
*
|
||||
* ```javascript
|
||||
* // app/(admin)/admin/[...admin]/page.js
|
||||
* import { protectAdmin } from '@zen/core/admin';
|
||||
* import { getDashboardStats, getModuleDashboardStats } from '@zen/core/admin/actions';
|
||||
* import { getDashboardStats } from '@zen/core/admin/actions';
|
||||
* import { AdminPagesClient } from '@zen/core/admin/pages';
|
||||
*
|
||||
* export default async function AdminPage({ params }) {
|
||||
* const { user } = await protectAdmin();
|
||||
*
|
||||
* // Fetch core dashboard stats
|
||||
* const statsResult = await getDashboardStats();
|
||||
* const dashboardStats = statsResult.success ? statsResult.stats : null;
|
||||
*
|
||||
* // Fetch module dashboard stats (for dynamic widgets)
|
||||
* const moduleStats = await getModuleDashboardStats();
|
||||
*
|
||||
* return (
|
||||
* <AdminPagesClient
|
||||
* params={params}
|
||||
* user={user}
|
||||
* dashboardStats={dashboardStats}
|
||||
* moduleStats={moduleStats}
|
||||
* />
|
||||
* );
|
||||
* }
|
||||
|
||||
@@ -1,85 +1,24 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* Admin Pages Component
|
||||
*
|
||||
* This component handles both core admin pages and module pages.
|
||||
* Module pages are loaded dynamically on the client where hooks work properly.
|
||||
*/
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import DashboardPage from './pages/DashboardPage.js';
|
||||
import UsersPage from './pages/UsersPage.js';
|
||||
import UserEditPage from './pages/UserEditPage.js';
|
||||
import ProfilePage from './pages/ProfilePage.js';
|
||||
import { getModulePageLoader } from '../../../modules/modules.pages.js';
|
||||
|
||||
// Loading component for suspense
|
||||
function PageLoading() {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default function AdminPagesClient({ params, user, dashboardStats = null }) {
|
||||
const parts = params?.admin || [];
|
||||
const page = parts[0] || 'dashboard';
|
||||
|
||||
export default function AdminPagesClient({
|
||||
params,
|
||||
user,
|
||||
dashboardStats = null,
|
||||
moduleStats = {},
|
||||
modulePageInfo = null,
|
||||
routeInfo = null,
|
||||
enabledModules = {}
|
||||
}) {
|
||||
// If this is a module page, render it with lazy loading
|
||||
if (modulePageInfo && routeInfo) {
|
||||
const LazyComponent = getModulePageLoader(modulePageInfo.module, modulePageInfo.path);
|
||||
if (LazyComponent) {
|
||||
// Build props for the page
|
||||
const pageProps = { user };
|
||||
if (routeInfo.action === 'edit' && routeInfo.id) {
|
||||
// Add ID props for edit pages (modules may use different prop names)
|
||||
pageProps.id = routeInfo.id;
|
||||
pageProps.invoiceId = routeInfo.id;
|
||||
pageProps.clientId = routeInfo.id;
|
||||
pageProps.itemId = routeInfo.id;
|
||||
pageProps.categoryId = routeInfo.id;
|
||||
pageProps.transactionId = routeInfo.id;
|
||||
pageProps.recurrenceId = routeInfo.id;
|
||||
pageProps.templateId = routeInfo.id;
|
||||
pageProps.postId = routeInfo.id;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<PageLoading />}>
|
||||
<LazyComponent {...pageProps} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
if (page === 'users' && parts[1] === 'edit' && parts[2]) {
|
||||
return <UserEditPage userId={parts[2]} user={user} />;
|
||||
}
|
||||
|
||||
// Determine core page from routeInfo or params
|
||||
let currentPage = 'dashboard';
|
||||
if (routeInfo?.path) {
|
||||
const parts = routeInfo.path.split('/').filter(Boolean);
|
||||
currentPage = parts[1] || 'dashboard'; // /admin/[page]
|
||||
} else if (params?.admin) {
|
||||
currentPage = params.admin[0] || 'dashboard';
|
||||
}
|
||||
|
||||
// Core page components mapping (non-module pages)
|
||||
const usersPageComponent = routeInfo?.action === 'edit' && routeInfo?.id
|
||||
? () => <UserEditPage userId={routeInfo.id} user={user} enabledModules={enabledModules} />
|
||||
: () => <UsersPage user={user} />;
|
||||
|
||||
const corePages = {
|
||||
dashboard: () => <DashboardPage user={user} stats={dashboardStats} moduleStats={moduleStats} enabledModules={enabledModules} />,
|
||||
users: usersPageComponent,
|
||||
dashboard: () => <DashboardPage user={user} stats={dashboardStats} />,
|
||||
users: () => <UsersPage user={user} />,
|
||||
profile: () => <ProfilePage user={user} />,
|
||||
};
|
||||
|
||||
// Render the appropriate core page or default to dashboard
|
||||
const CorePageComponent = corePages[currentPage];
|
||||
return CorePageComponent ? <CorePageComponent /> : <DashboardPage user={user} stats={dashboardStats} moduleStats={moduleStats} enabledModules={enabledModules} />;
|
||||
const CorePageComponent = corePages[page];
|
||||
return CorePageComponent ? <CorePageComponent /> : <DashboardPage user={user} stats={dashboardStats} />;
|
||||
}
|
||||
|
||||
@@ -1,33 +1,11 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* Admin Dashboard Page
|
||||
* Displays core stats and dynamically loads module dashboard widgets
|
||||
*/
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import { StatCard } from '../../../../shared/components';
|
||||
import { UserMultiple02Icon } from '../../../../shared/Icons.js';
|
||||
import { getModuleDashboardWidgets } from '../../../../modules/modules.pages.js';
|
||||
|
||||
/**
|
||||
* Loading placeholder for widgets
|
||||
*/
|
||||
function WidgetLoading() {
|
||||
return (
|
||||
<div className="animate-pulse bg-neutral-200 dark:bg-neutral-800 rounded-lg h-32"></div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DashboardPage({ user, stats, moduleStats = {}, enabledModules = {} }) {
|
||||
export default function DashboardPage({ user, stats }) {
|
||||
const loading = !stats;
|
||||
|
||||
// Get only enabled module dashboard widgets
|
||||
const allModuleWidgets = getModuleDashboardWidgets();
|
||||
const moduleWidgets = Object.fromEntries(
|
||||
Object.entries(allModuleWidgets).filter(([moduleName]) => enabledModules[moduleName])
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 sm:gap-6 lg:gap-8">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -40,16 +18,6 @@ export default function DashboardPage({ user, stats, moduleStats = {}, enabledMo
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
||||
{/* Module dashboard widgets (dynamically loaded) */}
|
||||
{Object.entries(moduleWidgets).map(([moduleName, widgets]) => (
|
||||
widgets.map((Widget, index) => (
|
||||
<Suspense key={`${moduleName}-widget-${index}`} fallback={<WidgetLoading />}>
|
||||
<Widget stats={moduleStats[moduleName]} />
|
||||
</Suspense>
|
||||
))
|
||||
))}
|
||||
|
||||
{/* Core stats - always shown */}
|
||||
<StatCard
|
||||
title="Nombre d'utilisateurs"
|
||||
value={loading ? '-' : String(stats?.totalUsers || 0)}
|
||||
|
||||
@@ -9,20 +9,17 @@ import { useToast } from '@zen/core/toast';
|
||||
* User Edit Page Component
|
||||
* Page for editing an existing user (admin only)
|
||||
*/
|
||||
const UserEditPage = ({ userId, user, enabledModules = {} }) => {
|
||||
const UserEditPage = ({ userId, user }) => {
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
const clientsModuleActive = Boolean(enabledModules?.clients);
|
||||
|
||||
const [userData, setUserData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [clients, setClients] = useState([]);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
role: 'user',
|
||||
email_verified: 'false',
|
||||
client_id: ''
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
@@ -40,15 +37,6 @@ const UserEditPage = ({ userId, user, enabledModules = {} }) => {
|
||||
loadUser();
|
||||
}, [userId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (clientsModuleActive) {
|
||||
fetch('/zen/api/admin/clients?limit=500', { credentials: 'include' })
|
||||
.then(res => res.json())
|
||||
.then(data => data.clients ? setClients(data.clients) : setClients([]))
|
||||
.catch(() => setClients([]));
|
||||
}
|
||||
}, [clientsModuleActive]);
|
||||
|
||||
const loadUser = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -64,7 +52,6 @@ const UserEditPage = ({ userId, user, enabledModules = {} }) => {
|
||||
name: data.user.name || '',
|
||||
role: data.user.role || 'user',
|
||||
email_verified: data.user.email_verified ? 'true' : 'false',
|
||||
client_id: data.linkedClient ? String(data.linkedClient.id) : ''
|
||||
}));
|
||||
} else {
|
||||
toast.error(data.message || 'Utilisateur introuvable');
|
||||
@@ -107,7 +94,6 @@ const UserEditPage = ({ userId, user, enabledModules = {} }) => {
|
||||
name: formData.name.trim(),
|
||||
role: formData.role,
|
||||
email_verified: formData.email_verified === 'true',
|
||||
...(clientsModuleActive && { client_id: formData.client_id ? parseInt(formData.client_id, 10) : null })
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
@@ -209,21 +195,6 @@ const UserEditPage = ({ userId, user, enabledModules = {} }) => {
|
||||
onChange={(value) => handleInputChange('email_verified', value)}
|
||||
options={emailVerifiedOptions}
|
||||
/>
|
||||
|
||||
{clientsModuleActive && (
|
||||
<Select
|
||||
label="Client associé"
|
||||
value={formData.client_id}
|
||||
onChange={(value) => handleInputChange('client_id', value)}
|
||||
options={[
|
||||
{ value: '', label: 'Aucun' },
|
||||
...clients.map(c => ({
|
||||
value: String(c.id),
|
||||
label: [c.client_number, c.company_name || [c.first_name, c.last_name].filter(Boolean).join(' ') || c.email].filter(Boolean).join(' – ')
|
||||
}))
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
/**
|
||||
* Admin Navigation Builder (Server-Only)
|
||||
*
|
||||
* This file imports from the module registry and should ONLY be used on the server.
|
||||
* It builds the complete navigation including dynamic module navigation.
|
||||
*
|
||||
* IMPORTANT: This file is NOT bundled to ensure it shares the same registry instance
|
||||
* that was populated during module discovery.
|
||||
*
|
||||
* IMPORTANT: We import from '@zen/core' (main package) to use the same registry
|
||||
* instance that was populated during initializeZen(). DO NOT import from
|
||||
* '@zen/core/core/modules' as that's a separate bundle with its own registry.
|
||||
*
|
||||
* IMPORTANT: Navigation data must be serializable (no functions/components).
|
||||
* Icons are passed as string names and resolved on the client.
|
||||
*/
|
||||
@@ -18,7 +8,7 @@
|
||||
/**
|
||||
* Build complete navigation sections
|
||||
* @param {string} pathname - Current pathname
|
||||
* @returns {Array} Complete navigation sections (serializable, icons as strings)
|
||||
* @returns {Array} Navigation sections (serializable, icons as strings)
|
||||
*/
|
||||
export function buildNavigationSections(pathname) {
|
||||
const coreNavigation = [
|
||||
|
||||
@@ -12,41 +12,6 @@ import { getDashboardStats } from '@zen/core/admin/actions';
|
||||
import { logoutAction } from '@zen/core/auth/actions';
|
||||
import { getAppName } from '@zen/core';
|
||||
|
||||
function parseAdminRoute(params) {
|
||||
const parts = params?.admin || [];
|
||||
|
||||
if (parts.length === 0) {
|
||||
return { path: '/admin/dashboard', action: null, id: null };
|
||||
}
|
||||
|
||||
const corePages = ['dashboard', 'users', 'profile'];
|
||||
if (corePages.includes(parts[0])) {
|
||||
if (parts[0] === 'users' && parts[1] === 'edit' && parts[2]) {
|
||||
return { path: '/admin/users', action: 'edit', id: parts[2] };
|
||||
}
|
||||
return { path: `/admin/${parts[0]}`, action: null, id: null };
|
||||
}
|
||||
|
||||
let pathParts = [];
|
||||
let action = null;
|
||||
let id = null;
|
||||
const actionKeywords = ['new', 'create', 'edit'];
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
if (actionKeywords.includes(part)) {
|
||||
action = part === 'create' ? 'new' : part;
|
||||
if (action === 'edit' && i + 1 < parts.length) {
|
||||
id = parts[i + 1];
|
||||
}
|
||||
break;
|
||||
}
|
||||
pathParts.push(part);
|
||||
}
|
||||
|
||||
return { path: '/admin/' + pathParts.join('/') + (action ? '/' + action : ''), action, id };
|
||||
}
|
||||
|
||||
export default async function AdminPage({ params }) {
|
||||
const resolvedParams = await params;
|
||||
const session = await protectAdmin();
|
||||
@@ -56,7 +21,6 @@ export default async function AdminPage({ params }) {
|
||||
const dashboardStats = statsResult.success ? statsResult.stats : null;
|
||||
|
||||
const navigationSections = buildNavigationSections('/');
|
||||
const { path, action, id } = parseAdminRoute(resolvedParams);
|
||||
|
||||
return (
|
||||
<AdminPagesLayout
|
||||
@@ -69,7 +33,6 @@ export default async function AdminPage({ params }) {
|
||||
params={resolvedParams}
|
||||
user={session.user}
|
||||
dashboardStats={dashboardStats}
|
||||
routeInfo={{ path, action, id }}
|
||||
/>
|
||||
</AdminPagesLayout>
|
||||
);
|
||||
|
||||
@@ -1,31 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { ToastProvider, ToastContainer } from '@zen/core/toast';
|
||||
import { registerExternalModulePages } from '../../modules/modules.pages.js';
|
||||
|
||||
/**
|
||||
* ZenProvider — root client provider for the ZEN CMS.
|
||||
*
|
||||
* Pass external module configs via the `modules` prop so their
|
||||
* admin pages and public pages are available to the client router.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Array} props.modules - External module configs from zen.config.js
|
||||
* @param {ReactNode} props.children
|
||||
*/
|
||||
export function ZenProvider({ modules = [], children }) {
|
||||
const registered = useRef(false);
|
||||
|
||||
if (!registered.current) {
|
||||
// Register synchronously on first render so pages are available
|
||||
// before any child component resolves a module route.
|
||||
if (modules.length > 0) {
|
||||
registerExternalModulePages(modules);
|
||||
}
|
||||
registered.current = true;
|
||||
}
|
||||
|
||||
export function ZenProvider({ children }) {
|
||||
return (
|
||||
<ToastProvider>
|
||||
{children}
|
||||
|
||||
Reference in New Issue
Block a user