V0.1
This commit is contained in:
173
server/routes/admin/index.js
Normal file
173
server/routes/admin/index.js
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user