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

main
Last change on this file since 72ec6a4 was 72ec6a4, checked in by Robin <roboburr@…>, 6 weeks ago

Hub-modus en guardian-lite eruit

Robins besluit (31-7): een instance is een eigenaar. Twee dingen weg.

HUB-MODUS was al dood: getTenancy() gaf sinds 24-6 hardcoded 'solo'
terug, dus elke tenancy === 'hub'-tak was onbereikbaar. Nu ook echt
verwijderd: getTenancy/setTenancy zelf, de /user/:slug-routing in
resolveSite, de hub-takken in admin, zoeken, audio, posts (neighbours
en related over alle sites), download, en de push-prefix. In de views
verdwijnen de hub-tagline, de hub-navigatie, de sites- en
users-tabellen (die kwamen alleen in hub-modus gevuld en verwezen nu
naar locals die niemand meer meegeeft), de eigenaar-toewijzing bij een
site, de /user/-slugprefix, het hub-brandblok en de hub-thuisknop.

GUARDIAN-LITE was de laatste multi-user-rest: /guardian/invite gaf een
link waarmee iemand via /guardian/join een echte user plus een site met
guardian_only=1 aanmaakte. Dat zette andermans wachtwoordhash, sessie
en PRIVATE actor-sleutel in jouw database, waardoor een verhuizing of
export nooit netjes kon (shaer-qw6q). Routes, formulier, kolom en
uitnodigingstabel zijn weg. Het guardian-DASHBOARD blijft: dat is
FEP-633c en werkt voor guardians met een eigen Klonkt. Bestaande
installaties houden kolom en tabel ongebruikt; nieuwe krijgen ze niet.

Changed files:
src/services/SettingsService.js

  • getTenancy/setTenancy verwijderd; kop herschreven

src/middleware/site.js, src/middleware/render.js

  • /user/:slug-routing weg; tenancy en hubTitle uit de locals

src/routes/admin.js, admin-settings.js, audio.js, download.js,
src/routes/posts.js, search.js

  • hub-takken en hub-queries weg; postNeighbors zonder isHub

src/services/ActivityPubService.js

  • pushPrefix is nu gewoon ; getTenancy-import weg

src/routes/guardian.js

  • /invite en /join verwijderd (dashboard blijft), imports opgeschoond

src/config/database.js

  • guardian_only-kolom en ap_guardian_invites-tabel niet meer aangemaakt

src/views/pages/admin.ejs, admin-users.ejs, admin-site-edit.ejs,
src/views/pages/guardian.ejs, partials/topnav.ejs, chrome.ejs, bottom-tab.ejs

  • alle hub-takken en de uitnodigingsknop weg

remarks: 320 regels weg, 63 erbij. Suite 372 groen; alle 85 templates
compileren; en met een wegwerp-kopie van de database daadwerkelijk
gedraaid en ingelogd: /, /admin, /admin/users, /admin/sites,
/admin/settings, /admin/sites/demo/edit, /admin/media en /guardian
geven alle 200 zonder fouten in het log, en /guardian/invite en
/guardian/join geven nu 404.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

  • Property mode set to 100644
File size: 8.1 KB
Line 
1/**
2 * Admin: global settings.
3 * - hub branding (name/tagline/intro/hero of the generic hub home page)
4 *
5 * GET /admin/settings -> show current settings
6 * POST /admin/settings -> save (god-only). Also accepts an uploaded
7 * hero image (multipart); an upload wins over the
8 * URL text field. Without an upload the URL field is leading.
9 *
10 * The hub page is generic (belonging to no user); this branding lives in
11 * global settings, not in a site.
12 */
13
14import express from 'express';
15import path from 'path';
16import fs from 'fs';
17import multer from 'multer';
18import { v4 as uuid } from 'uuid';
19import { renderPage } from '../middleware/render.js';
20import { requireGod } from '../middleware/auth.js';
21import { getSetting, setSetting } from '../services/SettingsService.js';
22import { SUPPORTED } from '../services/i18n.js';
23import { mailerStatus, sendMail } from '../config/mailer.js';
24import { entitlementStatus, premiumUnlocked } from '../services/PatreonService.js';
25import { toWebp } from '../services/ImageWebpService.js';
26import { mediaDir } from '../config/paths.js';
27
28const router = express.Router();
29
30// Hero dark overlay: percentage 0-100 (0 = no overlay, 100 = fully black).
31// Default 45 = the old hard-coded value, so existing hubs don't change appearance.
32function clampOverlay(raw) {
33 const v = parseInt(raw, 10);
34 return Number.isFinite(v) ? Math.max(0, Math.min(100, v)) : 45;
35}
36
37// Hero uploads land in storage/media/hero → accessible as /media/hero/<file>
38// (the /media static handler serves storage/media). Same model as avatars.
39const HERO_DIR = mediaDir('HERO_PATH', 'hero');
40fs.mkdirSync(HERO_DIR, { recursive: true });
41
42// Only raster formats for upload. SVG is intentionally NOT allowed via upload
43// (raw SVG can contain scripts → stored-XSS when opened directly); an SVG hero
44// can still be set via the URL field (like the bundled 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
66router.get('/', requireGod, (req, res) => {
67 renderPage(req, res, 'pages/admin-settings', {
68 pageTitleKey: 'admin.t_settings',
69 bodyClass: 'on-admin',
70 hubTagline: getSetting('hub_tagline') || '',
71 hubIntro: getSetting('hub_intro') || '',
72 hubHeroImage: getSetting('hub_hero_image') || '',
73 hubHeroOverlay: clampOverlay(getSetting('hub_hero_overlay')),
74 defaultLang: getSetting('default_lang') || '',
75 premium: entitlementStatus(),
76 smtp: mailerStatus(),
77 footerNewsletter: getSetting('footer_newsletter') === '1',
78 apEnabledSetting: getSetting('ap_enabled', '1') !== '0',
79 success: req.query.success || null,
80 error: req.query.error || null,
81 });
82});
83
84router.post('/', requireGod, (req, res) => {
85 // multer.single processes multipart (hub branding form). For a plain
86 // urlencoded POST: multer does nothing and req.body stays intact.
87 heroUpload.single('hub_hero_file')(req, res, (err) => {
88 if (err) {
89 return res.redirect('/admin/settings?error=' + encodeURIComponent(err.message));
90 }
91
92 if (typeof req.body.default_lang !== 'undefined') {
93 // Default language for visitors (empty = follow env/browser). Validated against NL/EN/DE.
94 const dl = (req.body.default_lang || '').toString().toLowerCase();
95 setSetting('default_lang', SUPPORTED.includes(dl) ? dl : '');
96 }
97 if (typeof req.body.timezone !== 'undefined') {
98 // Site timezone (IANA, e.g. Europe/Amsterdam). Empty = server default (UTC).
99 // Validate with Intl so a nonsense value never breaks date rendering.
100 const tz = (req.body.timezone || '').toString().trim();
101 let valid = '';
102 if (tz) { try { Intl.DateTimeFormat('en-US', { timeZone: tz }); valid = tz; } catch { valid = ''; } }
103 setSetting('timezone', valid);
104 }
105 if (typeof req.body.hub_tagline !== 'undefined') {
106 setSetting('hub_tagline', (req.body.hub_tagline || '').toString().slice(0, 120).trim());
107 }
108 if (typeof req.body.hub_intro !== 'undefined') {
109 setSetting('hub_intro', (req.body.hub_intro || '').toString().slice(0, 400).trim());
110 }
111
112 // Hero: an uploaded image wins; otherwise the URL text field.
113 if (req.file) {
114 const newUrl = `/media/hero/${toWebp(req.file)}`;
115 // Clean up a previously uploaded hero (only if it came from our hero dir).
116 const old = getSetting('hub_hero_image') || '';
117 if (old.startsWith('/media/hero/')) {
118 try { fs.unlinkSync(path.join(HERO_DIR, path.basename(old))); } catch {}
119 }
120 setSetting('hub_hero_image', newUrl);
121 } else if (typeof req.body.hub_hero_image !== 'undefined') {
122 setSetting('hub_hero_image', (req.body.hub_hero_image || '').toString().slice(0, 300).trim());
123 }
124
125 if (typeof req.body.hub_hero_overlay !== 'undefined') {
126 setSetting('hub_hero_overlay', String(clampOverlay(req.body.hub_hero_overlay)));
127 }
128
129 res.redirect('/admin/settings?success=' + encodeURIComponent('Opgeslagen'));
130 });
131});
132
133// ── SMTP / e-mail-instellingen ────────────────────────────────────
134router.post('/smtp', requireGod, (req, res) => {
135 const b = req.body || {};
136 if (b.clear === '1') {
137 ['smtp_host', 'smtp_port', 'smtp_user', 'smtp_pass', 'smtp_from'].forEach((k) => setSetting(k, ''));
138 return res.redirect('/admin/settings?success=' + encodeURIComponent('SMTP-instellingen gewist'));
139 }
140 setSetting('smtp_host', (b.smtp_host || '').toString().trim());
141 setSetting('smtp_port', (b.smtp_port || '').toString().trim());
142 setSetting('smtp_user', (b.smtp_user || '').toString().trim());
143 setSetting('smtp_from', (b.smtp_from || '').toString().trim());
144 // Only overwrite the password if a new value was entered.
145 const pass = (b.smtp_pass || '').toString();
146 if (pass) setSetting('smtp_pass', pass);
147 res.redirect('/admin/settings?success=' + encodeURIComponent('SMTP-instellingen opgeslagen'));
148});
149
150// Newsletter sign-up in the footer on/off.
151router.post('/footer', requireGod, (req, res) => {
152 setSetting('footer_newsletter', req.body.footer_newsletter ? '1' : '0');
153 res.redirect('/admin/settings?success=' + encodeURIComponent('Footer-instelling opgeslagen'));
154});
155
156// Site mode: Solo (ap off → no federation, no comments) or Circles (ap on).
157// Driven by a radio (mode=solo|cirkels); legacy ap_enabled checkbox still accepted.
158router.post('/ap', requireGod, (req, res) => {
159 let enabled;
160 if (typeof req.body.mode !== 'undefined') enabled = req.body.mode === 'solo' ? '0' : '1';
161 else enabled = req.body.ap_enabled ? '1' : '0';
162 setSetting('ap_enabled', enabled);
163 res.redirect('/admin/settings?success=' + encodeURIComponent('Modus opgeslagen'));
164});
165
166// Send a test email to a specified address (or the logged-in user).
167router.post('/smtp/test', requireGod, async (req, res) => {
168 const to = ((req.body && req.body.to) || (req.session.user && req.session.user.email) || '').toString().trim();
169 if (!to || to.indexOf('@') === -1) {
170 return res.redirect('/admin/settings?error=' + encodeURIComponent('Geef een geldig test-e-mailadres op.'));
171 }
172 try {
173 await sendMail({
174 to,
175 subject: 'Klonkt — SMTP-test',
176 text: 'Gelukt! Je SMTP-instellingen werken. Dit is een testbericht van je Klonkt-site.',
177 html: '<p>Gelukt! Je <strong>SMTP-instellingen werken</strong>. Dit is een testbericht van je Klonkt-site.</p>',
178 });
179 res.redirect('/admin/settings?success=' + encodeURIComponent('Testmail verstuurd naar ' + to));
180 } catch (e) {
181 res.redirect('/admin/settings?error=' + encodeURIComponent('Testmail mislukt: ' + (e.message || e)));
182 }
183});
184
185export default router;
Note: See TracBrowser for help on using the repository browser.