1d3818e725
- dms-lxc.sh: Proxmox-Host-Installer (unprivilegierter LXC, Debian 13, Docker), curl-Self-Download, Multi-Domain-DKIM, SnappyMail-Provisionierung, PVE-Firewall - Stack: docker-mailserver, Node-Admin-API (Supabase-Auth), React-Admin-UI (OPENBUREAU-Look), SnappyMail (Shibui-Theme), Rspamd-Web-UI, docker-socket-proxy - Admin: Postfächer/Aliase/Catch-all/Quota, editierbare Domains+Settings, Server (Quota/Queue über abgesicherte Bridge), Status & DNS - Hardening: no-new-privileges, Whitelisted exec-Bridge, Rspamd-Passwort, .env chmod 600, PVE-CT-Firewall, generisch/teilbar (keine festen Domains) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
78 lines
2.8 KiB
JavaScript
78 lines
2.8 KiB
JavaScript
// ---------------------------------------------------------------------------
|
|
// settings.js — editierbare Admin-Einstellungen (Domains, Webmail-Domain, Brand)
|
|
//
|
|
// Liegt als JSON in der DMS-Config (persistent). Beim ersten Start aus den
|
|
// ENV-Variablen (Deploy-Dialog) geseedet, danach in der Admin-UI editierbar.
|
|
// ---------------------------------------------------------------------------
|
|
import { promises as fs } from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const CONFIG_DIR = process.env.CONFIG_DIR || '/config';
|
|
const FILE = path.join(CONFIG_DIR, 'admin-settings.json');
|
|
|
|
const isDomain = (s) => /^[^@\s]+\.[^@\s]+$/.test(s);
|
|
const envDomains = () =>
|
|
(process.env.MAIL_DOMAINS || process.env.MAIL_DOMAIN || '').split(/[\s,]+/).filter(Boolean);
|
|
|
|
function seed() {
|
|
const domains = envDomains();
|
|
const primary = process.env.MAIL_DOMAIN || domains[0] || 'example.com';
|
|
return {
|
|
brand: process.env.BRAND || primary,
|
|
fqdn: process.env.MAIL_FQDN || `mail.${primary}`,
|
|
primaryDomain: primary,
|
|
domains: domains.length ? domains : [primary],
|
|
webmailFqdn: process.env.WEBMAIL_FQDN || `mail.${primary}`,
|
|
adminFqdn: process.env.ADMIN_FQDN || `admin.${primary}`,
|
|
};
|
|
}
|
|
|
|
let chain = Promise.resolve();
|
|
const withLock = (fn) => { const r = chain.then(fn, fn); chain = r.catch(() => {}); return r; };
|
|
const save = (s) => fs.writeFile(FILE, JSON.stringify(s, null, 2) + '\n', 'utf8');
|
|
|
|
export async function readSettings() {
|
|
try {
|
|
return { id: 'settings', ...JSON.parse(await fs.readFile(FILE, 'utf8')) };
|
|
} catch (e) {
|
|
if (e.code === 'ENOENT') { const s = seed(); await save(s); return { id: 'settings', ...s }; }
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
export function writeSettings(patch) {
|
|
return withLock(async () => {
|
|
const { id, ...cur } = await readSettings();
|
|
const next = { ...cur };
|
|
// nur erlaubte Felder
|
|
for (const k of ['brand', 'fqdn', 'primaryDomain', 'webmailFqdn', 'adminFqdn']) {
|
|
if (typeof patch[k] === 'string' && patch[k].trim()) next[k] = patch[k].trim();
|
|
}
|
|
if (Array.isArray(patch.domains)) {
|
|
next.domains = [...new Set(patch.domains.map((d) => String(d).trim()).filter(isDomain))];
|
|
}
|
|
await save(next);
|
|
return { id: 'settings', ...next };
|
|
});
|
|
}
|
|
|
|
export function addDomain(domain) {
|
|
return withLock(async () => {
|
|
domain = String(domain || '').trim().toLowerCase();
|
|
if (!isDomain(domain)) { const e = new Error('Ungültige Domain.'); e.status = 400; throw e; }
|
|
const { id, ...cur } = await readSettings();
|
|
if (!cur.domains.includes(domain)) cur.domains.push(domain);
|
|
await save(cur);
|
|
return { id: 'settings', ...cur };
|
|
});
|
|
}
|
|
|
|
export function removeDomain(domain) {
|
|
return withLock(async () => {
|
|
const { id, ...cur } = await readSettings();
|
|
cur.domains = cur.domains.filter((d) => d !== domain);
|
|
await save(cur);
|
|
return { id: 'settings', ...cur };
|
|
});
|
|
}
|