Quellcode des ICON CRM (3)

This commit is contained in:
2026-09-14 16:55:34 +02:00
parent 0a597ae527
commit 28763fed6d
25 changed files with 6158 additions and 0 deletions
+169
View File
@@ -0,0 +1,169 @@
import express from 'express';
import { createHash } from 'node:crypto';
import bcrypt from 'bcryptjs';
import { pool } from '../db.js';
import { signToken, requireAuth } from '../middleware/auth.js';
import { normalizeAvatar, normalizeColor, normalizeTheme, normalizeThemeMode } from '../userFields.js';
const router = express.Router();
export function publicUser(row) {
/*
* Ein Design, das es nicht mehr gibt, wird als "nicht gesetzt" ausgeliefert.
*
* Die Prüfung beim Schreiben reicht dafür nicht: sie greift erst ab dem
* Zeitpunkt, zu dem ein Wert aus der Liste fliegt. Wer ihn vorher gewählt
* hatte, trägt ihn weiter in der Datenbank - und die Oberfläche bekäme einen
* Namen, zu dem sie keine Beschriftung kennt.
*/
const theme = normalizeTheme(row.theme ?? null);
return {
id: row.id,
username: row.username,
email: row.email ?? null,
name: row.name,
role: row.role,
color: row.color ?? null,
avatar: row.avatar ?? null,
theme: theme.error ? null : theme.value,
theme_mode: row.theme_mode ?? null,
is_active: Boolean(row.is_active),
created_at: row.created_at,
updated_at: row.updated_at,
};
}
// POST /api/auth/login { identifier, password }
router.post('/login', async (req, res, next) => {
const identifier = String(req.body?.identifier || '').trim();
const password = String(req.body?.password || '');
if (!identifier || !password) {
return res.status(400).json({ error: 'Benutzername und Passwort sind erforderlich.' });
}
try {
const [rows] = await pool.query(
'SELECT * FROM icon_users WHERE username = ? OR email = ? LIMIT 1',
[identifier, identifier.toLowerCase()]
);
const user = rows[0];
// Bewusst dieselbe Meldung fuer "kein Benutzer" und "falsches Passwort",
// damit sich ueber die API keine Benutzernamen aufzaehlen lassen.
const invalid = () => res.status(401).json({ error: 'Benutzername oder Passwort ist falsch.' });
if (!user) return invalid();
const ok = await verifyPassword(password, user);
if (!ok) return invalid();
if (!user.is_active) {
return res.status(403).json({ error: 'Dieses Benutzerkonto ist deaktiviert.' });
}
res.json({ token: signToken(user), user: publicUser(user) });
} catch (err) {
next(err);
}
});
/*
* Passwortpruefung mit Uebergang aus dem alten CRM.
*
* Dort lagen die Passwoerter als ungesalzenes SHA-256 in Supabase. Solche
* Hashes sind an ihrer Form erkennbar (64 Hexziffern) - ein bcrypt-Hash
* beginnt immer mit $2. Stimmt ein Altpasswort, wird es im selben Zug auf
* bcrypt umgeschrieben: beim naechsten Mal gilt nur noch der starke Hash.
*
* Das ist bewusst kein dauerhafter zweiter Weg, sondern eine Einbahnstrasse -
* neu vergeben wird ausschliesslich bcrypt.
*/
async function verifyPassword(password, user) {
const hash = String(user.password_hash ?? '');
if (/^[a-f0-9]{64}$/i.test(hash)) {
const legacy = createHash('sha256').update(password, 'utf8').digest('hex');
if (legacy.toLowerCase() !== hash.toLowerCase()) return false;
await pool.query('UPDATE icon_users SET password_hash = ? WHERE id = ?', [
await bcrypt.hash(password, 12),
user.id,
]);
console.log(`[ICON CRM] Passwort von "${user.username}" auf bcrypt umgestellt.`);
return true;
}
return bcrypt.compare(password, hash);
}
// GET /api/auth/me
router.get('/me', requireAuth, (req, res) => {
res.json({ user: publicUser(req.user) });
});
// PATCH /api/auth/me { color?, avatar?, theme?, theme_mode? }
// Jeder darf seine eigene Farbe, sein Foto und seine Darstellung setzen.
// null bzw. "" setzt das Feld zurueck.
router.patch('/me', requireAuth, async (req, res, next) => {
const fields = [];
const values = [];
const changed = {};
for (const [field, normalize] of [
['color', normalizeColor],
['avatar', normalizeAvatar],
['theme', normalizeTheme],
['theme_mode', normalizeThemeMode],
]) {
if (req.body?.[field] === undefined) continue;
const result = normalize(req.body[field]);
if (result.error) return res.status(400).json({ error: result.error });
fields.push(`${field} = ?`);
values.push(result.value);
changed[field] = result.value;
}
if (fields.length === 0) {
return res.status(400).json({ error: 'Keine Änderungen übergeben.' });
}
try {
await pool.query(`UPDATE icon_users SET ${fields.join(', ')} WHERE id = ?`, [...values, req.user.id]);
res.json({ user: publicUser({ ...req.user, ...changed }) });
} catch (err) {
next(err);
}
});
// POST /api/auth/change-password { current_password, new_password }
router.post('/change-password', requireAuth, async (req, res, next) => {
const current = String(req.body?.current_password || '');
const next_ = String(req.body?.new_password || '');
if (!current || !next_) {
return res.status(400).json({ error: 'Aktuelles und neues Passwort sind erforderlich.' });
}
if (next_.length < 8) {
return res.status(400).json({ error: 'Das neue Passwort muss mindestens 8 Zeichen haben.' });
}
try {
const [rows] = await pool.query('SELECT password_hash FROM icon_users WHERE id = ? LIMIT 1', [
req.user.id,
]);
if (!rows[0]) return res.status(404).json({ error: 'Benutzer nicht gefunden.' });
const ok = await bcrypt.compare(current, rows[0].password_hash);
if (!ok) return res.status(400).json({ error: 'Das aktuelle Passwort ist falsch.' });
await pool.query('UPDATE icon_users SET password_hash = ? WHERE id = ?', [
await bcrypt.hash(next_, 12),
req.user.id,
]);
res.json({ ok: true });
} catch (err) {
next(err);
}
});
export default router;