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:
2026-04-14 19:26:48 -04:00
parent 242ea69664
commit 3131df2b71
9 changed files with 415 additions and 239 deletions
+387
View File
@@ -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
View File
@@ -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;
}
+11 -19
View File
@@ -1,34 +1,26 @@
/**
* 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}
* <AdminPagesClient
* params={params}
* user={user}
* dashboardStats={dashboardStats}
* moduleStats={moduleStats}
* />
* );
* }
@@ -72,9 +64,9 @@ export async function getDashboardStats() {
};
} catch (error) {
fail(`Error getting dashboard stats: ${error.message}`);
return {
success: false,
error: error.message || 'Failed to get dashboard statistics'
return {
success: false,
error: error.message || 'Failed to get dashboard statistics'
};
}
}
+9 -70
View File
@@ -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>
+4 -14
View File
@@ -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 = [
@@ -36,7 +26,7 @@ export function buildNavigationSections(pathname) {
]
}
];
const systemNavigation = [
{
id: 'users',
@@ -52,6 +42,6 @@ export function buildNavigationSections(pathname) {
]
}
];
return [...coreNavigation, ...systemNavigation];
}
-37
View File
@@ -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 -24
View File
@@ -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}