From a50f5cb4a6e819ae8527b1caa3289198547135fe Mon Sep 17 00:00:00 2001 From: Sascha Wolf Date: Thu, 10 Sep 2026 10:29:23 +0200 Subject: [PATCH] fix(design): Farbwelt und Modus bleiben nach dem Neuladen erhalten requireAuth hat den Benutzer mit einer ausgeschriebenen Spaltenliste geladen, in der theme und theme_mode fehlten. GET /api/auth/me antwortete deshalb mit null, und der ThemeProvider hat das beim Neuladen als "nichts gewaehlt" gelesen: die Anzeige fiel auf perinco/System zurueck und hat den localStorage-Zwischenspeicher gleich mit ueberschrieben. Direkt nach dem Klick und nach dem Anmelden sah es richtig aus, weil PATCH /me den geaenderten Wert selbst zurueckgibt und der Login SELECT * benutzt - nur das Neuladen nicht. Dazu ein Test, der die Spaltenliste mit den Feldern von publicUser() vergleicht, damit ein neues Benutzerfeld nicht wieder still verloren geht. Co-Authored-By: Claude Opus 5 (1M context) --- docs/08-anleitung-entwickler.md | 6 +++- docs/09-quellcode.md | 8 +++++ server/package.json | 2 +- server/src/middleware/auth.js | 13 +++++-- server/test/sessionUser.test.mjs | 58 ++++++++++++++++++++++++++++++++ 5 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 server/test/sessionUser.test.mjs diff --git a/docs/08-anleitung-entwickler.md b/docs/08-anleitung-entwickler.md index c6f6de9..5d901d9 100644 --- a/docs/08-anleitung-entwickler.md +++ b/docs/08-anleitung-entwickler.md @@ -53,7 +53,7 @@ ansehen - siehe *Oberfläche prüfen ohne Datenbank* weiter unten. ```bash npm run lint # = tsc --noEmit, das ist die Wahrheit npm run build # muss ohne Fehler durchlaufen -cd server && npm test # Postfachanbindung, Berater-Liste, Benutzerfelder, LinkedIn-Foto +cd server && npm test # Postfachanbindung, Berater-Liste, Benutzerfelder, LinkedIn-Foto, Sitzungsbenutzer ``` Automatische Tests gibt es für zwei Bereiche, beide ohne Test-Framework, reines @@ -68,6 +68,10 @@ Node und ohne Datenbank: das mehrere Namen kommagetrennt hält. Es steckt im Speichern, im Verlauf, in der Auswertung und in der Übernahme; ein Fehler darin fällt nicht auf, weil die Liste weiter Namen zeigt und nur die Zahlen falsch wären. +* `server/test/sessionUser.test.mjs` vergleicht die Spaltenliste in + `requireAuth` mit den Feldern, die `publicUser()` ausliefert. Fehlt dort eine + Spalte, antwortet die API still mit `null` - so ist die gespeicherte + Designwahl beim Neuladen verloren gegangen. Für alles andere gilt: `tsc`, ein Build und ein Blick in die Oberfläche - notfalls mit dem Stub aus [Oberfläche prüfen ohne Datenbank](#oberflaeche-pruefen-ohne-datenbank). diff --git a/docs/09-quellcode.md b/docs/09-quellcode.md index 4bf0332..a4f14a5 100644 --- a/docs/09-quellcode.md +++ b/docs/09-quellcode.md @@ -74,6 +74,14 @@ deaktivierter oder gelöschter Benutzer ist sofort draußen und nicht erst, wenn sein Token abläuft. `secret()` wirft, wenn `JWT_SECRET` fehlt oder kürzer als 16 Zeichen ist - lieber ein harter Startfehler als eine schwache Signatur. +Die Spaltenliste ist ausgeschrieben (kein `SELECT *`), damit das Passwort-Hash +nicht am Request hängt. Sie muss dafür alles enthalten, was `publicUser()` +ausliefert. Fehlte dort `theme` bzw. `theme_mode`, antwortete +`GET /api/auth/me` mit `null`, und die Oberfläche fiel beim Neuladen auf das +Standarddesign zurück - obwohl die Wahl in der Datenbank stand. +`server/test/sessionUser.test.mjs` vergleicht beide Listen, damit ein neues Feld +nicht wieder still verloren geht. + ### `server/src/userFields.js` `normalizeColor` und `normalizeAvatar`, benutzt von `routes/auth.js` und diff --git a/server/package.json b/server/package.json index 5041f1c..2895190 100644 --- a/server/package.json +++ b/server/package.json @@ -12,7 +12,7 @@ "start": "node src/server.js", "dev": "node --watch src/server.js", "migrate": "node src/migrate.js", - "test": "node test/mail.test.mjs && node test/acquisition.test.mjs && node test/userFields.test.mjs && node test/linkedinPhoto.test.mjs" + "test": "node test/mail.test.mjs && node test/acquisition.test.mjs && node test/userFields.test.mjs && node test/linkedinPhoto.test.mjs && node test/sessionUser.test.mjs" }, "dependencies": { "bcryptjs": "^2.4.3", diff --git a/server/src/middleware/auth.js b/server/src/middleware/auth.js index ff5d904..99f2353 100644 --- a/server/src/middleware/auth.js +++ b/server/src/middleware/auth.js @@ -15,7 +15,15 @@ export function signToken(user) { }); } -/** Prueft das Bearer-Token und laedt den aktuellen Benutzer aus der DB. */ +/** + * Prueft das Bearer-Token und laedt den aktuellen Benutzer aus der DB. + * + * Die Spaltenliste ist bewusst vollstaendig und kein `SELECT *`: das + * Passwort-Hash soll nicht am Request haengen. Sie muss aber alles enthalten, + * was publicUser() ausliefert - theme und theme_mode gehoeren dazu, sonst + * antwortet GET /api/auth/me mit null und das Frontend haelt die gespeicherte + * Wahl beim Neuladen fuer "nichts gewaehlt". + */ export async function requireAuth(req, res, next) { const header = req.headers.authorization || ''; const token = header.startsWith('Bearer ') ? header.slice(7) : null; @@ -30,7 +38,8 @@ export async function requireAuth(req, res, next) { try { const [rows] = await pool.query( - `SELECT id, username, email, name, role, color, avatar, is_active, created_at, updated_at + `SELECT id, username, email, name, role, color, avatar, theme, theme_mode, + is_active, created_at, updated_at FROM crm_users WHERE id = ? LIMIT 1`, [payload.sub] ); diff --git a/server/test/sessionUser.test.mjs b/server/test/sessionUser.test.mjs new file mode 100644 index 0000000..c85d854 --- /dev/null +++ b/server/test/sessionUser.test.mjs @@ -0,0 +1,58 @@ +/* + * Prueft, dass die Sitzung alle Felder mitbringt, die die API ausliefert. + * + * requireAuth() laedt den Benutzer mit einer ausgeschriebenen Spaltenliste + * (kein SELECT *, damit das Passwort-Hash nicht am Request haengt). publicUser() + * liest daraus. Fehlt dort eine Spalte, antwortet die API stillschweigend mit + * null - genau so ist die gespeicherte Designwahl beim Neuladen verloren + * gegangen: GET /api/auth/me lieferte theme = null, das Frontend hielt das fuer + * "nichts gewaehlt" und ist auf den Standard zurueckgefallen. + */ +import { readFileSync } from 'node:fs'; + +let failures = 0; + +function check(name, condition, detail = '') { + if (condition) { + console.log(` ok ${name}${detail ? ` - ${detail}` : ''}`); + } else { + console.log(` FAIL ${name}${detail ? ` - ${detail}` : ''}`); + failures += 1; + } +} + +const middleware = readFileSync(new URL('../src/middleware/auth.js', import.meta.url), 'utf8'); +const routes = readFileSync(new URL('../src/routes/auth.js', import.meta.url), 'utf8'); + +console.log('\nSitzungsbenutzer'); + +const select = /`SELECT id,([\s\S]*?)FROM crm_users WHERE id = \?/.exec(middleware); +check('Spaltenliste in requireAuth gefunden', Boolean(select)); + +const publicUser = /function publicUser\(row\) \{([\s\S]*?)\n\}/.exec(routes); +check('publicUser in routes/auth.js gefunden', Boolean(publicUser)); + +if (select && publicUser) { + const columns = [ + 'id', + ...select[1] + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean), + ]; + const used = [...new Set([...publicUser[1].matchAll(/row\.([a-z_]+)/g)].map((m) => m[1]))]; + const missing = used.filter((field) => !columns.includes(field)); + + check( + 'requireAuth laedt jede Spalte, die publicUser ausliefert', + missing.length === 0, + missing.length ? `fehlt: ${missing.join(', ')}` : `${used.length} Felder geprueft` + ); + check('theme und theme_mode sind dabei', columns.includes('theme') && columns.includes('theme_mode')); + check('Das Passwort-Hash bleibt draussen', !columns.includes('password_hash')); +} + +console.log( + failures === 0 ? '\nAlle Pruefungen bestanden.' : `\n${failures} Pruefung(en) fehlgeschlagen.` +); +process.exit(failures === 0 ? 0 : 1);