Quellcode des ICON CRM (4)
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
import express from 'express';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { pool } from '../db.js';
|
||||
import { requireAuth, requireAdmin } from '../middleware/auth.js';
|
||||
import { publicUser } from './auth.js';
|
||||
import { normalizeAvatar, normalizeColor } from '../userFields.js';
|
||||
import { logHistory } from '../history.js';
|
||||
|
||||
const router = express.Router();
|
||||
router.use(requireAuth);
|
||||
|
||||
/*
|
||||
* Ausnahme von der Admin-Regel: die Benutzerliste brauchen alle Rollen -
|
||||
* fuer @-Erwaehnungen, Zustaendigkeiten, Farben und die Historie. Der
|
||||
* Endpunkt liefert deshalb bewusst nur die Anzeigefelder.
|
||||
*/
|
||||
router.get('/directory', async (req, res, next) => {
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT id, username, name, role, color, avatar, is_active
|
||||
FROM icon_users ORDER BY name`
|
||||
);
|
||||
res.json({ users: rows.map((row) => ({ ...row, is_active: Boolean(row.is_active) })) });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.use(requireAdmin);
|
||||
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT id, username, email, name, role, color, avatar, theme, theme_mode,
|
||||
is_active, created_at, updated_at
|
||||
FROM icon_users ORDER BY name`
|
||||
);
|
||||
res.json({ users: rows.map(publicUser) });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', async (req, res, next) => {
|
||||
const username = String(req.body?.username || '').trim();
|
||||
const name = String(req.body?.name || '').trim();
|
||||
const email = String(req.body?.email || '').trim().toLowerCase() || null;
|
||||
const role = req.body?.role === 'admin' ? 'admin' : 'user';
|
||||
const password = String(req.body?.password || '');
|
||||
|
||||
if (!username || !name) return res.status(400).json({ error: 'Benutzername und Name sind erforderlich.' });
|
||||
if (password.length < 8) return res.status(400).json({ error: 'Das Passwort muss mindestens 8 Zeichen haben.' });
|
||||
|
||||
const color = normalizeColor(req.body?.color);
|
||||
if (color.error) return res.status(400).json({ error: color.error });
|
||||
const avatar = normalizeAvatar(req.body?.avatar);
|
||||
if (avatar.error) return res.status(400).json({ error: avatar.error });
|
||||
|
||||
try {
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO icon_users (username, email, name, role, password_hash, color, avatar, is_active)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 1)`,
|
||||
[username, email, name, role, await bcrypt.hash(password, 12), color.value, avatar.value]
|
||||
);
|
||||
await logHistory({
|
||||
entityType: 'user',
|
||||
entityId: result.insertId,
|
||||
entityLabel: name,
|
||||
action: 'created',
|
||||
text: `Benutzer ${name} angelegt`,
|
||||
userId: req.user.id,
|
||||
});
|
||||
const [rows] = await pool.query('SELECT * FROM icon_users WHERE id = ?', [result.insertId]);
|
||||
res.status(201).json({ user: publicUser(rows[0]) });
|
||||
} catch (err) {
|
||||
if (err.code === 'ER_DUP_ENTRY') {
|
||||
return res.status(409).json({ error: 'Benutzername oder E-Mail ist bereits vergeben.' });
|
||||
}
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.patch('/:id', async (req, res, next) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Kennung.' });
|
||||
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM icon_users WHERE id = ? LIMIT 1', [id]);
|
||||
const target = rows[0];
|
||||
if (!target) return res.status(404).json({ error: 'Benutzer nicht gefunden.' });
|
||||
|
||||
const fields = [];
|
||||
const values = [];
|
||||
|
||||
if (req.body?.name !== undefined) {
|
||||
const name = String(req.body.name).trim();
|
||||
if (!name) return res.status(400).json({ error: 'Der Name darf nicht leer sein.' });
|
||||
fields.push('name = ?');
|
||||
values.push(name);
|
||||
}
|
||||
if (req.body?.email !== undefined) {
|
||||
fields.push('email = ?');
|
||||
values.push(String(req.body.email).trim().toLowerCase() || null);
|
||||
}
|
||||
if (req.body?.color !== undefined) {
|
||||
const color = normalizeColor(req.body.color);
|
||||
if (color.error) return res.status(400).json({ error: color.error });
|
||||
fields.push('color = ?');
|
||||
values.push(color.value);
|
||||
}
|
||||
if (req.body?.avatar !== undefined) {
|
||||
const avatar = normalizeAvatar(req.body.avatar);
|
||||
if (avatar.error) return res.status(400).json({ error: avatar.error });
|
||||
fields.push('avatar = ?');
|
||||
values.push(avatar.value);
|
||||
}
|
||||
if (req.body?.password) {
|
||||
const password = String(req.body.password);
|
||||
if (password.length < 8) return res.status(400).json({ error: 'Das Passwort muss mindestens 8 Zeichen haben.' });
|
||||
fields.push('password_hash = ?');
|
||||
values.push(await bcrypt.hash(password, 12));
|
||||
}
|
||||
|
||||
// Sicherheitsnetz: der letzte aktive Administrator darf sich weder
|
||||
// degradieren noch abschalten - sonst kommt niemand mehr an die
|
||||
// Nutzerverwaltung.
|
||||
const losesAdmin =
|
||||
(req.body?.role !== undefined && req.body.role !== 'admin' && target.role === 'admin') ||
|
||||
(req.body?.is_active !== undefined && !req.body.is_active && target.role === 'admin');
|
||||
|
||||
if (losesAdmin && (await countOtherActiveAdmins(id)) === 0) {
|
||||
return res.status(400).json({ error: 'Der letzte aktive Administrator kann nicht geändert werden.' });
|
||||
}
|
||||
|
||||
if (req.body?.role !== undefined) {
|
||||
fields.push('role = ?');
|
||||
values.push(req.body.role === 'admin' ? 'admin' : 'user');
|
||||
}
|
||||
if (req.body?.is_active !== undefined) {
|
||||
fields.push('is_active = ?');
|
||||
values.push(req.body.is_active ? 1 : 0);
|
||||
}
|
||||
|
||||
if (fields.length === 0) return res.status(400).json({ error: 'Keine Änderungen übergeben.' });
|
||||
|
||||
await pool.query(`UPDATE icon_users SET ${fields.join(', ')} WHERE id = ?`, [...values, id]);
|
||||
await logHistory({
|
||||
entityType: 'user',
|
||||
entityId: id,
|
||||
entityLabel: target.name,
|
||||
action: 'updated',
|
||||
text: `Benutzer ${target.name} bearbeitet`,
|
||||
userId: req.user.id,
|
||||
});
|
||||
|
||||
const [updated] = await pool.query('SELECT * FROM icon_users WHERE id = ?', [id]);
|
||||
res.json({ user: publicUser(updated[0]) });
|
||||
} catch (err) {
|
||||
if (err.code === 'ER_DUP_ENTRY') {
|
||||
return res.status(409).json({ error: 'Benutzername oder E-Mail ist bereits vergeben.' });
|
||||
}
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:id', async (req, res, next) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Kennung.' });
|
||||
if (id === req.user.id) return res.status(400).json({ error: 'Das eigene Konto kann nicht gelöscht werden.' });
|
||||
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM icon_users WHERE id = ? LIMIT 1', [id]);
|
||||
const target = rows[0];
|
||||
if (!target) return res.status(404).json({ error: 'Benutzer nicht gefunden.' });
|
||||
if (target.role === 'admin' && (await countOtherActiveAdmins(id)) === 0) {
|
||||
return res.status(400).json({ error: 'Der letzte aktive Administrator kann nicht gelöscht werden.' });
|
||||
}
|
||||
|
||||
await pool.query('DELETE FROM icon_users WHERE id = ?', [id]);
|
||||
await logHistory({
|
||||
entityType: 'user',
|
||||
entityId: null,
|
||||
entityLabel: target.name,
|
||||
action: 'deleted',
|
||||
text: `Benutzer ${target.name} gelöscht`,
|
||||
userId: req.user.id,
|
||||
});
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
async function countOtherActiveAdmins(excludeId) {
|
||||
const [rows] = await pool.query(
|
||||
"SELECT COUNT(*) AS count FROM icon_users WHERE role = 'admin' AND is_active = 1 AND id <> ?",
|
||||
[excludeId]
|
||||
);
|
||||
return Number(rows[0].count);
|
||||
}
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user