Files
CLProject/src/routes/configRoutes.js
T
2026-04-22 22:39:26 +02:00

225 lines
6.3 KiB
JavaScript

import { Router } from 'express';
import {
getExportProfile,
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();
/**
* @openapi
* /api/v1/config/image-rules:
* get:
* summary: Get image validation rules
* tags:
* - Configuration
* security:
* - bearerAuth: []
* - cookieAuth: []
* responses:
* 200:
* description: Image rules configuration
* 404:
* description: Image rules not found
* put:
* summary: Update image validation rules
* tags:
* - Configuration
* security:
* - bearerAuth: []
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - name
* - allowedMimeTypes
* - maxFileSizeBytes
* - maxWidthPx
* - maxHeightPx
* - jpegQuality
* - oversizeBehavior
* - maxAttachmentsPerField
* properties:
* name:
* type: string
* allowedMimeTypes:
* type: array
* items:
* type: string
* maxFileSizeBytes:
* type: integer
* maxWidthPx:
* type: integer
* maxHeightPx:
* type: integer
* jpegQuality:
* type: integer
* minimum: 1
* maximum: 100
* oversizeBehavior:
* type: string
* enum: [auto_optimize, warn_then_optimize, block]
* maxAttachmentsPerField:
* type: integer
* responses:
* 200:
* description: Image rules updated
* 400:
* description: Invalid input
* 404:
* description: Image rules not found
* /api/v1/config/export:
* get:
* summary: Get export profile configuration
* tags:
* - Configuration
* security:
* - bearerAuth: []
* - cookieAuth: []
* responses:
* 200:
* description: Export profile configuration
* 404:
* description: Export profile not found
*/
/*
* 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.' });
}
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);
})
);
router.get(
'/export',
asyncHandler(async (_req, res) => {
const exportProfile = await getExportProfile();
if (!exportProfile) {
return res.status(404).json({ message: 'Export profile not found.' });
}
res.json(exportProfile);
})
);
export default router;