157 lines
3.6 KiB
JavaScript
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,
|
|
};
|