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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
71a8c30b23
commit
a50f5cb4a6
@@ -53,7 +53,7 @@ ansehen - siehe *Oberfläche prüfen ohne Datenbank* weiter unten.
|
|||||||
```bash
|
```bash
|
||||||
npm run lint # = tsc --noEmit, das ist die Wahrheit
|
npm run lint # = tsc --noEmit, das ist die Wahrheit
|
||||||
npm run build # muss ohne Fehler durchlaufen
|
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
|
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
|
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
|
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.
|
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
|
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).
|
mit dem Stub aus [Oberfläche prüfen ohne Datenbank](#oberflaeche-pruefen-ohne-datenbank).
|
||||||
|
|||||||
@@ -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
|
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.
|
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`
|
### `server/src/userFields.js`
|
||||||
|
|
||||||
`normalizeColor` und `normalizeAvatar`, benutzt von `routes/auth.js` und
|
`normalizeColor` und `normalizeAvatar`, benutzt von `routes/auth.js` und
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
"start": "node src/server.js",
|
"start": "node src/server.js",
|
||||||
"dev": "node --watch src/server.js",
|
"dev": "node --watch src/server.js",
|
||||||
"migrate": "node src/migrate.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": {
|
"dependencies": {
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
|||||||
@@ -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) {
|
export async function requireAuth(req, res, next) {
|
||||||
const header = req.headers.authorization || '';
|
const header = req.headers.authorization || '';
|
||||||
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
|
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
|
||||||
@@ -30,7 +38,8 @@ export async function requireAuth(req, res, next) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query(
|
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`,
|
FROM crm_users WHERE id = ? LIMIT 1`,
|
||||||
[payload.sub]
|
[payload.sub]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
Reference in New Issue
Block a user