| [6351545] | 1 | /**
|
|---|
| [6e0249f] | 2 | * Admin: globale instellingen.
|
|---|
| 3 | * - tenancy-modus (Solo/Hub)
|
|---|
| [63edb0d] | 4 | * - hub-branding (naam/tagline/intro/hero van de generieke hub-hoofdpagina)
|
|---|
| [6351545] | 5 | *
|
|---|
| [6e0249f] | 6 | * GET /admin/settings -> toon huidige instellingen
|
|---|
| [63edb0d] | 7 | * POST /admin/settings -> sla op (god-only). Accepteert nu ook een geuploade
|
|---|
| 8 | * hero-afbeelding (multipart); een upload wint van het
|
|---|
| 9 | * URL-tekstveld. Zonder upload blijft het URL-veld leidend.
|
|---|
| [6351545] | 10 | *
|
|---|
| [6e0249f] | 11 | * De hub-pagina is generiek (van geen enkele user); deze branding leeft in
|
|---|
| 12 | * globale settings, niet in een site.
|
|---|
| [6351545] | 13 | */
|
|---|
| 14 |
|
|---|
| 15 | import express from 'express';
|
|---|
| [63edb0d] | 16 | import path from 'path';
|
|---|
| 17 | import fs from 'fs';
|
|---|
| 18 | import { fileURLToPath } from 'url';
|
|---|
| 19 | import multer from 'multer';
|
|---|
| 20 | import { v4 as uuid } from 'uuid';
|
|---|
| [6351545] | 21 | import { renderPage } from '../middleware/render.js';
|
|---|
| 22 | import { requireGod } from '../middleware/auth.js';
|
|---|
| [6e0249f] | 23 | import { getTenancy, setTenancy, getSetting, setSetting } from '../services/SettingsService.js';
|
|---|
| [6351545] | 24 |
|
|---|
| 25 | const router = express.Router();
|
|---|
| 26 |
|
|---|
| [63edb0d] | 27 | // Hero dark-overlay: percentage 0-100 (0 = geen overlay, 100 = volledig zwart).
|
|---|
| 28 | // Default 45 = de oude hardgecodeerde waarde, zodat bestaande hubs niet wijzigen.
|
|---|
| 29 | function clampOverlay(raw) {
|
|---|
| 30 | const v = parseInt(raw, 10);
|
|---|
| 31 | return Number.isFinite(v) ? Math.max(0, Math.min(100, v)) : 45;
|
|---|
| 32 | }
|
|---|
| 33 |
|
|---|
| 34 | const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|---|
| 35 | // Hero-uploads landen in storage/media/hero → bereikbaar als /media/hero/<file>
|
|---|
| 36 | // (de /media static handler serveert storage/media). Zelfde model als avatars.
|
|---|
| 37 | const HERO_DIR = path.resolve(
|
|---|
| 38 | process.env.HERO_PATH || path.join(__dirname, '..', '..', 'storage', 'media', 'hero')
|
|---|
| 39 | );
|
|---|
| 40 | fs.mkdirSync(HERO_DIR, { recursive: true });
|
|---|
| 41 |
|
|---|
| 42 | // Alleen raster-formaten voor de upload. SVG mag bewust NIET via upload (raw
|
|---|
| 43 | // SVG kan script bevatten → opgeslagen-XSS bij direct openen); een SVG-hero kan
|
|---|
| 44 | // nog steeds via het URL-veld (zoals de meegeleverde demo-placeholder).
|
|---|
| 45 | const ALLOWED_HERO_EXT = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif']);
|
|---|
| 46 | const MAX_HERO_BYTES = 5 * 1024 * 1024;
|
|---|
| 47 |
|
|---|
| 48 | const heroUpload = multer({
|
|---|
| 49 | storage: multer.diskStorage({
|
|---|
| 50 | destination: (req, file, cb) => cb(null, HERO_DIR),
|
|---|
| 51 | filename: (req, file, cb) => {
|
|---|
| 52 | const ext = path.extname(file.originalname).toLowerCase();
|
|---|
| 53 | cb(null, `${uuid()}${ext}`);
|
|---|
| 54 | },
|
|---|
| 55 | }),
|
|---|
| 56 | limits: { fileSize: MAX_HERO_BYTES },
|
|---|
| 57 | fileFilter: (req, file, cb) => {
|
|---|
| 58 | const ext = path.extname(file.originalname).toLowerCase();
|
|---|
| 59 | if (!ALLOWED_HERO_EXT.has(ext)) {
|
|---|
| 60 | return cb(new Error('Hero-afbeelding moet jpg/png/webp/gif zijn'));
|
|---|
| 61 | }
|
|---|
| 62 | cb(null, true);
|
|---|
| 63 | },
|
|---|
| 64 | });
|
|---|
| 65 |
|
|---|
| [6351545] | 66 | router.get('/', requireGod, (req, res) => {
|
|---|
| 67 | renderPage(req, res, 'pages/admin-settings', {
|
|---|
| 68 | pageTitle: 'Instellingen',
|
|---|
| 69 | bodyClass: 'on-admin',
|
|---|
| 70 | tenancy: getTenancy(),
|
|---|
| [6e0249f] | 71 | hubTitle: getSetting('hub_title') || '',
|
|---|
| 72 | hubTagline: getSetting('hub_tagline') || '',
|
|---|
| 73 | hubIntro: getSetting('hub_intro') || '',
|
|---|
| [4c5169f] | 74 | hubHeroImage: getSetting('hub_hero_image') || '',
|
|---|
| [63edb0d] | 75 | hubHeroOverlay: clampOverlay(getSetting('hub_hero_overlay')),
|
|---|
| [6351545] | 76 | success: req.query.success || null,
|
|---|
| [63edb0d] | 77 | error: req.query.error || null,
|
|---|
| [6351545] | 78 | });
|
|---|
| 79 | });
|
|---|
| 80 |
|
|---|
| 81 | router.post('/', requireGod, (req, res) => {
|
|---|
| [63edb0d] | 82 | // multer.single verwerkt multipart (hub-branding form). Bij een gewone
|
|---|
| 83 | // urlencoded POST (tenancy-form) doet multer niets en blijft req.body intact.
|
|---|
| 84 | heroUpload.single('hub_hero_file')(req, res, (err) => {
|
|---|
| 85 | if (err) {
|
|---|
| 86 | return res.redirect('/admin/settings?error=' + encodeURIComponent(err.message));
|
|---|
| 87 | }
|
|---|
| 88 |
|
|---|
| 89 | if (typeof req.body.tenancy !== 'undefined') {
|
|---|
| [0b7ebbf] | 90 | setTenancy(req.body.tenancy); // valideert naar solo | hub | circle
|
|---|
| [63edb0d] | 91 | }
|
|---|
| 92 | if (typeof req.body.hub_title !== 'undefined') {
|
|---|
| 93 | setSetting('hub_title', (req.body.hub_title || '').toString().slice(0, 80).trim());
|
|---|
| 94 | }
|
|---|
| 95 | if (typeof req.body.hub_tagline !== 'undefined') {
|
|---|
| 96 | setSetting('hub_tagline', (req.body.hub_tagline || '').toString().slice(0, 120).trim());
|
|---|
| 97 | }
|
|---|
| 98 | if (typeof req.body.hub_intro !== 'undefined') {
|
|---|
| 99 | setSetting('hub_intro', (req.body.hub_intro || '').toString().slice(0, 400).trim());
|
|---|
| 100 | }
|
|---|
| 101 |
|
|---|
| 102 | // Hero: een geüploade afbeelding wint; anders het URL-tekstveld.
|
|---|
| 103 | if (req.file) {
|
|---|
| 104 | const newUrl = `/media/hero/${req.file.filename}`;
|
|---|
| 105 | // Ruim een vorige geüploade hero op (alleen als die uit onze hero-map kwam).
|
|---|
| 106 | const old = getSetting('hub_hero_image') || '';
|
|---|
| 107 | if (old.startsWith('/media/hero/')) {
|
|---|
| 108 | try { fs.unlinkSync(path.join(HERO_DIR, path.basename(old))); } catch {}
|
|---|
| 109 | }
|
|---|
| 110 | setSetting('hub_hero_image', newUrl);
|
|---|
| 111 | } else if (typeof req.body.hub_hero_image !== 'undefined') {
|
|---|
| 112 | setSetting('hub_hero_image', (req.body.hub_hero_image || '').toString().slice(0, 300).trim());
|
|---|
| 113 | }
|
|---|
| 114 |
|
|---|
| 115 | if (typeof req.body.hub_hero_overlay !== 'undefined') {
|
|---|
| 116 | setSetting('hub_hero_overlay', String(clampOverlay(req.body.hub_hero_overlay)));
|
|---|
| 117 | }
|
|---|
| 118 |
|
|---|
| 119 | res.redirect('/admin/settings?success=' + encodeURIComponent('Opgeslagen'));
|
|---|
| 120 | });
|
|---|
| [6351545] | 121 | });
|
|---|
| 122 |
|
|---|
| 123 | export default router;
|
|---|