diff --git a/server.js b/server.js index f99c0fa..cba9e65 100644 --- a/server.js +++ b/server.js @@ -1266,9 +1266,27 @@ async function initializeDatabase() { username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, is_admin INTEGER NOT NULL DEFAULT 0, + -- Darf der Nutzer selber Suchläufe starten („Jetzt suchen") und darf er + -- den Zeitplan (Wochentage/Uhrzeit/aktiv) ändern? Beides ist standardmäßig + -- AUS — der Admin aktiviert es pro Nutzer. Der automatische Zeitplan selbst + -- läuft weiter, sobald aktiviert (nur das Starten/Ändern ist beschränkt). + darf_suchlauf_starten INTEGER NOT NULL DEFAULT 0, + darf_zeitplan_aendern INTEGER NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); + // Bestehende Installationen: die beiden Spalten kamen nach dem CREATE TABLE + // dazu. ALTER ist idempotent via PRAGMA table_info (Default 0 = deaktiviert, + // auch für alle existierenden Nutzer). + { + const userSpalten = await dbAll('PRAGMA table_info(users)'); + if (!userSpalten.some((s) => s.name === 'darf_suchlauf_starten')) { + await exec('ALTER TABLE users ADD COLUMN darf_suchlauf_starten INTEGER NOT NULL DEFAULT 0'); + } + if (!userSpalten.some((s) => s.name === 'darf_zeitplan_aendern')) { + await exec('ALTER TABLE users ADD COLUMN darf_zeitplan_aendern INTEGER NOT NULL DEFAULT 0'); + } + } await exec(` CREATE TABLE IF NOT EXISTS sessions ( token TEXT PRIMARY KEY, @@ -4074,6 +4092,16 @@ initializeDatabase().then(async () => { "SELECT value FROM app_state WHERE user_id = ? AND key = 'cfg:OLLAMA_API_KEY' AND value != ''", [ziel.id] ); + // Pro-Nutzer-Rechte: darf der Zielbenutzer Suchläufe manuell starten und + // darf er den Zeitplan ändern? Ein Admin, der ein fremdes Profil verwaltet + // (fremd), darf beides immer — die Rechte schränken nur den eigentlichen + // Nutzer auf seiner eigenen /jobsuche ein. + const rechte = await dbGet( + 'SELECT darf_suchlauf_starten, darf_zeitplan_aendern FROM users WHERE id = ?', + [ziel.id] + ); + const darfSuchlaufStarten = ziel.fremd || Boolean(rechte && rechte.darf_suchlauf_starten); + const darfZeitplanAendern = ziel.fremd || Boolean(rechte && rechte.darf_zeitplan_aendern); res.render('jobsuche', { profil, @@ -4086,6 +4114,8 @@ initializeDatabase().then(async () => { hatLebenslauf: Boolean(lebenslauf), hatUnterlagen: Boolean(unterlagen), hatOllamaKey: Boolean(ollamaKey), + darfSuchlaufStarten, + darfZeitplanAendern, fehler: req.query.fehler ? String(req.query.fehler) : null, hinweis: req.query.hinweis ? String(req.query.hinweis) : null, hideSettings: false, @@ -4103,6 +4133,21 @@ initializeDatabase().then(async () => { const suffix = ziel.fremd ? `?user=${ziel.id}` : ''; const profil = suchprofil.fromForm(req.body, sanitizeInput); + + // Ohne das Recht „Zeitplan ändern" (und nicht als Admin unterwegs) darf + // der Nutzer die Wochentage/Uhrzeit/den aktiv-Schalter nicht anfassen — + // der adminstrulierte Zeitplan bleibt erhalten. Nur die Suchbegriffe / + // der Modus sind dann noch editierbar. + if (!ziel.fremd) { + const rechte = await dbGet('SELECT darf_zeitplan_aendern FROM users WHERE id = ?', [ziel.id]); + if (!rechte || !rechte.darf_zeitplan_aendern) { + const aktuell = await loadSuchprofil(ziel.id); + profil.aktiv = aktuell.aktiv; + profil.zeitplan_tage = aktuell.zeitplan_tage; + profil.zeitplan_zeit = aktuell.zeitplan_zeit; + } + } + const fehler = suchprofil.validate(profil); if (fehler.length) { return res.redirect(`/jobsuche${suffix}${suffix ? '&' : '?'}fehler=${encodeURIComponent(fehler.join(' '))}`); @@ -4164,6 +4209,16 @@ initializeDatabase().then(async () => { const suffix = ziel.fremd ? `?user=${ziel.id}` : ''; const sep = suffix ? '&' : '?'; + // Manuelle Suchläufe sind ein eigenes Recht. Ein Admin, der ein fremdes + // Profil verwaltet, darf immer starten — der eigentliche Nutzer nur, wenn + // der Admin es ihm freigegeben hat. + if (!ziel.fremd) { + const rechte = await dbGet('SELECT darf_suchlauf_starten FROM users WHERE id = ?', [ziel.id]); + if (!rechte || !rechte.darf_suchlauf_starten) { + return res.status(403).send('Suchläufe dürfen nur von einem Administrator gestartet werden.'); + } + } + const profil = await loadSuchprofil(ziel.id); const fehler = suchprofil.validate(profil); if (fehler.length) { @@ -4288,7 +4343,7 @@ initializeDatabase().then(async () => { app.get('/admin/users/:id', requireAdmin, async (req, res) => { try { const id = Number(req.params.id); - const user = await dbGet('SELECT id, username, is_admin, created_at FROM users WHERE id = ?', [id]); + const user = await dbGet('SELECT id, username, is_admin, darf_suchlauf_starten, darf_zeitplan_aendern, created_at FROM users WHERE id = ?', [id]); if (!user) return res.status(404).send('Benutzer nicht gefunden.'); const anzahl = await dbGet('SELECT COUNT(*) AS c FROM bewerbungen WHERE user_id = ?', [id]); const bewerbungen = await dbAll( @@ -4331,6 +4386,41 @@ initializeDatabase().then(async () => { } }); + // Darf der Nutzer Suchläufe selber starten („Jetzt suchen")? Standardmäßig + // aus — der Admin gibt es pro Nutzer frei. Der automatische Zeitplan läuft + // davon unberührt weiter, sobald er aktiviert ist. + app.post('/admin/users/:id/toggle-suchlauf', requireAdmin, async (req, res) => { + try { + const id = Number(req.params.id); + const target = await dbGet('SELECT id, darf_suchlauf_starten FROM users WHERE id = ?', [id]); + if (!target) return res.status(404).send('Benutzer nicht gefunden.'); + const neu = target.darf_suchlauf_starten ? 0 : 1; + await dbRun('UPDATE users SET darf_suchlauf_starten = ? WHERE id = ?', [neu, id]); + await auditLog(req.user.id, id, `suchlauf_starten → ${neu ? 'an' : 'aus'}`); + res.redirect('/admin/users/' + id); + } catch (error) { + console.error('Admin toggle-suchlauf error:', error); + res.status(500).send('Serverfehler'); + } + }); + + // Darf der Nutzer den Zeitplan (Wochentage/Uhrzeit/aktiv) ändern? + // Standardmäßig aus — nur der Admin stellt den Zeitplan ein. + app.post('/admin/users/:id/toggle-zeitplan', requireAdmin, async (req, res) => { + try { + const id = Number(req.params.id); + const target = await dbGet('SELECT id, darf_zeitplan_aendern FROM users WHERE id = ?', [id]); + if (!target) return res.status(404).send('Benutzer nicht gefunden.'); + const neu = target.darf_zeitplan_aendern ? 0 : 1; + await dbRun('UPDATE users SET darf_zeitplan_aendern = ? WHERE id = ?', [neu, id]); + await auditLog(req.user.id, id, `zeitplan_aendern → ${neu ? 'an' : 'aus'}`); + res.redirect('/admin/users/' + id); + } catch (error) { + console.error('Admin toggle-zeitplan error:', error); + res.status(500).send('Serverfehler'); + } + }); + // --- Impersonation: admin "logs in as" a user -------------------------- // Best practice (Django/Flask-impersonate style): the admin's own session // keeps its cookie token, but its user_id switches to the target and the diff --git a/views/admin-user.ejs b/views/admin-user.ejs index 94c0775..513ee5f 100644 --- a/views/admin-user.ejs +++ b/views/admin-user.ejs @@ -66,6 +66,42 @@
Suchläufe selber starten
+Manueller „Jetzt suchen"-Knopf auf der Jobsuche. Der automatische Zeitplan läuft davon unberührt.
+Zeitplan ändern dürfen
+Wochentage, Uhrzeit und „aktiv". Standard: Mo–Fr 17:00, verwaltet vom Administrator.
++ Der Zeitplan (aktiv, Wochentage, Uhrzeit) wird vom Administrator verwaltet. +
+ <% } %>+ Manuelle Suchläufe werden vom Administrator verwaltet. +
+ <% } %>