Einrichtung: Wizard um E-Mail, Kalender und REST-API erweitert

Der Assistent deckt nun alle Bereiche aus /einstellungen ab. Neue Schritte
fuer E-Mail (SMTP/IMAP) & CalDAV sowie REST-API; Indikator auf 7 Schritte.
Secrets (Passwort, Token) werden nie vorausgefuellt und beim Speeren leerer
Felder nicht ueberschrieben. Neuer Endpunkt /api/einrichtung/config mit
SSRF-Guard (Mail-Host, CalDAV-URL, Ollama-Host) und API-Token-Kollisionspruefung.
Abschluss-Checkliste trennt Pflicht- von optionalen Verbindungen.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-14 12:55:20 +02:00
co-authored by Claude
parent 1bc0188fcc
commit 2566dcc98d
2 changed files with 355 additions and 171 deletions
+58 -2
View File
@@ -1709,8 +1709,9 @@ initializeDatabase().then(async () => {
// Einrichtungsassistent: eingeblendet, wenn der Nutzer noch keine
// Bewerbung angelegt hat und den Assistenten nicht dauerhaft ausgeblendet
// hat. Der Wizard selbst führt dann durch die fehlenden Einstellungen
// (Persönliche Angaben, Basis-Unterlagen, Ollama-KI).
// hat. Der Wizard führt durch alle Bereiche, die sonst unter /einstellungen
// liegen: Persönliche Angaben, Basis-Unterlagen, Ollama-KI, E-Mail,
// Bewerbungskalender (CalDAV) und REST-API.
const keinDashboard = !totalCount || totalCount.count === 0;
const verstecktRow = keinDashboard
? await dbGet("SELECT value FROM app_state WHERE user_id = ? AND key = 'state:einrichtung_versteckt'", [U])
@@ -1720,6 +1721,10 @@ initializeDatabase().then(async () => {
const basisRows = await dbAll('SELECT id, typ, inhalt FROM basis_dokumente WHERE user_id = ? ORDER BY id ASC', [U]);
const anschreiben = basisRows.find((d) => d.typ === 'Anschreiben');
const lebenslauf = basisRows.find((d) => d.typ === 'Lebenslauf');
// Aktuelle Konfiguration für die Vorbelegung — Secrets (Passwörter, Token,
// API-Schlüssel) werden bewusst nicht ans Frontend gegeben, sondern nur
// als „vorhanden"-Flag.
const cfg = config.getAll();
setupWizard = {
aktiv: true,
persoenlich: await loadSettings(),
@@ -1728,6 +1733,14 @@ initializeDatabase().then(async () => {
hasApiKey: Boolean(config.get('OLLAMA_API_KEY')),
ollamaModel: config.get('OLLAMA_MODEL'),
ollamaHost: config.get('OLLAMA_HOST'),
cfg: {
MAIL_HOST: cfg.MAIL_HOST, MAIL_SMTP_PORT: cfg.MAIL_SMTP_PORT, MAIL_IMAP_PORT: cfg.MAIL_IMAP_PORT,
MAIL_USER: cfg.MAIL_USER, MAIL_FROM_NAME: cfg.MAIL_FROM_NAME, MAIL_FROM: cfg.MAIL_FROM,
MAIL_IMAP_MAILBOX: cfg.MAIL_IMAP_MAILBOX, MAIL_POLL_MS: cfg.MAIL_POLL_MS,
hasMailPassword: Boolean(cfg.MAIL_PASSWORD),
CALDAV_URL: cfg.CALDAV_URL, CALDAV_ALARM_MIN: cfg.CALDAV_ALARM_MIN, CALDAV_POLL_MS: cfg.CALDAV_POLL_MS,
hasApiToken: Boolean(cfg.API_TOKEN),
},
};
}
@@ -2063,6 +2076,49 @@ initializeDatabase().then(async () => {
}
});
// Generisches Speichern für die weiteren Wizard-Schritte (E-Mail, CalDAV,
// REST-API). Nimmt nur Schlüssel aus config.DEFAULTS entgegen und schreibt
// sie gezielt via setForUser — ungenannte Keys bleiben unangetastet. Hosts
// werden gegen SSRF geprüft, der API-Token auf Kollision mit anderem Benutzer.
app.post('/api/einrichtung/config', async (req, res) => {
try {
const u = uid();
const erlaubt = new Set(Object.keys(config.DEFAULTS));
const werte = {};
Object.keys(req.body || {}).forEach(function (k) {
if (erlaubt.has(k)) werte[k] = String(req.body[k] == null ? '' : req.body[k]).trim();
});
// SSRF-Guard für externe Hosts, die der Server anruft.
if (werte.OLLAMA_HOST) {
try { await assertSafeUrl(werte.OLLAMA_HOST); }
catch (vErr) { return res.status(400).json({ error: 'Ollama-Host abgelehnt: ' + (vErr.message || 'ungültig') }); }
}
if (werte.CALDAV_URL) {
try { await assertSafeUrl(werte.CALDAV_URL); }
catch (vErr) { return res.status(400).json({ error: 'Kalender-URL abgelehnt: ' + (vErr.message || 'ungültig') }); }
}
if (werte.MAIL_HOST) {
try { await assertSafeHost(werte.MAIL_HOST); }
catch (vErr) { return res.status(400).json({ error: 'Mail-Host abgelehnt: ' + (vErr.message || 'ungültig') }); }
}
// API-Token identifiziert den Benutzer gegenüber /api/v1 — Kollision würde
// beide Nutzer aussperren, daher schon hier ablehnen.
if (werte.API_TOKEN) {
const fremd = await dbGet(
`SELECT u.username FROM app_state a JOIN users u ON u.id = a.user_id
WHERE a.key = ? AND a.value = ? AND a.user_id != ?`,
[config.PREFIX + 'API_TOKEN', werte.API_TOKEN, u]
);
if (fremd) return res.status(409).json({ error: 'API-Token bereits von anderem Benutzer belegt. Bitte „Neu generieren".' });
}
for (const k of Object.keys(werte)) await config.setForUser(u, k, werte[k]);
res.json({ success: true });
} catch (error) {
console.error('Einrichtung/config:', error);
res.status(500).json({ error: 'Serverfehler' });
}
});
// Wizard dauerhaft ausblenden (app_state-Flag), damit er nicht bei jedem
// Besuch der leeren Startseite wieder erscheint.
app.post('/api/einrichtung/verstecken', async (req, res) => {