Quellcode des ICON CRM (3)
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user