feat(storage): refactor storage config and remove module registry

Introduce a dedicated `storage-config.js` for registering public
prefixes and access policies via `configureStorageApi()`, replacing the
previous `getAllStoragePublicPrefixes` / `getAllStorageAccessPolicies`
imports from the module registry.

Remove `getAllApiRoutes()` from the router so module-level routes are no
longer auto-collected; feature routes must now be registered explicitly
via `registerFeatureRoutes()` during `initializeZen()`.

Update `.env.example` to document separate `ZEN_STORAGE_PROVIDER`,
`ZEN_STORAGE_B2_*` variables for Backblaze B2 alongside the existing
Cloudflare R2 variables, making provider selection explicit.

Clean up admin navigation and page components to drop module-injected
nav entries, keeping only core and system sections.
This commit is contained in:
2026-04-14 17:43:06 -04:00
parent 4a06cace5d
commit 242ea69664
15 changed files with 404 additions and 640 deletions
-27
View File
@@ -3,8 +3,6 @@
* Centralized configuration management for the entire package
*/
import { getAvailableModules } from '../../modules/modules.registry.js';
/**
* Get application name from environment variables
* @returns {string} Application name
@@ -36,28 +34,6 @@ export function getPublicBaseUrl() {
return String(raw).replace(/\/$/, '');
}
/**
* Get enabled modules configuration (server-side only)
* This function dynamically reads from modules.registry.js and checks environment variables
* Use this on the server and pass the result to client components as props
*
* To enable a module, set the environment variable: ZEN_MODULE_{NAME}=true
* Example: ZEN_MODULE_INVOICE=true
*
* @returns {Object} Object with module names as keys and boolean values
*/
export function getModulesConfig() {
const modules = {};
const availableModules = getAvailableModules();
for (const moduleName of availableModules) {
const envVar = `ZEN_MODULE_${moduleName.toUpperCase()}`;
modules[moduleName] = process.env[envVar] === 'true';
}
return modules;
}
/**
* Get application configuration
* @returns {Object} Application configuration object
@@ -68,10 +44,7 @@ export function getAppConfig() {
sessionCookieName: getSessionCookieName(),
timezone: process.env.ZEN_TIMEZONE || 'America/Toronto',
dateFormat: process.env.ZEN_DATE_FORMAT || 'YYYY-MM-DD',
// Currency configuration (for currency module)
defaultCurrency: process.env.ZEN_CURRENCY || 'CAD',
currencySymbol: process.env.ZEN_CURRENCY_SYMBOL || '$',
// Enabled modules
modules: getModulesConfig(),
};
}
+15 -102
View File
@@ -1,136 +1,49 @@
/**
* ZEN Initialization
* Initialize all ZEN services and modules using dynamic module discovery
*/
import { discoverModules, registerExternalModules, startModuleCronJobs, stopModuleCronJobs } from '../../core/modules/index.js';
import { configureRouter, registerFeatureRoutes, clearFeatureRoutes, clearRouterConfig } from '../../core/api/index.js';
import { validateSession } from '../../features/auth/lib/session.js';
import { routes as authRoutes } from '../../features/auth/api.js';
import { step, done, warn, fail } from './logger.js';
// Use globalThis to persist initialization flag across module reloads
const ZEN_INIT_KEY = Symbol.for('__ZEN_INITIALIZED__');
/**
* Initialize ZEN system
* Discovers modules dynamically and starts cron jobs
*
* Recommended: Use instrumentation.js for automatic initialization
* Alternative: Call this function manually in your root layout
* Wires core feature dependencies into the API router.
* This is the composition root — the only place that connects features to core.
*
* @example
* // instrumentation.js (Recommended) — internal modules only
* // instrumentation.js
* export async function register() {
* if (process.env.NEXT_RUNTIME === 'nodejs') {
* const { initializeZen } = await import('@zen/core');
* await initializeZen();
* }
* }
*
* @example
* // instrumentation.js — with external modules from zen.config.js
* import zenConfig from './zen.config.js';
* export async function register() {
* if (process.env.NEXT_RUNTIME === 'nodejs') {
* const { initializeZen } = await import('@zen/core');
* await initializeZen(zenConfig);
* }
* }
*
* @param {Object} config - Configuration object
* @param {Array} config.modules - External module configs (from zen.config.js)
* @param {boolean} config.skipCron - Skip cron job initialization
* @param {boolean} config.skipDb - Skip database initialization
* @returns {Promise<Object>} Initialization result
*/
export async function initializeZen(config = {}) {
const { modules: externalModules = [], skipCron = false, skipDb = true } = config;
// Only run on server-side
import { configureRouter, registerFeatureRoutes, clearRouterConfig, clearFeatureRoutes } from '../../core/api/index.js';
import { validateSession } from '../../features/auth/lib/session.js';
import { routes as authRoutes } from '../../features/auth/api.js';
import { done, warn } from './logger.js';
const ZEN_INIT_KEY = Symbol.for('__ZEN_INITIALIZED__');
export async function initializeZen() {
if (typeof window !== 'undefined') {
return { skipped: true, reason: 'client-side' };
}
// Prevent multiple initializations using globalThis
if (globalThis[ZEN_INIT_KEY]) {
warn('ZEN: already initialized, skipping');
return { skipped: true, reason: 'already-initialized' };
}
globalThis[ZEN_INIT_KEY] = true;
step('ZEN starting...');
const result = {
discovery: null,
cron: { started: [], errors: [] }
};
configureRouter({ resolveSession: validateSession });
registerFeatureRoutes(authRoutes);
try {
// Step 1: Wire core feature dependencies into the API router.
// init.js is the composition root — the only place that wires features
// into core, keeping core/api/ free of static feature-level imports.
configureRouter({ resolveSession: validateSession });
registerFeatureRoutes(authRoutes);
done('ZEN: ready');
// Step 2: Discover and register internal modules (from modules.registry.js)
result.discovery = await discoverModules();
const enabledCount = result.discovery.enabled?.length || 0;
const skippedCount = result.discovery.skipped?.length || 0;
if (enabledCount > 0) {
done(`ZEN: ${enabledCount} module(s): ${result.discovery.enabled.join(', ')}`);
}
if (skippedCount > 0) {
warn(`ZEN: skipped ${skippedCount} module(s): ${result.discovery.skipped.join(', ')}`);
}
// Step 3: Register external modules from zen.config.js (if any)
if (externalModules.length > 0) {
result.external = await registerExternalModules(externalModules);
if (result.external.registered.length > 0) {
done(`ZEN: ${result.external.registered.length} external module(s): ${result.external.registered.join(', ')}`);
}
}
// Step 4: Start cron jobs for all enabled modules (internal + external)
if (!skipCron) {
result.cron = await startModuleCronJobs();
if (result.cron.started.length > 0) {
done(`ZEN: ${result.cron.started.length} cron job(s): ${result.cron.started.join(', ')}`);
}
}
done('ZEN: ready');
} catch (error) {
fail(`ZEN: init failed: ${error.message}`);
result.error = error.message;
}
return result;
return {};
}
/**
* Reset initialization flag (useful for testing or manual reinitialization)
* @returns {void}
*/
export function resetZenInitialization() {
globalThis[ZEN_INIT_KEY] = false;
// Stop all cron jobs using the module system
try {
stopModuleCronJobs();
} catch (e) {
// Cron system not available
}
// Clear router config and feature routes so they are re-registered on next initializeZen()
clearRouterConfig();
clearFeatureRoutes();
warn('ZEN: initialization reset');
}