Security Hardening: Proaktive Kontosperren, Fail2Ban-Integration, SecurityLogger, Alerting und API-Schutz

This commit is contained in:
Deploymentcenter Bot
2026-09-08 11:45:04 +02:00
parent 087f4392a9
commit edea46e342
17 changed files with 927 additions and 169 deletions
+50 -5
View File
@@ -1613,6 +1613,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
dc_redirect('#sub-system-users');
}
case 'unlock_user': {
$userId = (int)($_POST['user_id'] ?? 0);
if (Auth::unlockAccount($pdo, $userId)) {
dc_flash('Benutzerkonto wurde erfolgreich entsperrt.');
} else {
dc_flash('Konto konnte nicht entsperrt werden oder existiert nicht.', 'danger');
}
dc_redirect('#sub-system-users');
}
default:
dc_flash('Unbekannte Aktion.', 'danger');
dc_redirect();
@@ -1869,12 +1879,19 @@ $releases = $updateManager->getReleases();
// mitgelesen - er hat in der Ansicht nichts verloren.
try {
$users = $pdo->query('
SELECT id, username, role, disabled, last_login_at, created_at
SELECT id, username, role, disabled, failed_login_count, locked_until, last_login_at, created_at
FROM dc_users ORDER BY role ASC, username ASC
')->fetchAll() ?: [];
} catch (Throwable $e) {
// Vor Migration 012 fehlen role, disabled und last_login_at.
$users = $pdo->query('SELECT id, username, created_at FROM dc_users ORDER BY username ASC')->fetchAll() ?: [];
try {
$users = $pdo->query('
SELECT id, username, role, disabled, last_login_at, created_at
FROM dc_users ORDER BY role ASC, username ASC
')->fetchAll() ?: [];
} catch (Throwable $e2) {
// Vor Migration 012 fehlen role, disabled und last_login_at.
$users = $pdo->query('SELECT id, username, created_at FROM dc_users ORDER BY username ASC')->fetchAll() ?: [];
}
}
$usersSchemaReady = $users === [] || array_key_exists('role', $users[0]);
@@ -6285,8 +6302,11 @@ Antwort:
$uRole = (string)($u['role'] ?? 'admin');
$uDisabled = (int)($u['disabled'] ?? 0) === 1;
$isSelf = (int)$u['id'] === Auth::userId();
$isLocked = !empty($u['locked_until']) && strtotime((string)$u['locked_until'] . ' UTC') > time();
$lockMins = $isLocked ? (int)ceil((strtotime((string)$u['locked_until'] . ' UTC') - time()) / 60) : 0;
$failCount = (int)($u['failed_login_count'] ?? 0);
?>
<tr<?= $uDisabled ? ' style="opacity:0.55;"' : '' ?>>
<tr<?= $uDisabled ? ' style="opacity:0.55;"' : ($isLocked ? ' style="background:rgba(239,68,68,0.08);"' : '') ?>>
<td>
<strong><?= e((string)$u['username']) ?></strong>
<?php if ($isSelf): ?><span class="badge badge-up">Sie</span><?php endif; ?>
@@ -6296,13 +6316,38 @@ Antwort:
<?= e(strtoupper($uRole)) ?>
</span>
</td>
<td><?= $uDisabled ? '<span class="badge badge-down">DEAKTIVIERT</span>' : 'aktiv' ?></td>
<td>
<?php if ($isLocked): ?>
<span class="badge badge-down">GESPERRT (noch <?= $lockMins ?>m)</span>
<?php elseif ($uDisabled): ?>
<span class="badge badge-down">DEAKTIVIERT</span>
<?php else: ?>
<span style="color:var(--text-main);">aktiv</span>
<?php endif; ?>
<?php if ($failCount > 0 && !$isLocked): ?>
<div style="font-size:0.75rem; color:var(--text-muted); margin-top:2px;">
<?= $failCount ?> Fehlversuch(e)
</div>
<?php endif; ?>
</td>
<td style="font-size:0.8rem; color:var(--text-muted);">
<?= !empty($u['last_login_at']) ? e((string)$u['last_login_at']) : 'nie' ?>
</td>
<td style="font-size:0.8rem; color:var(--text-muted);"><?= e((string)($u['created_at'] ?? '-')) ?></td>
<td>
<div style="display:flex; flex-wrap:wrap; gap:0.35rem;">
<?php if ($isLocked): ?>
<form method="POST" action="index.php" style="display:inline;">
<?= $csrfField ?>
<input type="hidden" name="action" value="unlock_user">
<input type="hidden" name="_return" value="sub-system-users">
<input type="hidden" name="user_id" value="<?= (int)$u['id'] ?>">
<button type="submit" class="btn btn-sm btn-primary">
🔓 Entsperren
</button>
</form>
<?php endif; ?>
<form method="POST" action="index.php" style="display:inline-flex; gap:0.25rem;"
onsubmit="return confirm('Passwort für <?= e((string)$u['username']) ?> wirklich ändern?');">
<?= $csrfField ?>