Initial Product release
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
const { fetchTemplateById } = require('./templates');
|
||||
const { parseJsonSafe } = require('./utils');
|
||||
|
||||
const ALLOWED_RICH_TEXT_TAGS = ['b', 'strong', 'i', 'em', 'u', 'br', 'p', 'div', 'ul', 'ol', 'li'];
|
||||
|
||||
function sanitizeRichText(html) {
|
||||
let output = String(html || '');
|
||||
output = output.replace(/<script[\s\S]*?<\/script>/gi, '');
|
||||
output = output.replace(/<style[\s\S]*?<\/style>/gi, '');
|
||||
return output.replace(/<[^>]+>/g, (tag) => {
|
||||
const match = tag.match(/^<\s*(\/?)\s*([a-z0-9]+)(?:\s[^>]*)?>$/i);
|
||||
if (!match) {
|
||||
return '';
|
||||
}
|
||||
const closing = Boolean(match[1]);
|
||||
const name = String(match[2] || '').toLowerCase();
|
||||
if (!ALLOWED_RICH_TEXT_TAGS.includes(name)) {
|
||||
return '';
|
||||
}
|
||||
if (name === 'br') {
|
||||
return '<br>';
|
||||
}
|
||||
return closing ? `</${name}>` : `<${name}>`;
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchSlideById(pool, id) {
|
||||
const [slides] = await pool.query(`
|
||||
SELECT s.id, s.title, s.body, s.template_id, s.content_json, s.media_path, s.media_type, s.created_at, s.modified_at,
|
||||
st.name AS template_name, cs.name AS canvas_size_name, cs.width AS canvas_width, cs.height AS canvas_height
|
||||
FROM slides s
|
||||
LEFT JOIN slide_templates st ON st.id = s.template_id
|
||||
LEFT JOIN canvas_sizes cs ON cs.id = st.canvas_size_id
|
||||
WHERE s.id = ?
|
||||
`, [id]);
|
||||
if (!slides.length) {
|
||||
return null;
|
||||
}
|
||||
const slide = slides[0];
|
||||
slide.content = parseJsonSafe(slide.content_json) || {};
|
||||
if (slide.template_id) {
|
||||
slide.template = await fetchTemplateById(pool, slide.template_id);
|
||||
}
|
||||
return slide;
|
||||
}
|
||||
|
||||
function getFilesByField(files) {
|
||||
const map = {};
|
||||
(files || []).forEach((file) => {
|
||||
map[file.fieldname] = file;
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
function sanitizeTextColor(value, fallback) {
|
||||
const raw = String(value || '').trim();
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(raw) || /^#[0-9a-fA-F]{3}$/.test(raw)) {
|
||||
return raw;
|
||||
}
|
||||
return fallback || '#000000';
|
||||
}
|
||||
|
||||
function getTextRegionStyle(body, region, existingContent) {
|
||||
const existing = existingContent && existingContent[region.region_key] ? existingContent[region.region_key] : {};
|
||||
const fontFamily = String(body[`region_font_family_${region.id}`] || existing.font_family || region.font_family || 'Arial').trim() || 'Arial';
|
||||
const fontSize = Math.max(8, Number(body[`region_font_size_${region.id}`] || existing.font_size || 24));
|
||||
const fontColor = sanitizeTextColor(body[`region_font_color_${region.id}`] || existing.font_color || region.font_color || '#000000');
|
||||
return {
|
||||
font_family: fontFamily,
|
||||
font_size: fontSize,
|
||||
font_color: fontColor
|
||||
};
|
||||
}
|
||||
|
||||
function buildTemplateContent(template, body, filesByField, existingContent) {
|
||||
const content = {};
|
||||
template.regions.forEach((region) => {
|
||||
if (region.region_type === 'image') {
|
||||
const uploaded = filesByField[`region_image_${region.id}`];
|
||||
const existing = body[`existing_region_image_${region.id}`];
|
||||
content[region.region_key] = {
|
||||
type: 'image',
|
||||
value: uploaded ? `/uploads/${uploaded.filename}` : String(existing || '').trim() || ((existingContent && existingContent[region.region_key]) ? existingContent[region.region_key].value : '')
|
||||
};
|
||||
} else if (region.region_type === 'webpage') {
|
||||
const submitted = body[`region_webpage_${region.id}`];
|
||||
const current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key].value : '';
|
||||
content[region.region_key] = {
|
||||
type: 'webpage',
|
||||
value: submitted === undefined ? current : String(submitted || '').trim()
|
||||
};
|
||||
} else if (region.region_type === 'html') {
|
||||
const submitted = body[`region_html_${region.id}`];
|
||||
const current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key].value : '';
|
||||
content[region.region_key] = {
|
||||
type: 'html',
|
||||
value: submitted === undefined ? current : String(submitted || '')
|
||||
};
|
||||
} else {
|
||||
const submitted = body[`region_text_${region.id}`];
|
||||
const current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key].value : '';
|
||||
const style = getTextRegionStyle(body, region, existingContent);
|
||||
content[region.region_key] = {
|
||||
type: 'text',
|
||||
value: submitted === undefined ? current : String(submitted),
|
||||
font_family: style.font_family,
|
||||
font_size: style.font_size,
|
||||
font_color: style.font_color
|
||||
};
|
||||
}
|
||||
});
|
||||
return content;
|
||||
}
|
||||
|
||||
async function buildSlidePayload(pool, req, existingSlide) {
|
||||
const title = String(req.body.title || '').trim();
|
||||
const templateId = req.body.template_id ? Number(req.body.template_id) : null;
|
||||
const filesByField = getFilesByField(req.files || []);
|
||||
const template = templateId ? await fetchTemplateById(pool, templateId) : null;
|
||||
const existingContent = existingSlide ? parseJsonSafe(existingSlide.content_json) || {} : {};
|
||||
|
||||
if (!title) {
|
||||
const error = new Error('Slide title is required.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (templateId && !template) {
|
||||
const error = new Error('Template not found.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (template) {
|
||||
return {
|
||||
title,
|
||||
body: existingSlide ? existingSlide.body : null,
|
||||
templateId: template.id,
|
||||
contentJson: JSON.stringify(buildTemplateContent(template, req.body, filesByField, existingContent)),
|
||||
mediaPath: null,
|
||||
mediaType: null
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
body: existingSlide ? existingSlide.body : null,
|
||||
templateId: null,
|
||||
contentJson: existingSlide ? existingSlide.content_json : null,
|
||||
mediaPath: existingSlide ? existingSlide.media_path : null,
|
||||
mediaType: existingSlide ? existingSlide.media_type : null
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchSlideById,
|
||||
buildSlidePayload
|
||||
};
|
||||
Reference in New Issue
Block a user