API: Zugriff strikt auf den Besitzer des API-Keys, Key generieren/anzeigen
Der X-API-Key identifiziert den Benutzer; alle Endpunkte waren bereits auf dessen user_id gescoped (27 Operationen geprueft). Zwei Luecken blieben: - Die Aufloesung Token -> Benutzer nahm bei mehrdeutigem Token per LIMIT 1 einfach den ersten Treffer. Haetten zwei Benutzer denselben Token, saehe der eine die Daten des anderen. Jetzt: fail closed (401 + Logeintrag), und /einstellungen weist einen bereits vergebenen Token mit 409 ab. - Ein leerer Token galt als Wert: wer seine Einstellungen einmal gespeichert hatte, besass eine API_TOKEN-Zeile mit ''. Leere Werte matchen jetzt nie. Neu in den Einstellungen (Abschnitt REST-API): - "Neu generieren" erzeugt einen zufaelligen Token (32 Byte, crypto.get- RandomValues); er wird nur ins Feld gefuellt und erst beim Speichern aktiv, ein Fehlklick laesst sich also verwerfen. - "Kopieren" legt den Token in die Zwischenablage; das Auge blendet ihn ein (bestand bereits fuer Secret-Felder). API aktualisiert: - PUT /settings kannte nur name/adresse/kundennummer, GET lieferte aber alle acht Felder. Jetzt schreibt PUT alle (email, telefon, ort, webseite, geburtsdatum) und aendert nur die im Body uebergebenen Felder; die Antwort enthaelt den neuen Stand. Swagger: - Beschreibung sagte "konfiguriert via Umgebungsvariable API_TOKEN" - das gilt seit der Multi-User-Umstellung nicht mehr. Jetzt dokumentiert: Token pro Benutzer aus den Einstellungen, Zugriff nur auf eigene Daten, fremde id -> 404, unbekannter/leerer/mehrdeutiger Token -> 401. - Settings-Schema um die fehlenden fuenf Felder ergaenzt. Verifiziert mit zwei Benutzern und je eigenem Key: Lesen, Aendern, Loeschen, Timeline, Anhaenge, E-Mails, Generierung, Jobangebote und Blacklist des jeweils anderen liefern durchgaengig 404; Listen, Export und Statistik zeigen nur eigene Daten; kollidierender Token -> 409; mehrdeutiger Token in der DB -> 401 fuer beide. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+30
-12
@@ -64,18 +64,25 @@ function createExternalApi(deps) {
|
||||
}
|
||||
try {
|
||||
// Resolve the token to its owning user (the user whose cfg:API_TOKEN
|
||||
// matches). Two users could in principle share a value — we take the
|
||||
// first match, which is fine since the data the caller then sees is that
|
||||
// one user's only.
|
||||
const user = await dbGet(
|
||||
// matches). An empty token is never a valid key — otherwise every user who
|
||||
// has saved their settings once (which stores API_TOKEN as '') would be
|
||||
// matched by an empty header.
|
||||
const matches = await dbAll(
|
||||
`SELECT u.id, u.username, u.is_admin
|
||||
FROM app_state a JOIN users u ON u.id = a.user_id
|
||||
WHERE a.key = ? AND a.value = ? LIMIT 1`,
|
||||
WHERE a.key = ? AND a.value = ? AND a.value != ''`,
|
||||
[CONFIG_PREFIX + 'API_TOKEN', provided]
|
||||
);
|
||||
if (!user) {
|
||||
// Fail closed on an ambiguous token: if two users somehow share a value we
|
||||
// must not silently pick one of them and hand the caller that user's data.
|
||||
// (Saving a token that another user already uses is rejected in the UI.)
|
||||
if (matches.length !== 1) {
|
||||
if (matches.length > 1) {
|
||||
console.error('API auth: Token ist mehreren Benutzern zugeordnet — Zugriff verweigert.');
|
||||
}
|
||||
return res.status(401).json({ error: 'Ungültiger oder fehlender API-Key (Header: X-API-Key).' });
|
||||
}
|
||||
const user = matches[0];
|
||||
req.user = user;
|
||||
// Warm this user's cfg rows: config.get() is synchronous and reads from
|
||||
// the per-user cache, so without this an API request could see the user
|
||||
@@ -494,19 +501,30 @@ function createExternalApi(deps) {
|
||||
}
|
||||
});
|
||||
|
||||
// Personal details, same set of fields the web UI writes (Persönliche Angaben).
|
||||
// Only the keys present in the body are changed; omitted keys keep their value,
|
||||
// so a client can patch a single field without wiping the rest.
|
||||
const SETTINGS_FIELDS = ['name', 'adresse', 'kundennummer', 'email', 'telefon', 'ort', 'webseite', 'geburtsdatum'];
|
||||
|
||||
router.put('/settings', async (req, res) => {
|
||||
try {
|
||||
const { name, adresse, kundennummer } = req.body || {};
|
||||
const b = req.body || {};
|
||||
const keys = SETTINGS_FIELDS.filter((k) => typeof b[k] !== 'undefined');
|
||||
if (!keys.length) {
|
||||
return res.status(400).json({ error: `Mindestens eines dieser Felder erforderlich: ${SETTINGS_FIELDS.join(', ')}` });
|
||||
}
|
||||
// Upsert, not UPDATE: a user without a settings row would otherwise match
|
||||
// zero rows and the write would be silently dropped.
|
||||
const werte = keys.map((k) => sanitizeInput(String(b[k] ?? '')));
|
||||
await dbRun(
|
||||
`INSERT INTO settings (user_id, name, adresse, kundennummer)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`INSERT INTO settings (user_id, ${keys.join(', ')})
|
||||
VALUES (?, ${keys.map(() => '?').join(', ')})
|
||||
ON CONFLICT(user_id) DO UPDATE SET
|
||||
name = excluded.name, adresse = excluded.adresse, kundennummer = excluded.kundennummer`,
|
||||
[uid(), sanitizeInput(name), sanitizeInput(adresse), sanitizeInput(kundennummer)]
|
||||
${keys.map((k) => `${k} = excluded.${k}`).join(', ')}`,
|
||||
[uid(), ...werte]
|
||||
);
|
||||
res.json({ success: true });
|
||||
const row = await dbGet('SELECT * FROM settings WHERE user_id = ?', [uid()]);
|
||||
res.json({ success: true, settings: row || {} });
|
||||
} catch (error) {
|
||||
console.error('API save settings error:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
|
||||
Reference in New Issue
Block a user