This commit is contained in:
2026-03-23 02:09:14 +01:00
parent 705329d3c2
commit d8d46ed8e9
61 changed files with 6054 additions and 3116 deletions

View File

@@ -0,0 +1,173 @@
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, 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)) : 1440;
}
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;
}