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:
@@ -68,6 +68,25 @@
|
||||
</button>
|
||||
<% } %>
|
||||
</div>
|
||||
<% if (f.generate) { %>
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<button type="button" data-generate="cfg-<%= f.key %>"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
Neu generieren
|
||||
</button>
|
||||
<button type="button" data-copy="cfg-<%= f.key %>"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
Kopieren
|
||||
</button>
|
||||
<span data-hint="cfg-<%= f.key %>" class="text-xs text-amber-600 dark:text-amber-400 hidden"></span>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if (f.help) { %>
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1"><%= f.help %></p>
|
||||
<% } %>
|
||||
@@ -106,6 +125,51 @@
|
||||
if (input) input.type = input.type === 'password' ? 'text' : 'password';
|
||||
});
|
||||
});
|
||||
|
||||
function hint(id, text, tone) {
|
||||
const el = document.querySelector('[data-hint="' + id + '"]');
|
||||
if (!el) return;
|
||||
el.textContent = text;
|
||||
el.classList.remove('hidden');
|
||||
el.classList.toggle('text-amber-600', tone !== 'ok');
|
||||
el.classList.toggle('dark:text-amber-400', tone !== 'ok');
|
||||
el.classList.toggle('text-green-600', tone === 'ok');
|
||||
el.classList.toggle('dark:text-green-400', tone === 'ok');
|
||||
}
|
||||
|
||||
// Generate a fresh random token (32 bytes hex, from the browser CSPRNG).
|
||||
// It only fills the field — nothing changes until the form is saved,
|
||||
// so an accidental click can still be discarded.
|
||||
document.querySelectorAll('[data-generate]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
const id = btn.getAttribute('data-generate');
|
||||
const input = document.getElementById(id);
|
||||
if (!input) return;
|
||||
const bytes = new Uint8Array(32);
|
||||
crypto.getRandomValues(bytes);
|
||||
input.value = Array.from(bytes).map(function (b) {
|
||||
return b.toString(16).padStart(2, '0');
|
||||
}).join('');
|
||||
input.type = 'text'; // reveal, so it can be copied down
|
||||
hint(id, 'Neuer Token erzeugt — zum Aktivieren speichern. Der alte wird damit ungültig.');
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-copy]').forEach(function (btn) {
|
||||
btn.addEventListener('click', async function () {
|
||||
const id = btn.getAttribute('data-copy');
|
||||
const input = document.getElementById(id);
|
||||
if (!input || !input.value) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(input.value);
|
||||
hint(id, 'In die Zwischenablage kopiert.', 'ok');
|
||||
} catch (e) {
|
||||
input.type = 'text';
|
||||
input.select();
|
||||
hint(id, 'Kopieren nicht möglich — Wert ist markiert, bitte manuell kopieren.');
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user