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:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user