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:
@@ -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
|
||||
|
||||
# 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)
|
||||
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_EMPLOI=title:title|slug:slug|date:date|description:markdown
|
||||
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`.
|
||||
|
||||
### 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
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
import {
|
||||
uploadImage,
|
||||
deleteFile,
|
||||
generateBlogFilePath,
|
||||
generatePostFilePath,
|
||||
generateUniqueFilename,
|
||||
validateUpload,
|
||||
FILE_TYPE_PRESETS,
|
||||
@@ -187,8 +187,11 @@ async function handleUploadImage(request) {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('file');
|
||||
const postType = formData.get('type');
|
||||
|
||||
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({
|
||||
filename: file.name,
|
||||
@@ -202,7 +205,7 @@ async function handleUploadImage(request) {
|
||||
}
|
||||
|
||||
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 uploadResult = await uploadImage({
|
||||
|
||||
@@ -82,6 +82,7 @@ function buildConfig() {
|
||||
const hasCategory = fields.some(f => f.type === 'category');
|
||||
const hasRelations = fields.some(f => f.type === 'relation');
|
||||
const label = customLabel || (key.charAt(0).toUpperCase() + key.slice(1));
|
||||
const isPublic = process.env[`ZEN_MODULE_POSTS_TYPE_${key.toUpperCase()}_PUBLIC`] === 'true';
|
||||
|
||||
types[key] = {
|
||||
key,
|
||||
@@ -91,6 +92,7 @@ function buildConfig() {
|
||||
hasRelations,
|
||||
titleField,
|
||||
slugField,
|
||||
public: isPublic,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Authentification requise.
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 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
|
||||
|
||||
```js
|
||||
|
||||
@@ -17,11 +17,17 @@ const CategoryEditPage = lazy(() => import('./categories/admin/CategoryEditPa
|
||||
|
||||
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 navigationSections = [];
|
||||
const storagePublicPrefixes = [];
|
||||
|
||||
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
|
||||
adminPages[`/admin/posts/${type.key}/list`] = PostsListPage;
|
||||
adminPages[`/admin/posts/${type.key}/new`] = PostCreatePage;
|
||||
@@ -84,6 +90,8 @@ export default defineModule({
|
||||
|
||||
envVars: ['ZEN_MODULE_POSTS_TYPES'],
|
||||
|
||||
storagePublicPrefixes,
|
||||
|
||||
// Array of sections — one per post type (server-side, env vars available)
|
||||
navigation: navigationSections,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user