39948-vm/backend/src/middlewares/runtime-public.js
2026-05-04 17:30:32 +02:00

157 lines
3.6 KiB
JavaScript

const PUBLIC_RUNTIME_ENTITY_FIELDS = {
projects: [
'id',
'name',
'slug',
'description',
'logo_url',
'favicon_url',
'og_image_url',
],
tour_pages: [
'id',
'projectId',
'environment',
'source_key',
'name',
'slug',
'sort_order',
'background_image_url',
'background_video_url',
'background_audio_url',
'background_loop',
'requires_auth',
'ui_schema_json',
],
project_audio_tracks: [
'id',
'projectId',
'environment',
'source_key',
'name',
'slug',
'url',
'loop',
'volume',
'sort_order',
'is_enabled',
],
global_transition_defaults: [
'id',
'transition_type',
'duration_ms',
'easing',
'overlay_color',
],
project_transition_settings: [
'id',
'projectId',
'environment',
'transition_type',
'duration_ms',
'easing',
'overlay_color',
],
};
// Entity-aware path patterns for public runtime access
// Entities not listed here default to allowing only '/'
const PUBLIC_RUNTIME_ALLOWED_PATHS = {
project_transition_settings: [
'/',
/^\/project\/[a-fA-F0-9-]+\/env\/(dev|stage|production)$/,
],
};
const pickFields = (record, fields) => {
if (!record || typeof record !== 'object') {
return record;
}
// Convert Sequelize instance to plain object if needed
const plainRecord =
typeof record.get === 'function' ? record.get({ plain: true }) : record;
return fields.reduce((acc, field) => {
if (field in plainRecord && plainRecord[field] !== undefined) {
acc[field] = plainRecord[field];
}
return acc;
}, {});
};
const isPublicRuntimeReadRequest = (req) => {
return req.isRuntimePublicRequest === true && req.method === 'GET';
};
const blockNonPublicRuntimeListEndpoints = (entityName) => (req, res, next) => {
if (!isPublicRuntimeReadRequest(req)) {
return next();
}
const allowedPaths = PUBLIC_RUNTIME_ALLOWED_PATHS[entityName] || ['/'];
const pathMatches = allowedPaths.some((pattern) =>
pattern instanceof RegExp ? pattern.test(req.path) : req.path === pattern,
);
if (!pathMatches) {
return res.status(404).send({ message: 'Not found' });
}
if (req.query.filetype === 'csv') {
return res.status(404).send({ message: 'Not found' });
}
return next();
};
const sanitizePublicRuntimeListResponse = (entityName) => {
const fields = PUBLIC_RUNTIME_ENTITY_FIELDS[entityName] || [];
const allowedPaths = PUBLIC_RUNTIME_ALLOWED_PATHS[entityName] || ['/'];
return (req, res, next) => {
const pathMatches = allowedPaths.some((pattern) =>
pattern instanceof RegExp ? pattern.test(req.path) : req.path === pattern,
);
if (
!isPublicRuntimeReadRequest(req) ||
!pathMatches ||
fields.length === 0
) {
return next();
}
const originalSend = res.send.bind(res);
res.send = (body) => {
if (!body || typeof body !== 'object') {
return originalSend(body);
}
// Handle list responses with rows array
if (Array.isArray(body.rows)) {
const sanitizedRows = body.rows.map((row) => pickFields(row, fields));
return originalSend({
...body,
rows: sanitizedRows,
});
}
// Handle single object responses (e.g., from findOne or project/:id/env/:env)
if (!Array.isArray(body) && body !== null) {
return originalSend(pickFields(body, fields));
}
return originalSend(body);
};
return next();
};
};
module.exports = {
blockNonPublicRuntimeListEndpoints,
sanitizePublicRuntimeListResponse,
};