// --------------------------------------------------------------------------- // server.js — Admin-API für docker-mailserver // // REST-Schnittstelle im Format von react-admin (ra-data-simple-rest): // GET /accounts Liste (mit Content-Range Header) // GET /accounts/:id einzelnes Konto // POST /accounts anlegen // PUT /accounts/:id ändern (Passwort/Quota) // DELETE /accounts/:id löschen // analog /aliases, sowie GET /status (DNS/DKIM-Infos). // --------------------------------------------------------------------------- import express from 'express'; import { requireAdmin } from './lib/auth.js'; import * as store from './lib/store.js'; import * as ms from './lib/mailserver.js'; import * as settings from './lib/settings.js'; const app = express(); app.use(express.json()); // --- ungeschützter Health-Check ------------------------------------------- app.get('/health', (_req, res) => res.json({ ok: true })); // --- ab hier alles geschützt ---------------------------------------------- app.use(requireAdmin); // Hilfsfunktion: Liste sortieren/filtern/paginieren wie ra-data-simple-rest function sendList(res, resource, rows, query) { let data = rows; // filter if (query.filter) { try { const f = JSON.parse(query.filter); // getMany: { id: [...] } if (Array.isArray(f.id)) data = data.filter((r) => f.id.includes(r.id)); // Volltext-Filter "q" if (f.q) { const q = String(f.q).toLowerCase(); data = data.filter((r) => JSON.stringify(r).toLowerCase().includes(q)); } } catch { /* ignore */ } } // sort if (query.sort) { try { const [field, order] = JSON.parse(query.sort); data = [...data].sort((a, b) => String(a[field]).localeCompare(String(b[field]))); if (order === 'DESC') data.reverse(); } catch { /* ignore */ } } const total = data.length; // range let [start, end] = [0, total - 1]; if (query.range) { try { [start, end] = JSON.parse(query.range); } catch { /* ignore */ } } const page = data.slice(start, end + 1); res.set('Content-Range', `${resource} ${start}-${start + page.length - 1}/${total}`); res.set('Access-Control-Expose-Headers', 'Content-Range'); res.json(page); } const id = (req) => decodeURIComponent(req.params.id); // ============================ ACCOUNTS ===================================== app.get('/accounts', async (req, res, next) => { try { sendList(res, 'accounts', await store.listAccounts(), req.query); } catch (e) { next(e); } }); app.get('/accounts/:id', async (req, res, next) => { try { const a = await store.getAccount(id(req)); if (!a) return res.status(404).json({ error: 'Konto nicht gefunden.' }); res.json(a); } catch (e) { next(e); } }); app.post('/accounts', async (req, res, next) => { try { res.status(201).json(await store.createAccount(req.body)); } catch (e) { next(e); } }); app.put('/accounts/:id', async (req, res, next) => { try { res.json(await store.updateAccount(id(req), req.body)); } catch (e) { next(e); } }); app.delete('/accounts/:id', async (req, res, next) => { try { res.json(await store.deleteAccount(id(req))); } catch (e) { next(e); } }); // ============================ ALIASES ====================================== app.get('/aliases', async (req, res, next) => { try { sendList(res, 'aliases', await store.listAliases(), req.query); } catch (e) { next(e); } }); app.get('/aliases/:id', async (req, res, next) => { try { const a = await store.getAlias(id(req)); if (!a) return res.status(404).json({ error: 'Alias nicht gefunden.' }); res.json(a); } catch (e) { next(e); } }); app.post('/aliases', async (req, res, next) => { try { res.status(201).json(await store.createAlias(req.body)); } catch (e) { next(e); } }); app.put('/aliases/:id', async (req, res, next) => { try { res.json(await store.updateAlias(id(req), req.body)); } catch (e) { next(e); } }); app.delete('/aliases/:id', async (req, res, next) => { try { res.json(await store.deleteAlias(id(req))); } catch (e) { next(e); } }); // ============================ STATUS ======================================= app.get('/status', async (_req, res, next) => { try { res.json(await store.status()); } catch (e) { next(e); } }); // react-admin erwartet bei getOne(status) ggf. /status/status app.get('/status/:id', async (_req, res, next) => { try { res.json(await store.status()); } catch (e) { next(e); } }); // ============== MAILSERVER-BRIDGE (über docker-socket-proxy) =============== app.get('/mailserver/overview', async (_req, res, next) => { try { res.json(await ms.overview()); } catch (e) { next(e); } }); app.get('/mailserver/quota', async (_req, res, next) => { try { res.json(await ms.quotaUsage()); } catch (e) { next(e); } }); app.get('/mailserver/queue', async (_req, res, next) => { try { res.json(await ms.queue()); } catch (e) { next(e); } }); app.get('/mailserver/who', async (_req, res, next) => { try { res.json(await ms.who()); } catch (e) { next(e); } }); app.post('/mailserver/dkim', async (req, res, next) => { try { res.json(await ms.generateDkim((req.body || {}).domain)); } catch (e) { next(e); } }); // ============== EINSTELLUNGEN (Domains / Webmail-Domain / Brand) ============ app.post('/settings/domains', async (req, res, next) => { try { const domain = (req.body || {}).domain; const result = await settings.addDomain(domain); ms.generateDkim(domain).catch(() => {}); // DKIM best-effort über die Bridge res.json(result); } catch (e) { next(e); } }); app.delete('/settings/domains/:domain', async (req, res, next) => { try { res.json(await settings.removeDomain(decodeURIComponent(req.params.domain))); } catch (e) { next(e); } }); app.get('/settings', async (_req, res, next) => { try { res.json(await settings.readSettings()); } catch (e) { next(e); } }); app.get('/settings/:id', async (_req, res, next) => { try { res.json(await settings.readSettings()); } catch (e) { next(e); } }); app.put('/settings/:id', async (req, res, next) => { try { res.json(await settings.writeSettings(req.body || {})); } catch (e) { next(e); } }); // --- Fehlerbehandlung ------------------------------------------------------ app.use((err, _req, res, _next) => { const code = err.status || 500; if (code >= 500) console.error(err); res.status(code).json({ error: err.message || 'Serverfehler' }); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => console.log(`[dms-admin-api] läuft auf :${PORT}`));