feat(storage): replace hardcoded blog prefix with module-declared public prefixes
Refactor storage access control to use dynamic public prefixes sourced from `getAllStoragePublicPrefixes()` instead of a hardcoded `blog` check. Each module can now declare its own public storage prefixes via `defineModule()` storagePublicPrefixes, making the system extensible without modifying the core handler. Also adds a `posts` path handler requiring admin access for private post types, removes the deprecated `version` API endpoint and its rate-limit exemption, and minor whitespace/comment cleanup.
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
import { validateSession } from '../../../features/auth/lib/session.js';
|
import { validateSession } from '../../../features/auth/lib/session.js';
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
import { getSessionCookieName } from '../../../shared/lib/appConfig.js';
|
import { getSessionCookieName } from '../../../shared/lib/appConfig.js';
|
||||||
|
import { getAllStoragePublicPrefixes } from '../../../../modules/modules.storage.js';
|
||||||
import { getFile } from '@zen/core/storage';
|
import { getFile } from '@zen/core/storage';
|
||||||
|
|
||||||
// Get cookie name from environment or use default
|
// Get cookie name from environment or use default
|
||||||
@@ -33,12 +34,15 @@ export async function handleGetFile(request, fileKey) {
|
|||||||
|
|
||||||
const pathParts = rawSegments;
|
const pathParts = rawSegments;
|
||||||
|
|
||||||
// Blog images: public read (no auth) for site integration.
|
// Public prefixes: declared by each module via defineModule() storagePublicPrefixes.
|
||||||
// Only static blog assets are served publicly. The path must be exactly
|
// Files whose path starts with a declared prefix are served without authentication.
|
||||||
// two segments deep (blog/{post-id-or-slug}/{filename}) to prevent
|
// The path must have at least two segments beyond the prefix ({...prefix}/{id}/{filename})
|
||||||
// unintentional exposure of files at the root of the blog prefix.
|
// to prevent unintentional exposure of files at the root of the prefix.
|
||||||
if (pathParts[0] === 'blog') {
|
const publicPrefixes = getAllStoragePublicPrefixes();
|
||||||
if (pathParts.length < 3) {
|
const matchedPrefix = publicPrefixes.find(prefix => fileKey.startsWith(prefix + '/'));
|
||||||
|
if (matchedPrefix) {
|
||||||
|
const prefixDepth = matchedPrefix.split('/').length;
|
||||||
|
if (pathParts.length < prefixDepth + 2) {
|
||||||
return { error: 'Bad Request', message: 'Invalid file path' };
|
return { error: 'Bad Request', message: 'Invalid file path' };
|
||||||
}
|
}
|
||||||
const result = await getFile(fileKey);
|
const result = await getFile(fileKey);
|
||||||
@@ -84,7 +88,7 @@ export async function handleGetFile(request, fileKey) {
|
|||||||
if (pathParts[0] === 'users') {
|
if (pathParts[0] === 'users') {
|
||||||
// User files: users/{userId}/{category}/{filename}
|
// User files: users/{userId}/{category}/{filename}
|
||||||
const userId = pathParts[1];
|
const userId = pathParts[1];
|
||||||
|
|
||||||
// Users can only access their own files, unless they're admin
|
// Users can only access their own files, unless they're admin
|
||||||
if (session.user.id !== userId && session.user.role !== 'admin') {
|
if (session.user.id !== userId && session.user.role !== 'admin') {
|
||||||
return {
|
return {
|
||||||
@@ -94,15 +98,24 @@ export async function handleGetFile(request, fileKey) {
|
|||||||
}
|
}
|
||||||
} else if (pathParts[0] === 'organizations') {
|
} else if (pathParts[0] === 'organizations') {
|
||||||
// Organization files: organizations/{orgId}/{category}/{filename}
|
// Organization files: organizations/{orgId}/{category}/{filename}
|
||||||
// For now, only admins can access organization files
|
// Only admins can access organization files
|
||||||
if (session.user.role !== 'admin') {
|
if (session.user.role !== 'admin') {
|
||||||
return {
|
return {
|
||||||
error: 'Forbidden',
|
error: 'Forbidden',
|
||||||
message: 'Admin access required for organization files'
|
message: 'Admin access required for organization files'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} else if (pathParts[0] === 'posts') {
|
||||||
|
// Post files: posts/{type}/{id}/{filename}
|
||||||
|
// Private types (not in storagePublicPrefixes) require admin access
|
||||||
|
if (session.user.role !== 'admin') {
|
||||||
|
return {
|
||||||
|
error: 'Forbidden',
|
||||||
|
message: 'Admin access required for this file'
|
||||||
|
};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Unknown file path pattern - deny by default
|
// Unknown file path pattern — deny by default
|
||||||
return {
|
return {
|
||||||
error: 'Forbidden',
|
error: 'Forbidden',
|
||||||
message: 'Invalid file path'
|
message: 'Invalid file path'
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
/**
|
|
||||||
* Version Handler
|
|
||||||
* Returns version information about the ZEN API.
|
|
||||||
* Requires a valid authenticated session to prevent application fingerprinting.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { getAppName } from '../../../shared/lib/appConfig.js';
|
|
||||||
import { validateSession } from '../../../features/auth/lib/session.js';
|
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { getSessionCookieName } from '../../../shared/lib/appConfig.js';
|
|
||||||
|
|
||||||
const COOKIE_NAME = getSessionCookieName();
|
|
||||||
|
|
||||||
export async function handleVersion(request) {
|
|
||||||
const cookieStore = await cookies();
|
|
||||||
const sessionToken = cookieStore.get(COOKIE_NAME)?.value;
|
|
||||||
|
|
||||||
if (!sessionToken) {
|
|
||||||
return { error: 'Unauthorized', message: 'Authentication required' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = await validateSession(sessionToken);
|
|
||||||
if (!session) {
|
|
||||||
return { error: 'Unauthorized', message: 'Invalid or expired session' };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: 'ZEN API',
|
|
||||||
appName: getAppName(),
|
|
||||||
version: '0.1.0',
|
|
||||||
apiVersion: '1.0',
|
|
||||||
description: 'ZEN API - Complete modular web platform'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -10,8 +10,7 @@ export { routeRequest, getStatusCode, requireAuth, requireAdmin } from './router
|
|||||||
|
|
||||||
// Export individual handlers (for custom usage)
|
// Export individual handlers (for custom usage)
|
||||||
export { handleHealth } from './handlers/health.js';
|
export { handleHealth } from './handlers/health.js';
|
||||||
export { handleVersion } from './handlers/version.js';
|
export {
|
||||||
export {
|
|
||||||
handleGetCurrentUser,
|
handleGetCurrentUser,
|
||||||
handleGetUserById,
|
handleGetUserById,
|
||||||
handleListUsers
|
handleListUsers
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ import { checkRateLimit, getIpFromRequest, formatRetryAfter } from '../../featur
|
|||||||
|
|
||||||
// Core handlers
|
// Core handlers
|
||||||
import { handleHealth } from './handlers/health.js';
|
import { handleHealth } from './handlers/health.js';
|
||||||
import { handleVersion } from './handlers/version.js';
|
import {
|
||||||
import {
|
|
||||||
handleGetCurrentUser,
|
handleGetCurrentUser,
|
||||||
handleGetUserById,
|
handleGetUserById,
|
||||||
handleListUsers,
|
handleListUsers,
|
||||||
@@ -153,8 +152,8 @@ async function requireAdmin(request) {
|
|||||||
export async function routeRequest(request, path) {
|
export async function routeRequest(request, path) {
|
||||||
const method = request.method;
|
const method = request.method;
|
||||||
|
|
||||||
// Global IP-based rate limit for all API calls (health/version are exempt)
|
// Global IP-based rate limit for all API calls (health is exempt)
|
||||||
const isExempt = (path[0] === 'health' || path[0] === 'version') && method === 'GET';
|
const isExempt = path[0] === 'health' && method === 'GET';
|
||||||
if (!isExempt) {
|
if (!isExempt) {
|
||||||
const ip = getIpFromRequest(request);
|
const ip = getIpFromRequest(request);
|
||||||
const rl = checkRateLimit(ip, 'api');
|
const rl = checkRateLimit(ip, 'api');
|
||||||
@@ -204,11 +203,6 @@ async function routeCoreRequest(request, path, method) {
|
|||||||
return await handleHealth();
|
return await handleHealth();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Version endpoint — authentication required; see handlers/version.js
|
|
||||||
if (path[0] === 'version' && method === 'GET') {
|
|
||||||
return await handleVersion(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Storage endpoint - serve files securely
|
// Storage endpoint - serve files securely
|
||||||
if (path[0] === 'storage' && method === 'GET') {
|
if (path[0] === 'storage' && method === 'GET') {
|
||||||
const fileKey = path.slice(1).join('/');
|
const fileKey = path.slice(1).join('/');
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ export function defineModule(config) {
|
|||||||
// SEO metadata generators
|
// SEO metadata generators
|
||||||
metadata: {},
|
metadata: {},
|
||||||
|
|
||||||
|
// Storage prefixes served publicly (no auth). Format: 'posts/blogue', 'assets/docs'
|
||||||
|
storagePublicPrefixes: [],
|
||||||
|
|
||||||
// Database (optional) — { createTables, dropTables }
|
// Database (optional) — { createTables, dropTables }
|
||||||
db: null,
|
db: null,
|
||||||
|
|
||||||
|
|||||||
@@ -646,7 +646,7 @@ export {
|
|||||||
formatFileSize,
|
formatFileSize,
|
||||||
generateUserFilePath,
|
generateUserFilePath,
|
||||||
generateOrgFilePath,
|
generateOrgFilePath,
|
||||||
generateBlogFilePath,
|
generatePostFilePath,
|
||||||
sanitizeFilename,
|
sanitizeFilename,
|
||||||
validateImageDimensions,
|
validateImageDimensions,
|
||||||
validateUpload,
|
validateUpload,
|
||||||
|
|||||||
@@ -158,13 +158,14 @@ export function generateOrgFilePath(orgId, category, filename) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a storage path for blog post images
|
* Generate a storage path for a post image, scoped by post type.
|
||||||
* @param {string|number} postIdOrSlug - Post ID or slug (e.g. for temp uploads use timestamp)
|
* @param {string} typeKey - Post type key (e.g. 'blogue', 'cve')
|
||||||
|
* @param {string|number} postIdOrSlug - Post ID or slug (use timestamp for pre-creation uploads)
|
||||||
* @param {string} filename - Filename
|
* @param {string} filename - Filename
|
||||||
* @returns {string} Storage path (e.g., 'blog/123/filename.jpg')
|
* @returns {string} Storage path (e.g., 'posts/blogue/123/filename.jpg')
|
||||||
*/
|
*/
|
||||||
export function generateBlogFilePath(postIdOrSlug, filename) {
|
export function generatePostFilePath(typeKey, postIdOrSlug, filename) {
|
||||||
return `blog/${postIdOrSlug}/${filename}`;
|
return `posts/${typeKey}/${postIdOrSlug}/${filename}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Module Storage Registry (Server-Side)
|
||||||
|
*
|
||||||
|
* Aggregates storage public prefixes declared by each module via defineModule().
|
||||||
|
* A prefix listed here is served without authentication by the storage handler.
|
||||||
|
*
|
||||||
|
* Internal modules declare `storagePublicPrefixes` in their defineModule() config.
|
||||||
|
* External modules registered at runtime are also included automatically.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { getAllStoragePublicPrefixes } from '@zen/core/modules/storage';
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getModule, getEnabledModules } from '@zen/core/core/modules';
|
||||||
|
import { getPostsConfig } from './posts/config.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute public storage prefixes for the posts module from its type config.
|
||||||
|
* Avoids importing module.config.js (which contains React lazy() calls).
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
function getPostsPublicPrefixes() {
|
||||||
|
if (process.env.ZEN_MODULE_POSTS !== 'true') return [];
|
||||||
|
const config = getPostsConfig();
|
||||||
|
return Object.values(config.types)
|
||||||
|
.filter(t => t.public)
|
||||||
|
.map(t => `posts/${t.key}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all storage public prefixes from every enabled module (internal + external).
|
||||||
|
* @returns {string[]} Deduplicated list of public storage prefixes
|
||||||
|
*/
|
||||||
|
export function getAllStoragePublicPrefixes() {
|
||||||
|
const prefixes = new Set();
|
||||||
|
|
||||||
|
// Internal modules — call server-only config helpers directly to avoid
|
||||||
|
// importing module.config.js files that contain React lazy() references.
|
||||||
|
for (const prefix of getPostsPublicPrefixes()) {
|
||||||
|
prefixes.add(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
// External modules — runtime registry
|
||||||
|
for (const mod of getEnabledModules()) {
|
||||||
|
if (!mod.external) continue;
|
||||||
|
const runtimeConfig = getModule(mod.name);
|
||||||
|
for (const prefix of runtimeConfig?.storagePublicPrefixes ?? []) {
|
||||||
|
prefixes.add(prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...prefixes];
|
||||||
|
}
|
||||||
@@ -7,10 +7,17 @@ ZEN_MODULE_POSTS=true
|
|||||||
ZEN_MODULE_ZEN_MODULE_POSTS_TYPES=blogue:Blogue|cve:CVE|emploi:Emplois
|
ZEN_MODULE_ZEN_MODULE_POSTS_TYPES=blogue:Blogue|cve:CVE|emploi:Emplois
|
||||||
|
|
||||||
# Fields for each type: name:type|name:type|...
|
# Fields for each type: name:type|name:type|...
|
||||||
# Supported field types: title, slug, text, markdown, date, category, image
|
# Supported field types: title, slug, text, markdown, date, datetime, color, category, image
|
||||||
# Relation field: name:relation:target_post_type (e.g. keywords:relation:mots-cle)
|
# Relation field: name:relation:target_post_type (e.g. keywords:relation:mots-cle)
|
||||||
ZEN_MODULE_POSTS_TYPE_BLOGUE=title:title|slug:slug|category:category|date:date|resume:text|content:markdown|image:image
|
ZEN_MODULE_POSTS_TYPE_BLOGUE=title:title|slug:slug|category:category|date:date|resume:text|content:markdown|image:image
|
||||||
ZEN_MODULE_POSTS_TYPE_CVE=title:title|slug:slug|cve_id:text|severity:text|description:markdown|date:date|keywords:relation:mots-cle
|
ZEN_MODULE_POSTS_TYPE_CVE=title:title|slug:slug|cve_id:text|severity:text|description:markdown|date:date|keywords:relation:mots-cle
|
||||||
ZEN_MODULE_POSTS_TYPE_EMPLOI=title:title|slug:slug|date:date|description:markdown
|
ZEN_MODULE_POSTS_TYPE_EMPLOI=title:title|slug:slug|date:date|description:markdown
|
||||||
ZEN_MODULE_POSTS_TYPE_MOTS-CLE=title:title|slug:slug
|
ZEN_MODULE_POSTS_TYPE_MOTS-CLE=title:title|slug:slug
|
||||||
|
|
||||||
|
# Public storage access per type (optional, default: false)
|
||||||
|
# When true, images of that type are served without authentication.
|
||||||
|
# Files are stored at posts/{type}/{id}/{filename} and accessible via /zen/api/storage/posts/{type}/...
|
||||||
|
ZEN_MODULE_POSTS_TYPE_BLOGUE_PUBLIC=true
|
||||||
|
# ZEN_MODULE_POSTS_TYPE_CVE_PUBLIC=false
|
||||||
|
# ZEN_MODULE_POSTS_TYPE_EMPLOI_PUBLIC=false
|
||||||
#################################
|
#################################
|
||||||
|
|||||||
@@ -29,6 +29,16 @@ Chaque type doit avoir au moins un champ `title` et un champ `slug`.
|
|||||||
|
|
||||||
Si un type utilise le champ `image`, configurer le stockage Zen dans le `.env` principal : `ZEN_STORAGE_REGION`, `ZEN_STORAGE_ACCESS_KEY`, `ZEN_STORAGE_SECRET_KEY`, `ZEN_STORAGE_BUCKET`.
|
Si un type utilise le champ `image`, configurer le stockage Zen dans le `.env` principal : `ZEN_STORAGE_REGION`, `ZEN_STORAGE_ACCESS_KEY`, `ZEN_STORAGE_SECRET_KEY`, `ZEN_STORAGE_BUCKET`.
|
||||||
|
|
||||||
|
### Accès public aux images
|
||||||
|
|
||||||
|
Par défaut, les images d'un type nécessitent une session authentifiée. Pour les rendre accessibles publiquement (ex. images de blogue affichées sur le site) :
|
||||||
|
|
||||||
|
```env
|
||||||
|
ZEN_MODULE_POSTS_TYPE_BLOGUE_PUBLIC=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Les images sont stockées sous `posts/{type}/{id}/{filename}` et servies via `/zen/api/storage/posts/{type}/...`. L'accès public est déclaré dans le module. Aucune variable d'environnement globale n'est nécessaire.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Base de données
|
## Base de données
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
uploadImage,
|
uploadImage,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
generateBlogFilePath,
|
generatePostFilePath,
|
||||||
generateUniqueFilename,
|
generateUniqueFilename,
|
||||||
validateUpload,
|
validateUpload,
|
||||||
FILE_TYPE_PRESETS,
|
FILE_TYPE_PRESETS,
|
||||||
@@ -187,8 +187,11 @@ async function handleUploadImage(request) {
|
|||||||
try {
|
try {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const file = formData.get('file');
|
const file = formData.get('file');
|
||||||
|
const postType = formData.get('type');
|
||||||
|
|
||||||
if (!file) return { success: false, error: 'No file provided' };
|
if (!file) return { success: false, error: 'No file provided' };
|
||||||
|
if (!postType) return { success: false, error: 'Post type is required' };
|
||||||
|
if (!getPostType(postType)) return { success: false, error: `Unknown post type: ${postType}` };
|
||||||
|
|
||||||
const validation = validateUpload({
|
const validation = validateUpload({
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
@@ -202,7 +205,7 @@ async function handleUploadImage(request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uniqueFilename = generateUniqueFilename(file.name);
|
const uniqueFilename = generateUniqueFilename(file.name);
|
||||||
const key = generateBlogFilePath(Date.now(), uniqueFilename);
|
const key = generatePostFilePath(postType, Date.now(), uniqueFilename);
|
||||||
const buffer = Buffer.from(await file.arrayBuffer());
|
const buffer = Buffer.from(await file.arrayBuffer());
|
||||||
|
|
||||||
const uploadResult = await uploadImage({
|
const uploadResult = await uploadImage({
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ function buildConfig() {
|
|||||||
const hasCategory = fields.some(f => f.type === 'category');
|
const hasCategory = fields.some(f => f.type === 'category');
|
||||||
const hasRelations = fields.some(f => f.type === 'relation');
|
const hasRelations = fields.some(f => f.type === 'relation');
|
||||||
const label = customLabel || (key.charAt(0).toUpperCase() + key.slice(1));
|
const label = customLabel || (key.charAt(0).toUpperCase() + key.slice(1));
|
||||||
|
const isPublic = process.env[`ZEN_MODULE_POSTS_TYPE_${key.toUpperCase()}_PUBLIC`] === 'true';
|
||||||
|
|
||||||
types[key] = {
|
types[key] = {
|
||||||
key,
|
key,
|
||||||
@@ -91,6 +92,7 @@ function buildConfig() {
|
|||||||
hasRelations,
|
hasRelations,
|
||||||
titleField,
|
titleField,
|
||||||
slugField,
|
slugField,
|
||||||
|
public: isPublic,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Authentification requise.
|
|||||||
| `POST` | `/zen/api/admin/posts/posts?type={type}` | Créer un post |
|
| `POST` | `/zen/api/admin/posts/posts?type={type}` | Créer un post |
|
||||||
| `PUT` | `/zen/api/admin/posts/posts?type={type}&id={id}` | Modifier un post |
|
| `PUT` | `/zen/api/admin/posts/posts?type={type}&id={id}` | Modifier un post |
|
||||||
| `DELETE` | `/zen/api/admin/posts/posts?type={type}&id={id}` | Supprimer un post |
|
| `DELETE` | `/zen/api/admin/posts/posts?type={type}&id={id}` | Supprimer un post |
|
||||||
| `POST` | `/zen/api/admin/posts/upload-image` | Upload d'image |
|
| `POST` | `/zen/api/admin/posts/upload-image` | Upload d'image (`multipart/form-data` : `file`, `type`) |
|
||||||
| `GET` | `/zen/api/admin/posts/categories?type={type}` | Liste des catégories |
|
| `GET` | `/zen/api/admin/posts/categories?type={type}` | Liste des catégories |
|
||||||
| `POST` | `/zen/api/admin/posts/categories?type={type}` | Créer une catégorie |
|
| `POST` | `/zen/api/admin/posts/categories?type={type}` | Créer une catégorie |
|
||||||
| `PUT` | `/zen/api/admin/posts/categories?type={type}&id={id}` | Modifier une catégorie |
|
| `PUT` | `/zen/api/admin/posts/categories?type={type}&id={id}` | Modifier une catégorie |
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Intégration Next.js — Module Posts
|
# Intégration Next.js — Module Posts
|
||||||
|
|
||||||
|
> **Images** — Les images sont stockées sous `posts/{type}/{id}/{filename}` et servies via `/zen/api/storage/posts/{type}/...`. Pour qu'elles soient accessibles sans authentification (nécessaire pour l'affichage public), activer `ZEN_MODULE_POSTS_TYPE_{TYPE}_PUBLIC=true` dans le `.env`.
|
||||||
|
|
||||||
## Liste de posts
|
## Liste de posts
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|||||||
@@ -17,11 +17,17 @@ const CategoryEditPage = lazy(() => import('./categories/admin/CategoryEditPa
|
|||||||
|
|
||||||
const postsConfig = getPostsConfig();
|
const postsConfig = getPostsConfig();
|
||||||
|
|
||||||
// Build adminPages and navigation dynamically from configured post types
|
// Build adminPages, navigation and public storage prefixes dynamically from configured post types
|
||||||
const adminPages = {};
|
const adminPages = {};
|
||||||
const navigationSections = [];
|
const navigationSections = [];
|
||||||
|
const storagePublicPrefixes = [];
|
||||||
|
|
||||||
for (const type of Object.values(postsConfig.types)) {
|
for (const type of Object.values(postsConfig.types)) {
|
||||||
|
// Register public storage prefix for this type if marked public
|
||||||
|
if (type.public) {
|
||||||
|
storagePublicPrefixes.push(`posts/${type.key}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Register routes for this post type
|
// Register routes for this post type
|
||||||
adminPages[`/admin/posts/${type.key}/list`] = PostsListPage;
|
adminPages[`/admin/posts/${type.key}/list`] = PostsListPage;
|
||||||
adminPages[`/admin/posts/${type.key}/new`] = PostCreatePage;
|
adminPages[`/admin/posts/${type.key}/new`] = PostCreatePage;
|
||||||
@@ -84,6 +90,8 @@ export default defineModule({
|
|||||||
|
|
||||||
envVars: ['ZEN_MODULE_POSTS_TYPES'],
|
envVars: ['ZEN_MODULE_POSTS_TYPES'],
|
||||||
|
|
||||||
|
storagePublicPrefixes,
|
||||||
|
|
||||||
// Array of sections — one per post type (server-side, env vars available)
|
// Array of sections — one per post type (server-side, env vars available)
|
||||||
navigation: navigationSections,
|
navigation: navigationSections,
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user