Einrichtung: Setup-Assistent fuer neue Nutzer auf der Startseite
Nutzer ohne Bewerbungen sehen einen mehrstufigen Wizard, der Persoenliche
Angaben, Basis-Unterlagen und Ollama-KI per fetch speichert. Dashboard bleibt
im DOM (hidden), damit main.js und "Bewerbung manuell anlegen" funktionieren.
Neue Endpunkte /api/einrichtung/{persoenlich,basis,ollama,verstecken} mit
SSRF-Guard; Ollama gezielt via setForUser, ohne Mail/Kalender/Token zu leeren.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1707,6 +1707,30 @@ initializeDatabase().then(async () => {
|
||||
ORDER BY yearmonth DESC
|
||||
`, [U, U]);
|
||||
|
||||
// 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).
|
||||
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])
|
||||
: null;
|
||||
let setupWizard = { aktiv: false };
|
||||
if (keinDashboard && !(verstecktRow && verstecktRow.value === '1')) {
|
||||
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');
|
||||
setupWizard = {
|
||||
aktiv: true,
|
||||
persoenlich: await loadSettings(),
|
||||
anschreiben: anschreiben ? { id: anschreiben.id, inhalt: anschreiben.inhalt || '' } : null,
|
||||
lebenslauf: lebenslauf ? { id: lebenslauf.id, inhalt: lebenslauf.inhalt || '' } : null,
|
||||
hasApiKey: Boolean(config.get('OLLAMA_API_KEY')),
|
||||
ollamaModel: config.get('OLLAMA_MODEL'),
|
||||
ollamaHost: config.get('OLLAMA_HOST'),
|
||||
};
|
||||
}
|
||||
|
||||
res.render('index', {
|
||||
applications,
|
||||
statistics: {
|
||||
@@ -1722,7 +1746,8 @@ initializeDatabase().then(async () => {
|
||||
caldavTz: caldav.TZ,
|
||||
artOptions: ART_OPTIONS,
|
||||
statusOptions: STATUS_OPTIONS,
|
||||
labelOptions: LABEL_OPTIONS
|
||||
labelOptions: LABEL_OPTIONS,
|
||||
setupWizard
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
@@ -1967,6 +1992,93 @@ initializeDatabase().then(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ----- Einrichtungsassistent (Onboarding auf der Startseite) -----
|
||||
// JSON-Endpunkte, die der Wizard Schritt für Schritt per fetch anstößt — ohne
|
||||
// Seitenwechsel, damit der Nutzer im Flow bleibt. Jeder speichert für den
|
||||
// aktuellen Benutzer und antwortet mit { success } bzw. { error }.
|
||||
app.post('/api/einrichtung/persoenlich', async (req, res) => {
|
||||
try {
|
||||
const { name, adresse, kundennummer, email, telefon, ort, webseite, geburtsdatum } = req.body;
|
||||
await dbRun(
|
||||
`INSERT INTO settings (user_id, name, adresse, kundennummer, email, telefon, ort, webseite, geburtsdatum)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET
|
||||
name = excluded.name, adresse = excluded.adresse, kundennummer = excluded.kundennummer,
|
||||
email = excluded.email, telefon = excluded.telefon, ort = excluded.ort,
|
||||
webseite = excluded.webseite, geburtsdatum = excluded.geburtsdatum`,
|
||||
[uid(), sanitizeInput(name), sanitizeInput(adresse), sanitizeInput(kundennummer),
|
||||
sanitizeInput(email), sanitizeInput(telefon), sanitizeInput(ort),
|
||||
sanitizeInput(webseite), sanitizeInput(geburtsdatum)]
|
||||
);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Einrichtung/persoenlich:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
// Basis-Unterlage (Anschreiben/Lebenslauf/…) je Typ upserten. Leerinhalt
|
||||
// löscht den Eintrag nicht, sondern speichert ihn als leeren Text — so bleibt
|
||||
// der Typ beim erneuten Öffnen sichtbar.
|
||||
app.post('/api/einrichtung/basis', async (req, res) => {
|
||||
try {
|
||||
const typ = String(req.body.typ || '').trim();
|
||||
if (!typ) return res.status(400).json({ error: 'Typ fehlt' });
|
||||
const inhalt = req.body.inhalt != null ? String(req.body.inhalt) : '';
|
||||
const existing = await dbGet('SELECT id FROM basis_dokumente WHERE user_id = ? AND typ = ?', [uid(), typ]);
|
||||
if (existing) {
|
||||
await dbRun('UPDATE basis_dokumente SET inhalt = ? WHERE id = ? AND user_id = ?',
|
||||
[inhalt, existing.id, uid()]);
|
||||
} else {
|
||||
await dbRun('INSERT INTO basis_dokumente (user_id, typ, name, inhalt) VALUES (?, ?, ?, ?)',
|
||||
[uid(), typ, typ, inhalt]);
|
||||
}
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Einrichtung/basis:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
// Ollama-KI: nur die drei Ollama-Keys setzen, ohne die restliche Konfiguration
|
||||
// (Mail, Kalender, API-Token) anzutasten — config.saveAll würde alle nicht
|
||||
// übergebenen Keys leeren, daher gezielt über setForUser.
|
||||
app.post('/api/einrichtung/ollama', async (req, res) => {
|
||||
try {
|
||||
const u = uid();
|
||||
const apiKey = req.body.OLLAMA_API_KEY != null ? String(req.body.OLLAMA_API_KEY).trim() : '';
|
||||
const model = req.body.OLLAMA_MODEL != null ? String(req.body.OLLAMA_MODEL).trim() : '';
|
||||
const host = req.body.OLLAMA_HOST != null ? String(req.body.OLLAMA_HOST).trim() : '';
|
||||
if (host) {
|
||||
try { await assertSafeUrl(host); }
|
||||
catch (vErr) { return res.status(400).json({ error: 'Host abgelehnt: ' + (vErr.message || 'ungültig') }); }
|
||||
}
|
||||
await config.setForUser(u, 'OLLAMA_API_KEY', apiKey);
|
||||
if (model) await config.setForUser(u, 'OLLAMA_MODEL', model);
|
||||
if (host) await config.setForUser(u, 'OLLAMA_HOST', host);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Einrichtung/ollama:', 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) => {
|
||||
try {
|
||||
await dbRun(
|
||||
"INSERT INTO app_state (user_id, key, value) VALUES (?, 'state:einrichtung_versteckt', '1') " +
|
||||
"ON CONFLICT(user_id, key) DO UPDATE SET value = '1'",
|
||||
[uid()]
|
||||
);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Einrichtung/verstecken:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
// ----- Indeed import (called by the browser extension) -----
|
||||
app.post('/api/indeed-import', async (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user