Ignore:
File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/admin-settings.js

    r0091cb7 r63edb0d  
    22 * Admin: globale instellingen.
    33 *  - tenancy-modus (Solo/Hub)
    4  *  - hub-branding (naam/tagline/intro van de generieke hub-hoofdpagina)
     4 *  - hub-branding (naam/tagline/intro/hero van de generieke hub-hoofdpagina)
    55 *
    66 * GET  /admin/settings   -> toon huidige instellingen
    7  * POST /admin/settings   -> sla op (god-only)
     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.
    810 *
    911 * De hub-pagina is generiek (van geen enkele user); deze branding leeft in
     
    1214
    1315import express from 'express';
     16import path from 'path';
     17import fs from 'fs';
     18import { fileURLToPath } from 'url';
     19import multer from 'multer';
     20import { v4 as uuid } from 'uuid';
    1421import { renderPage } from '../middleware/render.js';
    1522import { requireGod } from '../middleware/auth.js';
     
    1724
    1825const router = express.Router();
     26
     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.
     29function clampOverlay(raw) {
     30  const v = parseInt(raw, 10);
     31  return Number.isFinite(v) ? Math.max(0, Math.min(100, v)) : 45;
     32}
     33
     34const __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.
     37const HERO_DIR = path.resolve(
     38  process.env.HERO_PATH || path.join(__dirname, '..', '..', 'storage', 'media', 'hero')
     39);
     40fs.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).
     45const ALLOWED_HERO_EXT = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif']);
     46const MAX_HERO_BYTES = 5 * 1024 * 1024;
     47
     48const 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});
    1965
    2066router.get('/', requireGod, (req, res) => {
     
    2773    hubIntro: getSetting('hub_intro') || '',
    2874    hubHeroImage: getSetting('hub_hero_image') || '',
     75    hubHeroOverlay: clampOverlay(getSetting('hub_hero_overlay')),
    2976    success: req.query.success || null,
     77    error: req.query.error || null,
    3078  });
    3179});
    3280
    3381router.post('/', requireGod, (req, res) => {
    34   if (typeof req.body.tenancy !== 'undefined') {
    35     setTenancy(req.body.tenancy); // valideert naar solo | hub | circle
    36   }
    37   if (typeof req.body.hub_title !== 'undefined') {
    38     setSetting('hub_title', (req.body.hub_title || '').toString().slice(0, 80).trim());
    39   }
    40   if (typeof req.body.hub_tagline !== 'undefined') {
    41     setSetting('hub_tagline', (req.body.hub_tagline || '').toString().slice(0, 120).trim());
    42   }
    43   if (typeof req.body.hub_intro !== 'undefined') {
    44     setSetting('hub_intro', (req.body.hub_intro || '').toString().slice(0, 400).trim());
    45   }
    46   if (typeof req.body.hub_hero_image !== 'undefined') {
    47     setSetting('hub_hero_image', (req.body.hub_hero_image || '').toString().slice(0, 300).trim());
    48   }
    49   res.redirect('/admin/settings?success=' + encodeURIComponent('Opgeslagen'));
     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') {
     90      setTenancy(req.body.tenancy === 'hub' ? 'hub' : 'solo');
     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  });
    50121});
    51122
Note: See TracChangeset for help on using the changeset viewer.