This commit is contained in:
Stan
2026-04-19 21:14:16 +02:00
parent 0c74a75126
commit 28d167f11f
42 changed files with 5681 additions and 55 deletions
+113 -2
View File
@@ -3,22 +3,128 @@ import { Router } from 'express';
import {
getAppConfig,
getExportProfile,
getImageRules
getImageRules,
updateImageRules
} from '../services/configService.js';
import { logAuditEvent } from '../services/auditService.js';
import { asyncHandler } from '../utils/asyncHandler.js';
import { configCache } from '../services/cacheService.js';
const router = Router();
/*
* Image-rules validation is shared between server and admin frontend. The server
* acts as the final authority while the client validates proactively to give the
* administrator immediate feedback before the round-trip.
*/
function validateImageRulesPayload(payload) {
const oversizeBehaviors = ['auto_optimize', 'warn_then_optimize', 'block'];
const allowedMimeTypes = Array.isArray(payload.allowedMimeTypes)
? payload.allowedMimeTypes.filter((value) => typeof value === 'string' && value.trim())
: [];
if (!payload.name || typeof payload.name !== 'string') {
return 'Image policy name is required.';
}
if (!allowedMimeTypes.length) {
return 'At least one allowed MIME type is required.';
}
if (!Number.isInteger(payload.maxFileSizeBytes) || payload.maxFileSizeBytes <= 0) {
return 'Maximum file size must be a positive integer.';
}
if (!Number.isInteger(payload.maxWidthPx) || payload.maxWidthPx <= 0) {
return 'Maximum width must be a positive integer.';
}
if (!Number.isInteger(payload.maxHeightPx) || payload.maxHeightPx <= 0) {
return 'Maximum height must be a positive integer.';
}
if (!Number.isInteger(payload.jpegQuality) || payload.jpegQuality < 1 || payload.jpegQuality > 100) {
return 'JPEG quality must be an integer between 1 and 100.';
}
if (!oversizeBehaviors.includes(payload.oversizeBehavior)) {
return 'Oversize behavior is invalid.';
}
if (!Number.isInteger(payload.maxAttachmentsPerField) || payload.maxAttachmentsPerField <= 0) {
return 'Maximum attachments per field must be a positive integer.';
}
return null;
}
router.get(
'/image-rules',
asyncHandler(async (_req, res) => {
const cached = configCache.get('image-rules');
if (cached) {
return res.json(cached);
}
const imageRules = await getImageRules();
if (!imageRules) {
return res.status(404).json({ message: 'Image rules not found.' });
}
res.json(imageRules);
configCache.set('image-rules', imageRules);
return res.json(imageRules);
})
);
router.put(
'/image-rules',
asyncHandler(async (req, res) => {
/*
* Normalize incoming values before validation so the API can accept the
* browser form payload in a predictable shape. The frontend sends numbers as
* form values, but they still arrive over HTTP as strings until coerced.
*/
const payload = {
name: req.body?.name?.trim(),
allowedMimeTypes: Array.isArray(req.body?.allowedMimeTypes)
? req.body.allowedMimeTypes.map((value) => String(value).trim()).filter(Boolean)
: [],
maxFileSizeBytes: Number(req.body?.maxFileSizeBytes),
maxWidthPx: Number(req.body?.maxWidthPx),
maxHeightPx: Number(req.body?.maxHeightPx),
jpegQuality: Number(req.body?.jpegQuality),
oversizeBehavior: req.body?.oversizeBehavior,
maxAttachmentsPerField: Number(req.body?.maxAttachmentsPerField)
};
const validationMessage = validateImageRulesPayload(payload);
if (validationMessage) {
return res.status(400).json({ message: validationMessage });
}
/* Capture the current value before mutation for the audit trail. */
const previousRules = await getImageRules();
const imageRules = await updateImageRules(payload);
if (!imageRules) {
return res.status(404).json({ message: 'Image rules not found.' });
}
configCache.invalidate('image-rules');
await logAuditEvent({
entityType: 'image_rules',
entityCode: imageRules.code,
action: 'update',
oldValue: previousRules,
newValue: imageRules
});
return res.json(imageRules);
})
);
@@ -38,6 +144,11 @@ router.get(
router.get(
'/app-config',
asyncHandler(async (_req, res) => {
/*
* Generic application configuration is kept as a simple key/value list in
* the PoC. This avoids hardcoding small behavioral settings in the frontend
* while still keeping the schema easy to inspect and evolve.
*/
const config = await getAppConfig();
res.json({ items: config });
})