source: Klonkt/src/routes/admin-settings.js@ a9e23f6

main
Last change on this file since a9e23f6 was 0b7ebbf, checked in by roboburr <roboburr@…>, 3 months ago

Merge branch 'feat/cirkels-v1'

# Conflicts:
# src/routes/admin-settings.js

  • Property mode set to 100644
File size: 4.8 KB
RevLine 
[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
15import express from 'express';
[63edb0d]16import path from 'path';
17import fs from 'fs';
18import { fileURLToPath } from 'url';
19import multer from 'multer';
20import { v4 as uuid } from 'uuid';
[6351545]21import { renderPage } from '../middleware/render.js';
22import { requireGod } from '../middleware/auth.js';
[6e0249f]23import { getTenancy, setTenancy, getSetting, setSetting } from '../services/SettingsService.js';
[6351545]24
25const 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.
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});
65
[6351545]66router.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
81router.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
123export default router;
Note: See TracBrowser for help on using the repository browser.