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

@@ -64,6 +64,7 @@ function setupNavTabs() {
document.getElementById(`sec-${section}`).classList.add("active");
if (section === "licenses") loadExpiringLicenses();
if (section === "audit" && typeof loadAudit === "function") loadAudit();
});
});
}
@@ -489,12 +490,16 @@ function setupForms() {
if (licType !== "permanent") {
body.duration_days = parseInt(document.getElementById("newLicDuration").value);
}
const creditsTotal = document.getElementById('newLicCreditsTotal');
const costPerCredit = document.getElementById('newLicCostPerCredit');
const budgetUsd = document.getElementById('newLicBudgetUsd');
if (creditsTotal && creditsTotal.value) body.credits_total = parseInt(creditsTotal.value);
if (costPerCredit && costPerCredit.value) body.cost_per_credit = parseFloat(costPerCredit.value);
if (budgetUsd && budgetUsd.value) body.token_budget_usd = parseFloat(budgetUsd.value);
const unlimitedEl = document.getElementById('newLicUnlimited');
body.unlimited_budget = !!(unlimitedEl && unlimitedEl.checked);
if (!body.unlimited_budget) {
const creditsTotal = document.getElementById('newLicCreditsTotal');
const costPerCredit = document.getElementById('newLicCostPerCredit');
const budgetUsd = document.getElementById('newLicBudgetUsd');
if (creditsTotal && creditsTotal.value) body.credits_total = parseInt(creditsTotal.value);
if (costPerCredit && costPerCredit.value) body.cost_per_credit = parseFloat(costPerCredit.value);
if (budgetUsd && budgetUsd.value) body.token_budget_usd = parseFloat(budgetUsd.value);
}
try {
await API.post("/api/licenses", body);
closeModal("modalNewLicense");
@@ -599,11 +604,17 @@ async function loadOrgTokenUsage(orgId) {
const budget = current.budget || {};
const usage = current.usage || {};
const unlimited = !!budget.unlimited_budget;
const el = (id, val) => { const e = document.getElementById(id); if (e) e.textContent = val; };
el('tokenCreditsUsed', budget.credits_used != null ? Math.round(budget.credits_used).toLocaleString('de-DE') : '-');
el('tokenCreditsRemaining', budget.credits_remaining != null ? budget.credits_remaining.toLocaleString('de-DE') : '-');
el('tokenBudgetUsd', budget.token_budget_usd != null ? '$' + Number(budget.token_budget_usd).toFixed(2) : '-');
if (unlimited) {
el('tokenCreditsRemaining', '∞ Unbegrenzt');
el('tokenBudgetUsd', '—');
} else {
el('tokenCreditsRemaining', budget.credits_remaining != null ? budget.credits_remaining.toLocaleString('de-DE') : '-');
el('tokenBudgetUsd', budget.token_budget_usd != null ? '$' + Number(budget.token_budget_usd).toFixed(2) : '-');
}
el('tokenCostUsd', usage.total_cost_usd != null ? '$' + Number(usage.total_cost_usd).toFixed(2) : '-');
// Source-Split anzeigen
@@ -616,14 +627,29 @@ async function loadOrgTokenUsage(orgId) {
const bar = document.getElementById('tokenBudgetBar');
const percentEl = document.getElementById('tokenBudgetPercent');
const percent = budget.credits_percent_used || 0;
if (bar) {
bar.style.width = Math.min(100, percent) + '%';
bar.classList.remove('warning', 'critical');
if (percent > 80) bar.classList.add('critical');
else if (percent > 50) bar.classList.add('warning');
const barWrap = bar ? bar.parentElement : null;
if (unlimited) {
if (bar) bar.style.width = '0%';
if (percentEl) percentEl.textContent = 'Unbegrenzt';
if (barWrap) barWrap.style.opacity = '0.4';
} else {
const percent = budget.credits_percent_used || 0;
if (bar) {
bar.style.width = Math.min(100, percent) + '%';
bar.classList.remove('warning', 'critical', 'over-limit');
if (percent > 100) bar.classList.add('over-limit');
else if (percent > 80) bar.classList.add('critical');
else if (percent > 50) bar.classList.add('warning');
}
if (percentEl) {
percentEl.textContent = percent > 100
? ('UEBER LIMIT (' + percent.toFixed(1) + '%)')
: (percent.toFixed(1) + '%');
percentEl.style.color = percent > 100 ? 'var(--danger-text, #991b1b)' : '';
percentEl.style.fontWeight = percent > 100 ? '700' : '';
}
if (barWrap) barWrap.style.opacity = '1';
}
if (percentEl) percentEl.textContent = percent.toFixed(1) + '%';
fillBudgetForm(budget);
@@ -655,6 +681,37 @@ function fillBudgetForm(budget) {
el('editCostPerCredit', budget.cost_per_credit);
el('editBudgetUsd', budget.token_budget_usd);
el('editCreditsUsed', budget.credits_used ? Math.round(budget.credits_used) : 0);
const cb = document.getElementById('editUnlimitedBudget');
if (cb) {
cb.checked = !!budget.unlimited_budget;
onUnlimitedToggle();
}
}
// Unlimited-Toggle: Felder ausgrauen wenn aktiv
function onUnlimitedToggle() {
const cb = document.getElementById('editUnlimitedBudget');
const isUnlimited = cb && cb.checked;
['editCreditsTotal','editCostPerCredit','editBudgetUsd'].forEach(function(id) {
const e = document.getElementById(id);
if (e) {
e.disabled = isUnlimited;
e.style.opacity = isUnlimited ? '0.4' : '1';
}
});
}
// Unlimited-Toggle im Lizenz-Modal
function onNewLicUnlimitedToggle() {
const cb = document.getElementById('newLicUnlimited');
const isUnlimited = cb && cb.checked;
['newLicCreditsTotal','newLicCostPerCredit','newLicBudgetUsd'].forEach(function(id) {
const e = document.getElementById(id);
if (e) {
e.disabled = isUnlimited;
e.style.opacity = isUnlimited ? '0.4' : '1';
}
});
}
async function loadDashboardTokenStats() {
@@ -701,19 +758,34 @@ document.addEventListener('DOMContentLoaded', function() {
}
var body = {};
var unlimitedCb = document.getElementById('editUnlimitedBudget');
var isUnlimited = !!(unlimitedCb && unlimitedCb.checked);
body.unlimited_budget = isUnlimited;
var creditsTotal = document.getElementById('editCreditsTotal');
var costPerCredit = document.getElementById('editCostPerCredit');
var budgetUsd = document.getElementById('editBudgetUsd');
var creditsUsed = document.getElementById('editCreditsUsed');
if (creditsTotal && creditsTotal.value) body.credits_total = parseInt(creditsTotal.value);
if (costPerCredit && costPerCredit.value) body.cost_per_credit = parseFloat(costPerCredit.value);
if (budgetUsd && budgetUsd.value) body.token_budget_usd = parseFloat(budgetUsd.value);
if (!isUnlimited) {
if (creditsTotal && creditsTotal.value) body.credits_total = parseInt(creditsTotal.value);
if (costPerCredit && costPerCredit.value) body.cost_per_credit = parseFloat(costPerCredit.value);
if (budgetUsd && budgetUsd.value) body.token_budget_usd = parseFloat(budgetUsd.value);
}
// credits_used immer mitsenden (auch im Unlimited-Modus, fuer Korrekturen)
if (creditsUsed && creditsUsed.value !== '') body.credits_used = parseFloat(creditsUsed.value);
await API.put('/api/token-usage/budget/' + activeLic.id, body);
if (msgEl) msgEl.textContent = 'Gespeichert!';
setTimeout(function() { if (msgEl) msgEl.textContent = ''; }, 3000);
var result = await API.put('/api/token-usage/budget/' + activeLic.id, body);
if (msgEl) {
if (result && result.warning) {
msgEl.textContent = result.warning;
msgEl.style.color = 'var(--danger-text, #991b1b)';
} else {
msgEl.textContent = 'Gespeichert!';
msgEl.style.color = '';
}
}
setTimeout(function() { if (msgEl) { msgEl.textContent = ''; msgEl.style.color = ''; } }, 5000);
// Daten neu laden
loadOrgTokenUsage(currentOrgId);