diff --git a/server.js b/server.js index 2b97899..f99c0fa 100644 --- a/server.js +++ b/server.js @@ -4284,6 +4284,53 @@ initializeDatabase().then(async () => { } }); + // Einzelnen Benutzer verwalten (Detailansicht von „Bestehende Benutzer"). + 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]); + 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( + 'SELECT id, firma, stelle, status, datum FROM bewerbungen WHERE user_id = ? ORDER BY date(datum) DESC, created_at DESC LIMIT 10', + [id] + ); + const adminCount = await dbGet('SELECT COUNT(*) AS c FROM users WHERE is_admin = 1'); + res.render('admin-user', { + user, + currentUserId: req.user.id, + anzahlBewerbungen: anzahl ? anzahl.c : 0, + bewerbungen: bewerbungen || [], + adminCount: adminCount ? adminCount.c : 0, + hideSettings: false, + }); + } catch (error) { + console.error('Admin user detail error:', error); + res.status(500).send('Serverfehler'); + } + }); + + // Admin-Status eines Benutzers umschalten. Der letzte Admin darf nicht + // abgesetzt werden (sonst sperrt man sich aus). + app.post('/admin/users/:id/toggle-admin', requireAdmin, async (req, res) => { + try { + const id = Number(req.params.id); + const target = await dbGet('SELECT id, is_admin FROM users WHERE id = ?', [id]); + if (!target) return res.status(404).send('Benutzer nicht gefunden.'); + if (target.is_admin) { + const adminCount = await dbGet('SELECT COUNT(*) AS c FROM users WHERE is_admin = 1'); + if (adminCount && adminCount.c <= 1) return res.status(400).send('Der letzte Admin darf nicht abgesetzt werden.'); + } + const neu = target.is_admin ? 0 : 1; + await dbRun('UPDATE users SET is_admin = ? WHERE id = ?', [neu, id]); + await auditLog(req.user.id, id, `admin_toggle → ${neu ? 'admin' : 'user'}`); + res.redirect('/admin/users/' + id); + } catch (error) { + console.error('Admin toggle-admin 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 new file mode 100644 index 0000000..94c0775 --- /dev/null +++ b/views/admin-user.ejs @@ -0,0 +1,193 @@ + + + + <%- include('partials/head') %> + + + <%- include('partials/header') %> + +
+ + + + + Zur Benutzerverwaltung + + + <% + const istIch = user.id === currentUserId; + const letzterAdmin = user.is_admin && adminCount <= 1; + %> + +
+ + <%= String(user.username || '?').charAt(0).toUpperCase() %> + +
+
+

<%= user.username %>

+ <% if (user.is_admin) { %> + Admin + <% } else { %> + Benutzer + <% } %> + <% if (istIch) { %>(du)<% } %> +
+

+ <%= anzahlBewerbungen %> <%= anzahlBewerbungen === 1 ? 'Bewerbung' : 'Bewerbungen' %> · + angelegt +

+
+
+ +
+ +
+

Benutzer verwalten

+ +
+ +
+
+

Admin-Status

+

+ <% if (letzterAdmin) { %>Letzter Admin – kann nicht abgesetzt werden.<% } else { %>Admins können Benutzer verwalten und andere Konten übernehmen.<% } %> +

+
+
+ +
+
+ +
+ + +
+
+
+

Passwort zurücksetzen

+

Alle Sitzungen dieses Benutzers werden ungültig.

+
+ +
+ +
+ +
+ + +
+ <% if (!istIch) { %> +
+ +
+ <% } %> + + + Jobsuche + +
+ + <% if (!istIch) { %> +
+ + +
+
+

Benutzer löschen

+

Löscht das Konto inkl. aller Bewerbungen und Dateien. Unumkehrbar.

+
+
+ +
+
+ <% } %> +
+
+ + +
+

Letzte Bewerbungen

+ <% if (bewerbungen.length) { %> +
    + <% bewerbungen.forEach(function(b){ %> +
  • + +
    +

    <%= b.firma %>

    +

    <%= b.stelle %>

    +
    + <%= new Date(b.datum).toLocaleDateString('de-DE') %> +
  • + <% }); %> +
+ <% if (anzahlBewerbungen > bewerbungen.length) { %> +

… und <%= anzahlBewerbungen - bewerbungen.length %> weitere.

+ <% } %> + <% } else { %> +

Noch keine Bewerbungen.

+ <% } %> +
+
+
+ + <%- include('partials/footer') %> + + + + + + <%- include('partials/favorit-script') %> + + \ No newline at end of file diff --git a/views/admin.ejs b/views/admin.ejs index 0719ab0..8bdf928 100644 --- a/views/admin.ejs +++ b/views/admin.ejs @@ -104,13 +104,14 @@ <% users.forEach(function(u) { %> -
+ <%= String(u.username || '?').charAt(0).toUpperCase() %> - <%= u.username %> + <%= u.username %> <% if (u.id === currentUserId) { %>(du)<% } %> -
+ + <% if (u.is_admin) { %> @@ -128,11 +129,6 @@
- - - Jobsuche - <% if (u.id !== currentUserId) { %>
@@ -148,15 +144,6 @@ Passwort - <% if (u.id !== currentUserId) { %> - - -
- <% } %>
diff --git a/views/bewerbung.ejs b/views/bewerbung.ejs index 5648692..343b294 100644 --- a/views/bewerbung.ejs +++ b/views/bewerbung.ejs @@ -496,10 +496,30 @@
- <% const genStatus = application.generierung_status; %> - <% if (genStatus || (anhaenge && anhaenge.length) || (application.stellenbeschreibung && application.stellenbeschreibung.trim())) { %> -
+ <% if (zeigeUnterlagen) { %> + + +
+ + +
+ +

Bewerbungsunterlagen

@@ -1074,6 +1094,35 @@ } catch (e) { /* keep polling */ } }, 4000); } + + // --- Umschalter Unterlagen <-> E-Mail-Korrespondenz (rechte Spalte) --- + (function () { + var unterlagen = document.getElementById('unterlagen'); + var korrespondenz = document.getElementById('korrespondenz'); + if (!unterlagen || !korrespondenz) return; // kein Toggle nötig + var tabs = Array.prototype.slice.call(document.querySelectorAll('#rechtsTabs [data-tab]')); + function setActive(name) { + unterlagen.classList.toggle('hidden', name !== 'unterlagen'); + korrespondenz.classList.toggle('hidden', name !== 'korrespondenz'); + tabs.forEach(function (t) { t.classList.toggle('active', t.getAttribute('data-tab') === name); }); + // E-Mail-iframes nachmessen, falls sie beim Laden versteckt waren. + if (name === 'korrespondenz') { + korrespondenz.querySelectorAll('.email-html-frame').forEach(fitFrame); + } + } + function fromHash() { + var h = (location.hash || '').replace(/^#/, ''); + return (h === 'unterlagen' || h === 'korrespondenz') ? h : 'unterlagen'; + } + setActive(fromHash()); + window.addEventListener('hashchange', function () { setActive(fromHash()); }); + tabs.forEach(function (t) { + t.addEventListener('click', function () { + history.replaceState(null, '', '#' + t.getAttribute('data-tab')); + setActive(t.getAttribute('data-tab')); + }); + }); + })(); })(); // Generation form: require at least one document, and keep the