Merge pull request #372 from Luca-Timo/beta

feat(cms): add external URL toggle for imprint and privacy pages
This commit is contained in:
Paul Nothaft
2026-05-04 00:10:44 +02:00
committed by GitHub
9 changed files with 198 additions and 23 deletions
+22 -6
View File
@@ -464,14 +464,30 @@ async function ensureGlobalCategories() {
table.text('content_en');
table.text('content_de');
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());
});
} else if (!(await db.schema.hasColumn('cms_pages', 'logo_url'))) {
// Online migration for existing deployments — see issue #324, per-page
// logo override for admin-customisable error pages.
await db.schema.alterTable('cms_pages', (table) => {
table.string('logo_url').nullable();
});
} else {
if (!(await db.schema.hasColumn('cms_pages', 'logo_url'))) {
// Online migration for existing deployments — see issue #324, per-page
// logo override for admin-customisable error pages.
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();
+30 -2
View File
@@ -71,7 +71,9 @@ router.put('/pages/:slug', adminAuth, requirePermission('cms.edit'), [
body('title_de').optional().isString(),
body('content_en').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) => {
try {
const errors = validationResult(req);
@@ -80,7 +82,26 @@ router.put('/pages/:slug', adminAuth, requirePermission('cms.edit'), [
}
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();
if (!page) {
@@ -99,6 +120,13 @@ router.put('/pages/:slug', adminAuth, requirePermission('cms.edit'), [
if (Object.prototype.hasOwnProperty.call(req.body, 'logo_url')) {
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);
+5
View File
@@ -25,6 +25,11 @@ router.get('/pages/:slug', async (req, res) => {
// Per-page logo override (#324). Null means "fall back to global
// branding logo" — the consumer decides.
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
});
} catch (error) {