import { randomUUID } from 'crypto'; import { Router } from 'express'; import db from '../../db.js'; import { getSyncStatus, performLdapSync } from '../../ldap-sync.js'; import { badRequest, UUID } from '../../lib/http.js'; import { mapPublicUser } from '../../lib/mappers.js'; import { loadIntegrations, restartLdapSyncScheduler, saveIntegrations, } from '../../integrations.js'; import { hashPassword } from '../../password.js'; import { requireAdmin, requireAuth } from '../../middleware/auth.js'; export function createAdminRouter() { const admin = Router(); admin.use(requireAuth, requireAdmin); admin.get('/users', (_req, res) => { const rows = db .prepare( 'SELECT id, username, firstname, lastname, role, source, active, ldap_dn, created_at, updated_at FROM users ORDER BY username ASC', ) .all(); res.json(rows.map(mapPublicUser)); }); admin.post('/users', async (req, res) => { const { username, password, role } = req.body || {}; const un = String(username || '') .trim() .toLowerCase(); if (!un || !password) return badRequest(res, 'Benutzername und Passwort erforderlich.'); const r = role === 'admin' ? 'admin' : 'user'; const id = randomUUID(); const ph = await hashPassword(password); try { db.prepare( `INSERT INTO users (id, username, password_hash, role, source, active, updated_at) VALUES (?, ?, ?, ?, 'local', 1, datetime('now'))`, ).run(id, un, ph, r); } catch (e) { if (String(e.message || e).includes('UNIQUE')) { return res.status(409).json({ message: 'Benutzername bereits vergeben.' }); } throw e; } const row = db.prepare('SELECT * FROM users WHERE id = ?').get(id); res.status(201).json(mapPublicUser(row)); }); admin.put('/users/:id', async (req, res) => { const { id } = req.params; if (!UUID.test(id)) return res.status(404).json({ message: 'Nicht gefunden' }); const cur = db.prepare('SELECT * FROM users WHERE id = ?').get(id); if (!cur) return res.status(404).json({ message: 'Nicht gefunden' }); const b = req.body || {}; if (b.password != null && String(b.password).length > 0) { if (cur.source !== 'local') { return badRequest(res, 'Passwort nur für lokale Benutzer änderbar.'); } const ph = await hashPassword(b.password); db.prepare( 'UPDATE users SET password_hash = ?, updated_at = datetime(\'now\') WHERE id = ?', ).run(ph, id); } if (b.role !== undefined) { if (b.role !== 'admin' && b.role !== 'user') { return badRequest(res, 'Ungültige Rolle.'); } const admins = db .prepare( "SELECT COUNT(*) AS c FROM users WHERE role = 'admin' AND active = 1", ) .get().c; if (cur.role === 'admin' && b.role === 'user' && admins <= 1) { return res.status(400).json({ message: 'Letzter Administrator kann nicht herabgestuft werden.' }); } db.prepare('UPDATE users SET role = ?, updated_at = datetime(\'now\') WHERE id = ?').run( b.role, id, ); } if (b.active !== undefined) { const active = b.active ? 1 : 0; const admins = db .prepare( "SELECT COUNT(*) AS c FROM users WHERE role = 'admin' AND active = 1", ) .get().c; if (cur.role === 'admin' && cur.active && !active && admins <= 1) { return res.status(400).json({ message: 'Letzter Administrator kann nicht deaktiviert werden.' }); } db.prepare('UPDATE users SET active = ?, updated_at = datetime(\'now\') WHERE id = ?').run( active, id, ); } const row = db.prepare('SELECT * FROM users WHERE id = ?').get(id); res.json(mapPublicUser(row)); }); admin.delete('/users/:id', (req, res) => { const { id } = req.params; if (!UUID.test(id)) return res.status(404).json({ message: 'Nicht gefunden' }); if (id === req.session.userId) { return res.status(400).json({ message: 'Eigenes Konto kann nicht gelöscht werden.' }); } const cur = db.prepare('SELECT * FROM users WHERE id = ?').get(id); if (!cur) return res.status(404).json({ message: 'Nicht gefunden' }); if (cur.role === 'admin') { const admins = db .prepare( "SELECT COUNT(*) AS c FROM users WHERE role = 'admin' AND active = 1", ) .get().c; if (admins <= 1) { return res.status(400).json({ message: 'Letzter Administrator kann nicht gelöscht werden.' }); } } db.prepare('DELETE FROM users WHERE id = ?').run(id); res.json({ ok: true }); }); admin.get('/settings/integrations', (_req, res) => { res.json(loadIntegrations()); }); admin.put('/settings/integrations', (req, res) => { const b = req.body || {}; const cur = loadIntegrations(); if (b.ldap && typeof b.ldap === 'object') { const incoming = { ...b.ldap }; if (incoming.bindPassword === '' || incoming.bindPassword == null) { delete incoming.bindPassword; } if (incoming.syncIntervalMinutes != null) { const n = Number(incoming.syncIntervalMinutes); incoming.syncIntervalMinutes = Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0; } Object.assign(cur.ldap, incoming); if (b.ldap.userSearchFilter != null) { cur.ldap.userFilter = String(b.ldap.userSearchFilter); } } if (b.teamviewer && typeof b.teamviewer === 'object') { const inc = { ...b.teamviewer }; if (inc.bearerToken === '' || inc.bearerToken == null) { delete inc.bearerToken; } Object.assign(cur.teamviewer, inc); } saveIntegrations(cur); restartLdapSyncScheduler(); res.json(loadIntegrations()); }); admin.get('/ldap/sync-status', (_req, res) => { res.json(getSyncStatus(db)); }); admin.post('/ldap/sync', async (_req, res) => { const r = await performLdapSync(db, loadIntegrations, 'manual'); if (r.skipped) { return res.status(409).json({ message: r.message }); } res.json(r); }); return admin; }