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
+4 -6
View File
@@ -1,12 +1,10 @@
/**
* Admin Server Actions
*
* These are exported separately from admin/index.js to avoid bundling
*
* Exported separately from admin/index.js to avoid bundling
* server-side code (which includes database imports) into client components.
*
* Usage:
* import { getDashboardStats, getModuleDashboardStats } from '@zen/core/admin/actions';
*
* Usage: import { getDashboardStats } from '@zen/core/admin/actions';
*/
export { getDashboardStats } from './actions/statsActions.js';
export { getAllModuleDashboardStats as getModuleDashboardStats } from '@zen/core/modules/actions';
+3 -15
View File
@@ -15,20 +15,12 @@
* Icons are passed as string names and resolved on the client.
*/
// Import from the main package to use the same registry as discovery
import { moduleSystem } from '@zen/core';
const { getAllAdminNavigation } = moduleSystem;
/**
* Build complete navigation sections including modules
* This should ONLY be called on the server (in page.js)
* Build complete navigation sections
* @param {string} pathname - Current pathname
* @param {Object} enabledModules - Object with module names as keys (for compatibility)
* @returns {Array} Complete navigation sections (serializable, icons as strings)
*/
export function buildNavigationSections(pathname, enabledModules = null) {
// Core navigation sections (always available)
// Use icon NAMES (strings) for serialization across server/client boundary
export function buildNavigationSections(pathname) {
const coreNavigation = [
{
id: 'Dashboard',
@@ -45,10 +37,6 @@ export function buildNavigationSections(pathname, enabledModules = null) {
}
];
// Get module navigation from registry (only works on server)
const moduleNavigation = getAllAdminNavigation(pathname);
// System navigation (always at the end)
const systemNavigation = [
{
id: 'users',
@@ -65,5 +53,5 @@ export function buildNavigationSections(pathname, enabledModules = null) {
}
];
return [...coreNavigation, ...moduleNavigation, ...systemNavigation];
return [...coreNavigation, ...systemNavigation];
}
+21 -80
View File
@@ -1,134 +1,75 @@
/**
* Admin Page - Server Component Wrapper for Next.js App Router
*
* This is a complete server component that handles all admin routes.
* Users can simply re-export this in their app/admin/[...admin]/page.js:
*
* ```javascript
*
* Re-export this in your app/admin/[...admin]/page.js:
* export { default } from '@zen/core/admin/page';
* ```
*
* This eliminates the need to manually import and pass all actions and props.
*/
import { AdminPagesLayout, AdminPagesClient } from '@zen/core/admin/pages';
import { protectAdmin } from '@zen/core/admin';
import { buildNavigationSections } from '@zen/core/admin/navigation';
import { getDashboardStats, getModuleDashboardStats } from '@zen/core/admin/actions';
import { getDashboardStats } from '@zen/core/admin/actions';
import { logoutAction } from '@zen/core/auth/actions';
import { getAppName, getModulesConfig, getAppConfig, moduleSystem } from '@zen/core';
import { getAppName } from '@zen/core';
const { getAdminPage } = moduleSystem;
/**
* Parse admin route params and build the module path
* Handles nested paths like /admin/invoice/clients/edit/123
*
* @param {Object} params - Next.js route params
* @returns {Object} Parsed info with path, action, and id
*/
function parseAdminRoute(params) {
const parts = params?.admin || [];
if (parts.length === 0) {
return { path: '/admin/dashboard', action: null, id: null, isCorePage: true };
return { path: '/admin/dashboard', action: null, id: null };
}
// Check for core pages first
const corePages = ['dashboard', 'users', 'profile'];
if (corePages.includes(parts[0])) {
// Users: support /admin/users/edit/:id
if (parts[0] === 'users' && parts[1] === 'edit' && parts[2]) {
return { path: '/admin/users', action: 'edit', id: parts[2], isCorePage: true };
return { path: '/admin/users', action: 'edit', id: parts[2] };
}
return { path: `/admin/${parts[0]}`, action: null, id: null, isCorePage: true };
return { path: `/admin/${parts[0]}`, action: null, id: null };
}
// Build module path
// Look for 'new', 'create', or 'edit' to determine action
const actionKeywords = ['new', 'create', 'edit'];
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 it's 'edit', the next part is the ID
if (action === 'edit' && i + 1 < parts.length) {
id = parts[i + 1];
}
break;
}
pathParts.push(part);
}
// Build the full path
let fullPath = '/admin/' + pathParts.join('/');
if (action) {
fullPath += '/' + action;
}
return { path: fullPath, action, id, isCorePage: false };
}
/**
* Check if a path is a module page
* @param {string} fullPath - Full admin path
* @returns {Object|null} Module info if it's a module page, null otherwise
*/
function getModulePageInfo(fullPath) {
const modulePage = getAdminPage(fullPath);
if (modulePage) {
return {
module: modulePage.module,
path: fullPath
};
}
return null;
return { path: '/admin/' + pathParts.join('/') + (action ? '/' + action : ''), action, id };
}
export default async function AdminPage({ params }) {
const resolvedParams = await params;
const session = await protectAdmin();
const appName = getAppName();
const enabledModules = getModulesConfig();
const config = getAppConfig();
const statsResult = await getDashboardStats();
const dashboardStats = statsResult.success ? statsResult.stats : null;
// Fetch module dashboard stats for widgets
const moduleStats = await getModuleDashboardStats();
// Build navigation on server where module registry is available
const navigationSections = buildNavigationSections('/', enabledModules);
// Parse route and build path
const { path, action, id, isCorePage } = parseAdminRoute(resolvedParams);
// Check if this is a module page (just check existence, don't load)
const modulePageInfo = isCorePage ? null : getModulePageInfo(path);
const navigationSections = buildNavigationSections('/');
const { path, action, id } = parseAdminRoute(resolvedParams);
return (
<AdminPagesLayout
user={session.user}
onLogout={logoutAction}
<AdminPagesLayout
user={session.user}
onLogout={logoutAction}
appName={appName}
enabledModules={enabledModules}
navigationSections={navigationSections}
>
<AdminPagesClient
params={resolvedParams}
user={session.user}
dashboardStats={dashboardStats}
moduleStats={moduleStats}
modulePageInfo={modulePageInfo}
routeInfo={{ path, action, id }}
enabledModules={enabledModules}
/>
</AdminPagesLayout>
);