const fs = require('fs'); const path = require('path'); module.exports = function registerAdminContentRoutes(app, deps) { const pool = deps.pool; const common = deps.common; const pages = deps.pages; const upload = deps.upload; const fetchScreensBySlideId = deps.fetchScreensBySlideId; const fetchScreensByTemplateId = deps.fetchScreensByTemplateId; const collectUploadReferencesFromSlide = deps.collectUploadReferencesFromSlide; const collectUploadReferencesFromTemplate = deps.collectUploadReferencesFromTemplate; const collectUploadReferencesFromPayload = deps.collectUploadReferencesFromPayload; const removeUnusedUploadFiles = deps.removeUnusedUploadFiles; const syncPlaylistUploadsOnChange = deps.syncPlaylistUploadsOnChange; const getAuditUserId = deps.getAuditUserId; const redirectAfterSave = deps.redirectAfterSave; const notifyPlayerScreens = deps.notifyPlayerScreens; const broadcastDashboardState = deps.broadcastDashboardState; const backgroundTaskQueue = deps.backgroundTaskQueue; const getSlideDeleteBlockMessage = deps.getSlideDeleteBlockMessage; const getTemplateDeleteBlockMessage = deps.getTemplateDeleteBlockMessage; const getCanvasSizeDeleteBlockMessage = deps.getCanvasSizeDeleteBlockMessage; const requirePermission = deps.requirePermission; const hasAnyPermission = deps.hasAnyPermission; const { buildPagination } = require('../../lib/pagination'); const LIST_PAGE_SIZE = 10; const IMAGE_UPLOAD_MAX_BYTES = 100 * 1024 * 1024; const VIDEO_UPLOAD_MAX_BYTES = 1024 * 1024 * 1024; if (!pool || !common || !pages || !upload || typeof fetchScreensBySlideId !== 'function' || typeof fetchScreensByTemplateId !== 'function' || typeof collectUploadReferencesFromSlide !== 'function' || typeof collectUploadReferencesFromTemplate !== 'function' || typeof collectUploadReferencesFromPayload !== 'function' || typeof removeUnusedUploadFiles !== 'function' || typeof syncPlaylistUploadsOnChange !== 'function' || typeof getAuditUserId !== 'function' || typeof redirectAfterSave !== 'function' || typeof notifyPlayerScreens !== 'function' || typeof broadcastDashboardState !== 'function' || typeof getSlideDeleteBlockMessage !== 'function' || typeof getTemplateDeleteBlockMessage !== 'function' || typeof getCanvasSizeDeleteBlockMessage !== 'function' || typeof hasAnyPermission !== 'function') { throw new Error('registerAdminContentRoutes requires the content route dependencies.'); } function requireSlideUploadPermission(req, res, next) { if (!req.currentUser) { return res.redirect('/login?message=' + encodeURIComponent('Please sign in to continue.')); } if (hasAnyPermission(req.currentUser, ['slides.create', 'slides.update'])) { return next(); } const error = new Error('You do not have permission to access this area.'); error.statusCode = 403; error.expose = true; next(error); } function getUploadedFileMediaType(file) { const mimeType = String(file && file.mimetype || '').trim().toLowerCase(); const extension = path.extname(String(file && file.originalname || '')).toLowerCase(); if (mimeType.indexOf('video/') === 0 || ['.mp4', '.webm', '.ogg', '.ogv', '.mov', '.avi', '.mkv'].indexOf(extension) !== -1) { return 'video'; } if (mimeType.indexOf('image/') === 0 || ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.avif', '.tif', '.tiff'].indexOf(extension) !== -1) { return 'image'; } return null; } function getUploadedFileLimitBytes(file) { return getUploadedFileMediaType(file) === 'video' ? VIDEO_UPLOAD_MAX_BYTES : IMAGE_UPLOAD_MAX_BYTES; } function getUploadedFileLimitLabel(file) { return getUploadedFileMediaType(file) === 'video' ? '1 GB' : '100 MB'; } async function removeUploadedFile(file) { if (!file || !file.filename || !deps.uploadDir) { return; } const filePath = path.join(deps.uploadDir, file.filename); try { await fs.promises.unlink(filePath); } catch (_error) { // Ignore cleanup failures. } } async function validateUploadedFiles(files) { const list = Array.isArray(files) ? files.filter(Boolean) : []; for (let i = 0; i < list.length; i += 1) { const file = list[i]; const mediaType = getUploadedFileMediaType(file); if (!mediaType) { await removeUploadedFile(file); const error = new Error('Unsupported upload type.'); error.statusCode = 400; error.expose = true; throw error; } if (Number(file.size || 0) > getUploadedFileLimitBytes(file)) { await removeUploadedFile(file); const error = new Error('File must be ' + getUploadedFileLimitLabel(file) + ' or smaller.'); error.statusCode = 400; error.expose = true; throw error; } } } async function fetchTemplateSlideCount(templateId) { const [rows] = await pool.query('SELECT COUNT(*) AS slide_count FROM slides WHERE template_id = ?', [templateId]); return Number(rows[0] && rows[0].slide_count) || 0; } async function fetchTemplateRegionUsage(template) { const [slides] = await pool.query('SELECT content_json FROM slides WHERE template_id = ?', [template.id]); const usage = new Set(); const regionKeys = new Set((Array.isArray(template && template.regions) ? template.regions : []) .map((region) => String(region && region.region_key || '').trim()) .filter(Boolean)); if (!regionKeys.size || !slides.length) { return []; } slides.forEach((slide) => { const content = typeof common.parseJsonSafe === 'function' ? common.parseJsonSafe(slide.content_json) : null; if (!content || typeof content !== 'object') { return; } regionKeys.forEach((regionKey) => { if (Object.prototype.hasOwnProperty.call(content, regionKey)) { usage.add(regionKey); } }); }); return Array.from(usage); } function getRemovedTemplateRegionKeys(existingRegions, nextRegions) { const nextKeys = new Set((Array.isArray(nextRegions) ? nextRegions : []).map((region) => String(region && region.region_key || '').trim()).filter(Boolean)); return (Array.isArray(existingRegions) ? existingRegions : []) .map((region) => String(region && region.region_key || '').trim()) .filter((regionKey) => regionKey && !nextKeys.has(regionKey)); } async function getTemplateRegionDeleteBlockMessage(template, nextRegions) { const removedRegionKeys = getRemovedTemplateRegionKeys(template && template.regions ? template.regions : [], nextRegions); if (!removedRegionKeys.length) { return ''; } const usedRegionKeys = Array.isArray(template && template.region_usage) ? template.region_usage : []; const blockedKeys = removedRegionKeys.filter((regionKey) => usedRegionKeys.includes(regionKey)); return blockedKeys.length > 0 ? 'This region is still used by one or more slides.' : ''; } function queueSlideThumbnailRefresh(slideId, previousThumbnailPath) { if (!backgroundTaskQueue || typeof backgroundTaskQueue.enqueueTask !== 'function') { return Promise.resolve(null); } return backgroundTaskQueue.enqueueTask({ key: 'slide-thumbnail:' + Number(slideId), title: 'Refresh slide thumbnail', category: 'slides', taskType: 'slide-thumbnail-refresh', payload: { slideId: Number(slideId), previousThumbnailPath: String(previousThumbnailPath || '').trim() }, persist: true }); } async function canvasSizeExists(width, height, ignoreId) { const params = [width, height]; let query = 'SELECT COUNT(*) AS count FROM canvas_sizes WHERE width = ? AND height = ?'; if (ignoreId) { query += ' AND id <> ?'; params.push(ignoreId); } const [rows] = await pool.query(query, params); return Number(rows[0] && rows[0].count) > 0; } async function fetchScreenSlideCountsBySlug(screenSlugs) { const uniqueSlugs = Array.from(new Set(Array.isArray(screenSlugs) ? screenSlugs : [])).map(function (value) { return String(value || '').trim(); }).filter(Boolean); if (!uniqueSlugs.length) { return {}; } const [rows] = await pool.query( `SELECT s.slug, COUNT(ps.id) AS slide_count FROM screens s LEFT JOIN playlist_slides ps ON ps.playlist_id = s.playlist_id WHERE s.slug IN (?) GROUP BY s.id, s.slug`, [uniqueSlugs] ); return rows.reduce(function (counts, row) { counts[String(row.slug || '').trim()] = Number(row.slide_count) || 0; return counts; }, {}); } app.get('/slides', requirePermission('slides.read'), async function (req, res, next) { try { const page = Math.max(1, Math.floor(Number(req.query.page) || 1)); const search = common.getSearchQuery(req); const sort = common.getSortQuery(req); const direction = common.getSortDirectionQuery(req); const data = await common.fetchSlidesPage(pool, page, LIST_PAGE_SIZE, search, sort, direction); res.send(pages.renderSlidesPage({ slides: data.slides || [], pagination: buildPagination(data.totalItems, data.currentPage, 'page', { search: search, sort: sort, direction: direction }, LIST_PAGE_SIZE, 'slides', 'Slide pages') }, req.query.message ? String(req.query.message) : '', req.currentUser)); } catch (error) { next(error); } }); app.get('/slides/new', requirePermission('slides.create'), async function (req, res, next) { try { const data = await common.fetchTemplatesData(pool); const rssData = await common.fetchRssFeedsData(pool); const apiData = typeof common.fetchApiSourcesData === 'function' ? await common.fetchApiSourcesData(pool) : { apiSources: [] }; const apiSources = Array.isArray(apiData.apiSources) ? apiData.apiSources.map(function (source) { return Object.assign({}, source, { responseJson: typeof common.parseJsonSafe === 'function' ? common.parseJsonSafe(source.last_response_json) : null }); }) : []; const rssFeeds = await Promise.all((rssData.rssFeeds || []).map(async function (feed) { const items = typeof common.fetchRssFeedItemsByFeedId === 'function' ? await common.fetchRssFeedItemsByFeedId(pool, feed.id) : []; return Object.assign({}, feed, { items: items.map(function (item) { return typeof common.normalizeRssFeedItem === 'function' ? common.normalizeRssFeedItem(item) : item; }) }); })); Object.assign(data, rssData, apiData, { rssFeeds: rssFeeds, apiSources: apiSources }); res.send(pages.renderSlideFormPage(data, 'create', null, req.query.message ? String(req.query.message) : '', req.currentUser)); } catch (error) { next(error); } }); app.get('/slides/:id/edit', requirePermission('slides.update'), async function (req, res, next) { try { const slide = await common.fetchSlideById(pool, Number(req.params.id)); if (!slide) { return res.status(404).send('Slide not found'); } const data = await common.fetchTemplatesData(pool); const rssData = await common.fetchRssFeedsData(pool); const apiData = typeof common.fetchApiSourcesData === 'function' ? await common.fetchApiSourcesData(pool) : { apiSources: [] }; const apiSources = Array.isArray(apiData.apiSources) ? apiData.apiSources.map(function (source) { return Object.assign({}, source, { responseJson: typeof common.parseJsonSafe === 'function' ? common.parseJsonSafe(source.last_response_json) : null }); }) : []; const rssFeeds = await Promise.all((rssData.rssFeeds || []).map(async function (feed) { const items = typeof common.fetchRssFeedItemsByFeedId === 'function' ? await common.fetchRssFeedItemsByFeedId(pool, feed.id) : []; return Object.assign({}, feed, { items: items.map(function (item) { return typeof common.normalizeRssFeedItem === 'function' ? common.normalizeRssFeedItem(item) : item; }) }); })); Object.assign(data, rssData, apiData, { rssFeeds: rssFeeds, apiSources: apiSources }); res.send(pages.renderSlideFormPage(data, 'edit', slide, req.query.message ? String(req.query.message) : '', req.currentUser)); } catch (error) { next(error); } }); app.post('/slides/uploads', requireSlideUploadPermission, upload.single('file'), async function (req, res, next) { try { if (!req.file) { return res.status(400).json({ error: 'No file was uploaded.' }); } await validateUploadedFiles([req.file]); res.json({ path: '/media/uploads/' + req.file.filename, filename: req.file.filename, originalName: req.file.originalname || '' }); } catch (error) { next(error); } }); app.post('/slides/uploads/cleanup', requireSlideUploadPermission, async function (req, res, next) { try { const uploadPaths = Array.isArray(req.body && req.body.uploadPaths) ? req.body.uploadPaths : Array.isArray(req.body && req.body.paths) ? req.body.paths : []; await removeUnusedUploadFiles(pool, deps.uploadDir, uploadPaths); res.json({ ok: true }); } catch (error) { next(error); } }); app.post('/slides', requirePermission('slides.create'), upload.any(), async function (req, res, next) { try { await validateUploadedFiles(req.files || []); const payload = await common.buildSlidePayload(pool, req, null); if (await common.fetchDuplicateName(pool, 'slides', payload.title, null, 'title')) { return res.status(400).send('A slide with that title already exists.'); } const actorId = getAuditUserId(req); const [result] = await pool.query( 'INSERT INTO slides (title, body, template_id, content_json, media_path, media_type, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [payload.title, payload.body, payload.templateId, payload.contentJson, payload.mediaPath, payload.mediaType, actorId, actorId] ); await syncPlaylistUploadsOnChange({ key: 'slide:create:' + result.insertId, pool: pool, localUploadDir: deps.uploadDir, nextUploadRefs: collectUploadReferencesFromPayload(payload) }); queueSlideThumbnailRefresh(result.insertId, null).catch(function (error) { console.warn('Unable to queue slide thumbnail refresh:', error); }); redirectAfterSave(req, res, '/slides/' + result.insertId + '/edit', { closeUrl: '/slides', newUrl: '/slides/new', message: 'Slide created.' }); } catch (error) { next(error); } }); app.post('/slides/:id', requirePermission('slides.update'), upload.any(), async function (req, res, next) { try { await validateUploadedFiles(req.files || []); const slide = await common.fetchSlideById(pool, Number(req.params.id)); if (!slide) { return res.status(404).send('Slide not found'); } const affectedScreens = await fetchScreensBySlideId(pool, slide.id); const screenSlideCounts = await fetchScreenSlideCountsBySlug(affectedScreens); const existingUploadRefs = collectUploadReferencesFromSlide(slide); const payload = await common.buildSlidePayload(pool, req, slide); if (await common.fetchDuplicateName(pool, 'slides', payload.title, slide.id, 'title')) { return res.status(400).send('A slide with that title already exists.'); } const nextUploadRefs = collectUploadReferencesFromPayload(payload); const actorId = getAuditUserId(req); await pool.query( 'UPDATE slides SET title = ?, body = ?, template_id = ?, content_json = ?, media_path = ?, media_type = ?, modified_by = ? WHERE id = ?', [payload.title, payload.body, payload.templateId, payload.contentJson, payload.mediaPath, payload.mediaType, actorId, slide.id] ); await syncPlaylistUploadsOnChange({ key: 'slide:update:' + slide.id, pool: pool, localUploadDir: deps.uploadDir, previousUploadRefs: existingUploadRefs, nextUploadRefs: nextUploadRefs, blockedSlideIds: [slide.id], refreshScreenSlugs: affectedScreens, screenSlideCounts: screenSlideCounts }); await broadcastDashboardState(); queueSlideThumbnailRefresh(slide.id, slide.thumbnail_path).catch(function (error) { console.warn('Unable to queue slide thumbnail refresh:', error); }); redirectAfterSave(req, res, '/slides/' + slide.id + '/edit', { closeUrl: '/slides', newUrl: '/slides/new', message: 'Slide updated.' }); } catch (error) { next(error); } }); app.post('/slides/:id/delete', requirePermission('slides.delete'), async function (req, res, next) { try { const slide = await common.fetchSlideById(pool, Number(req.params.id)); if (!slide) { return res.status(404).send('Slide not found'); } const blockMessage = await getSlideDeleteBlockMessage(pool, slide); if (blockMessage) { return res.redirect('/slides?message=' + encodeURIComponent(blockMessage)); } const affectedScreens = await fetchScreensBySlideId(pool, slide.id); const screenSlideCounts = await fetchScreenSlideCountsBySlug(affectedScreens); const uploadRefs = collectUploadReferencesFromSlide(slide); await pool.query('DELETE FROM slides WHERE id = ?', [slide.id]); await syncPlaylistUploadsOnChange({ key: 'slide:delete:' + slide.id, pool: pool, localUploadDir: deps.uploadDir, previousUploadRefs: uploadRefs, blockedSlideIds: [slide.id], refreshScreenSlugs: affectedScreens, screenSlideCounts: screenSlideCounts }); await broadcastDashboardState(); res.redirect('/slides?message=' + encodeURIComponent('Slide deleted.')); } catch (error) { next(error); } }); app.get('/templates', requirePermission('templates.read'), async function (req, res, next) { try { const page = Math.max(1, Math.floor(Number(req.query.page) || 1)); const search = common.getSearchQuery(req); const sort = common.getSortQuery(req); const direction = common.getSortDirectionQuery(req); const data = await common.fetchTemplatesPage(pool, page, LIST_PAGE_SIZE, search, sort, direction); res.send(pages.renderTemplatesPage({ templates: data.templates || [], pagination: buildPagination(data.totalItems, data.currentPage, 'page', { search: search, sort: sort, direction: direction }, LIST_PAGE_SIZE, 'templates', 'Template pages') }, req.query.message ? String(req.query.message) : '', req.currentUser)); } catch (error) { next(error); } }); app.get('/templates/new', requirePermission('templates.create'), async function (req, res, next) { try { const data = await common.fetchCanvasSizesData(pool); res.send(pages.renderTemplateFormPage(null, 'create', req.query.message ? String(req.query.message) : '', data.canvasSizes, req.currentUser)); } catch (error) { next(error); } }); app.post('/templates', requirePermission('templates.create'), upload.any(), async function (req, res, next) { try { const payload = await common.buildTemplatePayload(pool, req, null); if (await common.fetchDuplicateName(pool, 'slide_templates', payload.name)) { return res.redirect('/templates/new?message=' + encodeURIComponent('A template with that name already exists.')); } const actorId = getAuditUserId(req); const [result] = await pool.query( 'INSERT INTO slide_templates (name, canvas_size_id, background_image_path, background_color, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?)', [payload.name, payload.canvasSizeId, payload.backgroundImagePath, payload.backgroundColor, actorId, actorId] ); for (let i = 0; i < payload.regions.length; i += 1) { const region = payload.regions[i]; await pool.query( 'INSERT INTO slide_template_regions (template_id, region_key, region_type, label, font_family, lock_ratio, x, y, width, height, z_index, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [result.insertId, region.region_key, region.region_type, region.label, region.font_family, region.lock_ratio, region.x, region.y, region.width, region.height, region.z_index, actorId, actorId] ); } await syncPlaylistUploadsOnChange({ key: 'template:create:' + result.insertId, pool: pool, localUploadDir: deps.uploadDir, nextUploadRefs: collectUploadReferencesFromPayload(payload) }); redirectAfterSave(req, res, '/templates/' + result.insertId + '/edit', { closeUrl: '/templates', newUrl: '/templates/new', message: 'Template created.' }); } catch (error) { next(error); } }); app.get('/templates/:id/edit', requirePermission('templates.update'), async function (req, res, next) { try { const template = await common.fetchTemplateById(pool, Number(req.params.id)); if (!template) { return res.status(404).send('Template not found'); } template.region_usage = await fetchTemplateRegionUsage(template); const sizeData = await common.fetchCanvasSizesData(pool); res.send(pages.renderTemplateFormPage(template, 'edit', req.query.message ? String(req.query.message) : '', sizeData.canvasSizes, req.currentUser)); } catch (error) { next(error); } }); app.post('/templates/:id', requirePermission('templates.update'), upload.any(), async function (req, res, next) { try { const template = await common.fetchTemplateById(pool, Number(req.params.id)); if (!template) { return res.status(404).send('Template not found'); } template.region_usage = await fetchTemplateRegionUsage(template); const existingUploadRefs = collectUploadReferencesFromTemplate(template); const payload = await common.buildTemplatePayload(pool, req, template); if (await common.fetchDuplicateName(pool, 'slide_templates', payload.name, template.id)) { return res.redirect('/templates/' + template.id + '/edit?message=' + encodeURIComponent('A template with that name already exists.')); } const regionDeleteBlockMessage = await getTemplateRegionDeleteBlockMessage(template, payload.regions); if (regionDeleteBlockMessage) { return res.redirect('/templates/' + template.id + '/edit?message=' + encodeURIComponent(regionDeleteBlockMessage)); } const nextUploadRefs = collectUploadReferencesFromPayload(payload); const affectedScreens = await fetchScreensByTemplateId(pool, template.id); const actorId = getAuditUserId(req); await pool.query( 'UPDATE slide_templates SET name = ?, canvas_size_id = ?, background_image_path = ?, background_color = ?, modified_by = ? WHERE id = ?', [payload.name, payload.canvasSizeId, payload.backgroundImagePath, payload.backgroundColor, actorId, template.id] ); await pool.query('DELETE FROM slide_template_regions WHERE template_id = ?', [template.id]); for (let i = 0; i < payload.regions.length; i += 1) { const region = payload.regions[i]; await pool.query( 'INSERT INTO slide_template_regions (template_id, region_key, region_type, label, font_family, lock_ratio, x, y, width, height, z_index, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [template.id, region.region_key, region.region_type, region.label, region.font_family, region.lock_ratio, region.x, region.y, region.width, region.height, region.z_index, actorId, actorId] ); } await syncPlaylistUploadsOnChange({ key: 'template:update:' + template.id, pool: pool, localUploadDir: deps.uploadDir, previousUploadRefs: existingUploadRefs, nextUploadRefs: nextUploadRefs }); await notifyPlayerScreens(affectedScreens, 'refresh'); redirectAfterSave(req, res, '/templates/' + template.id + '/edit', { closeUrl: '/templates', newUrl: '/templates/new', message: 'Template updated.' }); } catch (error) { next(error); } }); app.post('/templates/:id/delete', requirePermission('templates.delete'), async function (req, res, next) { try { const template = await common.fetchTemplateById(pool, Number(req.params.id)); if (!template) { return res.status(404).send('Template not found'); } const blockMessage = await getTemplateDeleteBlockMessage(pool, template); if (blockMessage) { return res.redirect('/templates?message=' + encodeURIComponent(blockMessage)); } const uploadRefs = collectUploadReferencesFromTemplate(template); const affectedScreens = await fetchScreensByTemplateId(pool, template.id); await pool.query('UPDATE slides SET template_id = NULL, modified_by = ? WHERE template_id = ?', [getAuditUserId(req), template.id]); await pool.query('DELETE FROM slide_template_regions WHERE template_id = ?', [template.id]); await pool.query('DELETE FROM slide_templates WHERE id = ?', [template.id]); await syncPlaylistUploadsOnChange({ key: 'template:delete:' + template.id, pool: pool, localUploadDir: deps.uploadDir, previousUploadRefs: uploadRefs }); await notifyPlayerScreens(affectedScreens, 'refresh'); res.redirect('/templates?message=' + encodeURIComponent('Template deleted.')); } catch (error) { next(error); } }); app.get('/canvas-sizes', requirePermission('canvas-sizes.read'), async function (req, res, next) { try { const page = Math.max(1, Math.floor(Number(req.query.page) || 1)); const search = common.getSearchQuery(req); const sort = common.getSortQuery(req); const direction = common.getSortDirectionQuery(req); const data = await common.fetchCanvasSizesPage(pool, page, LIST_PAGE_SIZE, search, sort, direction); res.send(pages.renderCanvasSizesPage({ canvasSizes: data.canvasSizes || [], pagination: buildPagination(data.totalItems, data.currentPage, 'page', { search: search, sort: sort, direction: direction }, LIST_PAGE_SIZE, 'canvas sizes', 'Canvas size pages') }, req.query.message ? String(req.query.message) : '', req.currentUser)); } catch (error) { next(error); } }); app.get('/canvas-sizes/new', requirePermission('canvas-sizes.create'), function (req, res) { res.send(pages.renderCanvasSizeFormPage(null, 'create', req.query.message ? String(req.query.message) : '', req.currentUser)); }); app.post('/canvas-sizes', requirePermission('canvas-sizes.create'), async function (req, res, next) { try { const payload = common.buildCanvasSizePayload(req, null); if (await common.fetchDuplicateName(pool, 'canvas_sizes', payload.name)) { return res.redirect('/canvas-sizes/new?message=' + encodeURIComponent('A canvas size with that name already exists.')); } if (await canvasSizeExists(payload.width, payload.height)) { return res.redirect('/canvas-sizes/new?message=' + encodeURIComponent('That canvas size already exists.')); } const actorId = getAuditUserId(req); const [result] = await pool.query('INSERT INTO canvas_sizes (name, width, height, created_by, modified_by) VALUES (?, ?, ?, ?, ?)', [payload.name, payload.width, payload.height, actorId, actorId]); redirectAfterSave(req, res, '/canvas-sizes/' + result.insertId + '/edit', { closeUrl: '/canvas-sizes', newUrl: '/canvas-sizes/new', message: 'Canvas size created.' }); } catch (error) { next(error); } }); app.get('/canvas-sizes/:id/edit', requirePermission('canvas-sizes.update'), async function (req, res, next) { try { const canvasSize = await common.fetchCanvasSizeById(pool, Number(req.params.id)); if (!canvasSize) { return res.status(404).send('Canvas size not found'); } res.send(pages.renderCanvasSizeEditPage(canvasSize, null, req.query.message ? String(req.query.message) : '', req.currentUser)); } catch (error) { next(error); } }); app.post('/canvas-sizes/:id', requirePermission('canvas-sizes.update'), async function (req, res, next) { try { const canvasSize = await common.fetchCanvasSizeById(pool, Number(req.params.id)); if (!canvasSize) { return res.status(404).send('Canvas size not found'); } const payload = common.buildCanvasSizePayload(req, canvasSize); if (await common.fetchDuplicateName(pool, 'canvas_sizes', payload.name, canvasSize.id)) { return res.redirect('/canvas-sizes/' + canvasSize.id + '/edit?message=' + encodeURIComponent('A canvas size with that name already exists.')); } if (await canvasSizeExists(payload.width, payload.height, canvasSize.id)) { return res.redirect('/canvas-sizes/' + canvasSize.id + '/edit?message=' + encodeURIComponent('That canvas size already exists.')); } await pool.query('UPDATE canvas_sizes SET name = ?, width = ?, height = ?, modified_by = ? WHERE id = ?', [payload.name, payload.width, payload.height, getAuditUserId(req), canvasSize.id]); redirectAfterSave(req, res, '/canvas-sizes', { closeUrl: '/canvas-sizes', newUrl: '/canvas-sizes/new', message: 'Canvas size updated.' }); } catch (error) { next(error); } }); app.post('/canvas-sizes/:id/delete', requirePermission('canvas-sizes.delete'), async function (req, res, next) { try { const canvasSize = await common.fetchCanvasSizeById(pool, Number(req.params.id)); if (!canvasSize) { return res.status(404).send('Canvas size not found'); } const blockMessage = await getCanvasSizeDeleteBlockMessage(pool, canvasSize); if (blockMessage) { return res.redirect('/canvas-sizes?message=' + encodeURIComponent(blockMessage)); } await pool.query('UPDATE slide_templates SET canvas_size_id = NULL, modified_by = ? WHERE canvas_size_id = ?', [getAuditUserId(req), canvasSize.id]); await pool.query('DELETE FROM canvas_sizes WHERE id = ?', [canvasSize.id]); res.redirect('/canvas-sizes?message=' + encodeURIComponent('Canvas size deleted.')); } catch (error) { next(error); } }); };