Audit-Log + Brute-Force-Schutz + unlimited_budget + User-Delete-Fix

- Schema-Migration: ON DELETE SET NULL fuer incidents.created_by, magic_links.user_id,
  network_analyses.created_by (behebt 500er beim User-Loeschen). Neue Spalte
  licenses.unlimited_budget. Neue Tabellen portal_audit_log, portal_login_attempts.
- Audit-Log: alle CREATE/UPDATE/DELETE auf Org/User/Lizenz/Quelle + Login-Events
  werden mit before/after-Diff in portal_audit_log geschrieben.
- Brute-Force-Schutz: 5 Fehlversuche pro IP+Username/15min -> 429 mit Retry-After.
- Token-Budget: expliziter Schalter unlimited_budget pro Lizenz. UI zeigt ehrlich
  >100%-Verbrauch (kein Math.min mehr) und ungebremste Anzeige bei unlimited.
- Neuer Audit-Log Tab mit Filter (Aktion/Ressource/Admin/Zeitraum) und Pagination.
Dieser Commit ist enthalten in:
claude-dev
2026-05-02 20:16:03 +00:00
Ursprung 0da66fb585
Commit 4dc372814d
15 geänderte Dateien mit 1215 neuen und 151 gelöschten Zeilen

Datei anzeigen

@@ -31,6 +31,7 @@
<button class="nav-tab" data-section="orgs">Organisationen</button>
<button class="nav-tab" data-section="licenses">Lizenzen</button>
<button class="nav-tab" data-section="sources">Quellen</button>
<button class="nav-tab" data-section="audit">Audit-Log</button>
</nav>
<!-- Dashboard Section -->
@@ -224,6 +225,12 @@
<div class="card" style="margin-top:12px;">
<div class="card-body">
<form id="tokenBudgetForm" style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
<div style="grid-column:1/-1; padding:8px 12px; background:var(--bg-tertiary,#0f172a); border-radius:6px; border:1px solid var(--border);">
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; margin:0;">
<input type="checkbox" id="editUnlimitedBudget" onchange="onUnlimitedToggle()">
<span><strong>Unbegrenztes Budget</strong> &mdash; Verbrauch wird trotzdem getrackt, aber kein Limit/Hard-Stop</span>
</label>
</div>
<div class="form-group">
<label for="editCreditsTotal">Credits-Kontingent</label>
<input type="number" id="editCreditsTotal" placeholder="z.B. 600000">
@@ -392,6 +399,48 @@
</div>
</div>
</div>
<!-- Audit-Log Section -->
<div class="section" id="sec-audit">
<div class="action-bar" style="flex-wrap:wrap;gap:8px;">
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
<select class="filter-select" id="auditFilterAction">
<option value="">Alle Aktionen</option>
</select>
<select class="filter-select" id="auditFilterResource">
<option value="">Alle Ressourcen</option>
</select>
<select class="filter-select" id="auditFilterAdmin">
<option value="">Alle Admins</option>
</select>
<input type="date" class="filter-select" id="auditFilterFrom" title="Von (Datum)">
<input type="date" class="filter-select" id="auditFilterTo" title="Bis (Datum)">
<button class="btn btn-secondary btn-small" id="auditFilterReset">Filter zuruecksetzen</button>
<span class="text-secondary" id="auditCount"></span>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<button class="btn btn-secondary btn-small" id="auditPrevBtn" disabled>&larr; Zurueck</button>
<button class="btn btn-secondary btn-small" id="auditNextBtn" disabled>Weiter &rarr;</button>
</div>
</div>
<div class="card">
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:170px;">Zeitpunkt</th>
<th style="width:120px;">Admin</th>
<th style="width:140px;">IP</th>
<th style="width:140px;">Aktion</th>
<th>Ressource</th>
<th style="width:50px;"></th>
</tr>
</thead>
<tbody id="auditTable"><tr><td colspan="6" class="text-muted">Lade...</td></tr></tbody>
</table>
</div>
</div>
</div>
</main>
<!-- Modal: New Organization -->
@@ -478,6 +527,12 @@
<div class="text-muted mt-8" style="font-size: 12px;">Trial: Standard 14 Tage, Jahreslizenz: Standard 365 Tage</div>
</div>
<div class="form-group" style="background:var(--bg-tertiary,#0f172a); padding:8px 12px; border-radius:6px; border:1px solid var(--border);">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;margin:0;">
<input type="checkbox" id="newLicUnlimited" onchange="onNewLicUnlimitedToggle()">
<span><strong>Unbegrenztes Budget</strong> &mdash; getrackt, aber kein Hard-Stop</span>
</label>
</div>
<div class="form-group">
<label for="newLicCreditsTotal">Credits-Kontingent</label>
<input type="number" id="newLicCreditsTotal" placeholder="z.B. 600000">
@@ -626,5 +681,6 @@
<script src="/static/js/app.js"></script>
<script src="/static/js/sources.js"></script>
<script src="/static/js/source-health.js"></script>
<script src="/static/js/audit.js"></script>
</body>
</html>