feat(cms): add per-page external URL override — backend
This commit is contained in:
@@ -464,14 +464,30 @@ async function ensureGlobalCategories() {
|
|||||||
table.text('content_en');
|
table.text('content_en');
|
||||||
table.text('content_de');
|
table.text('content_de');
|
||||||
table.string('logo_url').nullable();
|
table.string('logo_url').nullable();
|
||||||
|
table.boolean('use_external_url').notNullable().defaultTo(false);
|
||||||
|
table.string('external_url').nullable();
|
||||||
table.timestamp('updated_at').defaultTo(db.fn.now());
|
table.timestamp('updated_at').defaultTo(db.fn.now());
|
||||||
});
|
});
|
||||||
} else if (!(await db.schema.hasColumn('cms_pages', 'logo_url'))) {
|
} else {
|
||||||
// Online migration for existing deployments — see issue #324, per-page
|
if (!(await db.schema.hasColumn('cms_pages', 'logo_url'))) {
|
||||||
// logo override for admin-customisable error pages.
|
// Online migration for existing deployments — see issue #324, per-page
|
||||||
await db.schema.alterTable('cms_pages', (table) => {
|
// logo override for admin-customisable error pages.
|
||||||
table.string('logo_url').nullable();
|
await db.schema.alterTable('cms_pages', (table) => {
|
||||||
});
|
table.string('logo_url').nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!(await db.schema.hasColumn('cms_pages', 'use_external_url'))) {
|
||||||
|
// Per-page toggle to redirect visitors to an external imprint /
|
||||||
|
// privacy-policy URL instead of rendering the internal CMS content.
|
||||||
|
await db.schema.alterTable('cms_pages', (table) => {
|
||||||
|
table.boolean('use_external_url').notNullable().defaultTo(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!(await db.schema.hasColumn('cms_pages', 'external_url'))) {
|
||||||
|
await db.schema.alterTable('cms_pages', (table) => {
|
||||||
|
table.string('external_url').nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const categoryCountRow = await db('photo_categories').count({ count: 'id' }).first();
|
const categoryCountRow = await db('photo_categories').count({ count: 'id' }).first();
|
||||||
|
|||||||
@@ -71,7 +71,9 @@ router.put('/pages/:slug', adminAuth, requirePermission('cms.edit'), [
|
|||||||
body('title_de').optional().isString(),
|
body('title_de').optional().isString(),
|
||||||
body('content_en').optional().isString(),
|
body('content_en').optional().isString(),
|
||||||
body('content_de').optional().isString(),
|
body('content_de').optional().isString(),
|
||||||
body('logo_url').optional({ nullable: true }).isString()
|
body('logo_url').optional({ nullable: true }).isString(),
|
||||||
|
body('use_external_url').optional().isBoolean(),
|
||||||
|
body('external_url').optional({ nullable: true }).isString()
|
||||||
], async (req, res) => {
|
], async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
@@ -80,7 +82,26 @@ router.put('/pages/:slug', adminAuth, requirePermission('cms.edit'), [
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { slug } = req.params;
|
const { slug } = req.params;
|
||||||
const { title_en, title_de, content_en, content_de, logo_url } = req.body;
|
const { title_en, title_de, content_en, content_de, logo_url, use_external_url, external_url } = req.body;
|
||||||
|
|
||||||
|
// When the external-URL toggle is on, the URL must parse and use https://.
|
||||||
|
// express-validator's isURL() is too permissive (allows http:, ftp:, etc.) —
|
||||||
|
// an explicit protocol check is the security-relevant gate.
|
||||||
|
if (use_external_url === true) {
|
||||||
|
const candidate = typeof external_url === 'string' ? external_url.trim() : '';
|
||||||
|
if (!candidate) {
|
||||||
|
return res.status(400).json({ error: 'external_url is required when use_external_url is true' });
|
||||||
|
}
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = new URL(candidate);
|
||||||
|
} catch (_err) {
|
||||||
|
return res.status(400).json({ error: 'external_url must be a valid URL' });
|
||||||
|
}
|
||||||
|
if (parsed.protocol !== 'https:') {
|
||||||
|
return res.status(400).json({ error: 'external_url must use https://' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const page = await db('cms_pages').where('slug', slug).first();
|
const page = await db('cms_pages').where('slug', slug).first();
|
||||||
if (!page) {
|
if (!page) {
|
||||||
@@ -99,6 +120,13 @@ router.put('/pages/:slug', adminAuth, requirePermission('cms.edit'), [
|
|||||||
if (Object.prototype.hasOwnProperty.call(req.body, 'logo_url')) {
|
if (Object.prototype.hasOwnProperty.call(req.body, 'logo_url')) {
|
||||||
updateFields.logo_url = logo_url || null;
|
updateFields.logo_url = logo_url || null;
|
||||||
}
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(req.body, 'use_external_url')) {
|
||||||
|
updateFields.use_external_url = !!use_external_url;
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(req.body, 'external_url')) {
|
||||||
|
const trimmed = typeof external_url === 'string' ? external_url.trim() : '';
|
||||||
|
updateFields.external_url = trimmed || null;
|
||||||
|
}
|
||||||
|
|
||||||
await db('cms_pages').where('slug', slug).update(updateFields);
|
await db('cms_pages').where('slug', slug).update(updateFields);
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ router.get('/pages/:slug', async (req, res) => {
|
|||||||
// Per-page logo override (#324). Null means "fall back to global
|
// Per-page logo override (#324). Null means "fall back to global
|
||||||
// branding logo" — the consumer decides.
|
// branding logo" — the consumer decides.
|
||||||
logo_url: page.logo_url || null,
|
logo_url: page.logo_url || null,
|
||||||
|
// Per-page external-URL override. When use_external_url is true and
|
||||||
|
// external_url is set, consumers should redirect / link out instead
|
||||||
|
// of rendering the internal title/content.
|
||||||
|
use_external_url: !!page.use_external_url,
|
||||||
|
external_url: page.external_url || null,
|
||||||
updated_at: page.updated_at
|
updated_at: page.updated_at
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user