diff --git a/docs/modules/INTERNAL_MODULE.md b/docs/modules/INTERNAL_MODULE.md index 37848ef..3836528 100644 --- a/docs/modules/INTERNAL_MODULE.md +++ b/docs/modules/INTERNAL_MODULE.md @@ -132,17 +132,14 @@ async function handleList(request) { export default { routes: [ - { - path: 'mon-module/list', - method: 'GET', - handler: handleList, - requireAuth: true, - requireAdmin: false, - }, + { path: '/admin/mon-module/list', method: 'GET', handler: handleList, auth: 'admin' }, + { path: '/mon-module/list', method: 'GET', handler: handleList, auth: 'public' }, ], }; ``` +Valeurs acceptées pour `auth` : `'admin'` (JWT admin requis), `'user'` (JWT utilisateur requis), `'public'` (aucune auth). + --- ## cron.config.js diff --git a/src/core/modules/client.js b/src/core/modules/client.js index a335072..e4bc4f2 100644 --- a/src/core/modules/client.js +++ b/src/core/modules/client.js @@ -23,7 +23,7 @@ export { getAllCronJobs, getAllPublicRoutes, getAllDatabaseSchemas, - getModuleMetadata, + getModuleMetadataGenerator, getAllModuleMetadata, } from './registry.js'; diff --git a/src/core/modules/discovery.js b/src/core/modules/discovery.js index f34421c..d4585f4 100644 --- a/src/core/modules/discovery.js +++ b/src/core/modules/discovery.js @@ -13,7 +13,7 @@ import { getAvailableModules } from '../../modules/modules.registry.js'; * @returns {boolean} */ export function isModuleEnabledInEnv(moduleName) { - const envVar = `ZEN_MODULE_${moduleName.toUpperCase()}`; + const envVar = `ZEN_MODULE_${moduleName.toUpperCase().replace(/-/g, '_')}`; return process.env[envVar] === 'true'; } diff --git a/src/core/modules/index.js b/src/core/modules/index.js index 95891a8..887ddec 100644 --- a/src/core/modules/index.js +++ b/src/core/modules/index.js @@ -26,7 +26,7 @@ export { getAllCronJobs, getAllPublicRoutes, getAllDatabaseSchemas, - getModuleMetadata, + getModuleMetadataGenerator, getAllModuleMetadata, getModulePublicPages // returns route metadata only, use modules.pages.js for components } from './registry.js'; diff --git a/src/core/modules/registry.js b/src/core/modules/registry.js index c718b04..e00d774 100644 --- a/src/core/modules/registry.js +++ b/src/core/modules/registry.js @@ -232,12 +232,16 @@ export function getAllDatabaseSchemas() { } /** - * Get metadata generator function from a module + * Get a specific metadata generator function from a module. + * Use this when you need to call a generator directly (e.g. for Next.js generateMetadata). + * + * To get the full metadata object for a module, use getModuleMetadata() from modules.metadata.js. + * * @param {string} moduleName - Module name (e.g., 'invoice') - * @param {string} type - Metadata type (e.g., 'payment', 'pdf', 'receipt') + * @param {string} type - Metadata type key (e.g., 'payment', 'pdf', 'receipt') * @returns {Function|null} Metadata generator function or null if not found */ -export function getModuleMetadata(moduleName, type) { +export function getModuleMetadataGenerator(moduleName, type) { const module = getModule(moduleName); if (module?.enabled && module?.metadata) { diff --git a/src/features/provider/ZenProvider.js b/src/features/provider/ZenProvider.js index 17a03e6..1cb3dbc 100644 --- a/src/features/provider/ZenProvider.js +++ b/src/features/provider/ZenProvider.js @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useEffect, useRef } from 'react'; import { ToastProvider, ToastContainer } from '@zen/core/toast'; import { registerExternalModulePages } from '../../modules/modules.pages.js'; @@ -15,13 +15,16 @@ import { registerExternalModulePages } from '../../modules/modules.pages.js'; * @param {ReactNode} props.children */ export function ZenProvider({ modules = [], children }) { - // Register external module pages once, synchronously, before first render. - // useState initializer runs exactly once and does not cause a re-render. - useState(() => { + 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; + } return ( diff --git a/src/modules/index.js b/src/modules/index.js index fed9df1..7a4e8a1 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -37,7 +37,7 @@ export { getEnabledModules, // Module-specific getters - getModuleMetadata, + getModuleMetadataGenerator, getAllModuleMetadata, getModulePublicPages, @@ -46,6 +46,9 @@ export { isModuleRegistered, } from '../core/modules/index.js'; +// Module metadata (server-side object getters) +export { getModuleMetadata, getMetadataGenerator } from './modules.metadata.js'; + // Client-side module pages registry export { registerExternalModulePages } from './modules.pages.js'; diff --git a/src/modules/modules.actions.js b/src/modules/modules.actions.js index b0f1d90..f2844b0 100644 --- a/src/modules/modules.actions.js +++ b/src/modules/modules.actions.js @@ -9,7 +9,7 @@ * const { getInvoiceByToken } = getModuleActions('invoice'); */ -import { getModule } from '../core/modules/registry.js'; +import { getModule, getEnabledModules } from '../core/modules/registry.js'; // Static actions for internal modules (add entries here for new internal modules) export const MODULE_ACTIONS = { @@ -43,14 +43,15 @@ export function getModuleDashboardAction(moduleName) { } /** - * Get all dashboard stats from all modules + * Get all dashboard stats from all modules (internal static + external runtime). * @returns {Promise} Object with module names as keys and stats as values */ export async function getAllModuleDashboardStats() { const stats = {}; + // Internal modules — static action map for (const [moduleName, getStats] of Object.entries(MODULE_DASHBOARD_ACTIONS)) { - const envKey = `ZEN_MODULE_${moduleName.toUpperCase()}`; + const envKey = `ZEN_MODULE_${moduleName.toUpperCase().replace(/-/g, '_')}`; if (process.env[envKey] !== 'true') continue; try { @@ -63,6 +64,20 @@ export async function getAllModuleDashboardStats() { } } + // External modules — runtime registry + for (const mod of getEnabledModules()) { + if (mod.external && typeof mod.actions?.getDashboardStats === 'function') { + try { + const result = await mod.actions.getDashboardStats(); + if (result.success) { + stats[mod.name] = result.stats; + } + } catch (error) { + console.error(`Error getting dashboard stats for ${mod.name}:`, error); + } + } + } + return stats; } diff --git a/src/modules/modules.pages.js b/src/modules/modules.pages.js index 62033b5..0bcb09d 100644 --- a/src/modules/modules.pages.js +++ b/src/modules/modules.pages.js @@ -148,11 +148,17 @@ export function getAllModuleConfigs() { } /** - * Get all dashboard widgets from all modules + * Get all dashboard widgets from all modules (internal + external). * @returns {Object} Object with module names as keys and arrays of lazy widgets */ export function getModuleDashboardWidgets() { - return MODULE_DASHBOARD_WIDGETS; + const widgets = { ...MODULE_DASHBOARD_WIDGETS }; + for (const [name, config] of EXTERNAL_MODULE_CONFIGS) { + if (config.dashboardWidgets?.length) { + widgets[name] = config.dashboardWidgets; + } + } + return widgets; } // Legacy export for backward compatibility diff --git a/src/modules/modules.registry.js b/src/modules/modules.registry.js index 18b6435..5e3ac5d 100644 --- a/src/modules/modules.registry.js +++ b/src/modules/modules.registry.js @@ -1,15 +1,16 @@ /** * Available Modules Registry - * - * Add your module name here to enable discovery. - * See MODULES.md for the full module creation guide. - * - * Files to update when adding a module: - * 1. modules.registry.js → Add to AVAILABLE_MODULES - * 2. modules.pages.js → Import config, add to MODULE_CONFIGS - * 3. modules.actions.js → Import actions (if public pages) - * 4. modules.metadata.js → Import metadata (if SEO needed) - * 5. modules/init.js → Import createTables for database CLI + * + * Add the module name here to make it discoverable by the server. + * See docs/modules/INTERNAL_MODULE.md for the full creation guide. + * + * Required steps when adding an internal module: + * 1. modules.registry.js → Add name to AVAILABLE_MODULES (this file) + * 2. modules.pages.js → Import module.config.js, add to MODULE_CONFIGS + * + * Optional steps (only when the capability is needed): + * 3. modules.actions.js → Add to MODULE_ACTIONS (public page server actions) + * 4. modules.metadata.js → Add to MODULE_METADATA (Next.js SEO generators) */ export const AVAILABLE_MODULES = [ 'posts', diff --git a/src/modules/posts/module.config.js b/src/modules/posts/module.config.js index 636bc22..b65ab61 100644 --- a/src/modules/posts/module.config.js +++ b/src/modules/posts/module.config.js @@ -1,9 +1,10 @@ /** * Posts Module Configuration - * Navigation and adminPages are generated dynamically from ZEN_MODULE_ZEN_MODULE_POSTS_TYPES env var. + * Navigation and adminPages are generated dynamically from ZEN_MODULE_POSTS_TYPES env var. */ import { lazy } from 'react'; +import { defineModule } from '../../core/modules/defineModule.js'; import { getPostsConfig } from './config.js'; // Lazy components — shared across all post types @@ -73,7 +74,7 @@ function pageResolver(path) { return null; } -export default { +export default defineModule({ name: 'posts', displayName: 'Posts', version: '1.0.0', @@ -81,7 +82,7 @@ export default { dependencies: [], - envVars: ['ZEN_MODULE_ZEN_MODULE_POSTS_TYPES'], + envVars: ['ZEN_MODULE_POSTS_TYPES'], // Array of sections — one per post type (server-side, env vars available) navigation: navigationSections, @@ -95,4 +96,4 @@ export default { publicPages: {}, publicRoutes: [], dashboardWidgets: [], -}; +});