Files
deskhub/DeskHub-Webseite/staff/login_log.php
T
2026-10-30 21:50:04 +00:00

119 lines
9.3 KiB
PHP

<?php
require_once __DIR__ . '/../includes/bootstrap.php';
$admin = require_admin();
$pdo = get_db();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$action = (string) ($_POST['action'] ?? '');
if ($action === 'save') {
set_setting('login_guard', !empty($_POST['login_guard']) ? '1' : '0');
set_setting('login_guard_warn_place', !empty($_POST['login_guard_warn_place']) ? '1' : '0');
set_setting('login_guard_notify_admins', !empty($_POST['login_guard_notify_admins']) ? '1' : '0');
set_setting('login_guard_max_attempts', (string) max(3, min(20, (int) ($_POST['login_guard_max_attempts'] ?? 5))));
set_setting('login_guard_lock_minutes', (string) max(5, min(240, (int) ($_POST['login_guard_lock_minutes'] ?? 15))));
set_setting('login_guard_retention_days', (string) max(7, min(1095, (int) ($_POST['login_guard_retention_days'] ?? 180))));
audit_log($admin['id'], 'Anmelde-Wächter: Einstellungen geändert');
flash('success', 'Einstellungen gespeichert.');
} elseif ($action === 'purge') {
$n = $pdo->exec('DELETE FROM login_events');
audit_log($admin['id'], 'Anmelde-Wächter: Protokoll geleert (' . (int) $n . ' Einträge)');
flash('success', 'Protokoll geleert.');
} elseif ($action === 'forget_devices') {
$sid = (int) ($_POST['staff_id'] ?? 0);
if ($sid > 0) {
$pdo->prepare('DELETE FROM login_devices WHERE staff_id = ?')->execute([$sid]);
$pdo->prepare('DELETE FROM login_places WHERE staff_id = ?')->execute([$sid]);
audit_log($admin['id'], 'Anmelde-Wächter: gemerkte Geräte und Orte vergessen (Mitarbeiter ' . $sid . ')');
flash('success', 'Geräte und Orte vergessen. Die nächste Anmeldung zählt wieder als erste.');
}
}
redirect('/staff/login_log.php');
}
$labels = login_guard_event_labels();
$filter = (string) ($_GET['event'] ?? '');
if (!isset($labels[$filter])) $filter = '';
$q = trim((string) ($_GET['q'] ?? ''));
$where = ['1=1'];
$params = [];
if ($filter !== '') { $where[] = 'e.event = ?'; $params[] = $filter; }
if ($q !== '') { $where[] = '(e.email LIKE ? OR e.ip LIKE ? OR e.device LIKE ? OR e.place LIKE ?)'; $like = '%' . $q . '%'; array_push($params, $like, $like, $like, $like); }
$st = $pdo->prepare('SELECT e.*, s.name AS staff_name FROM login_events e LEFT JOIN staff s ON s.id = e.staff_id WHERE ' . implode(' AND ', $where) . ' ORDER BY e.id DESC LIMIT 300');
$st->execute($params);
$rows = $st->fetchAll();
$since = date('Y-m-d H:i:s', time() - 86400);
$cnt = $pdo->prepare('SELECT event, COUNT(*) AS n FROM login_events WHERE created_at >= ? GROUP BY event');
$cnt->execute([$since]);
$counts = array_fill_keys(array_keys($labels), 0);
foreach ($cnt->fetchAll() as $r) $counts[$r['event']] = (int) $r['n'];
$pageTitle = 'Anmelde-Wächter';
$activePage = 'login_log';
$colors = ['success' => '#15803d', 'new_device' => '#b45309', 'new_place' => '#b45309', 'failed' => '#b91c1c', 'locked' => '#b91c1c', 'revoked' => '#7c3aed'];
require __DIR__ . '/_header.php';
?>
<h1>Anmelde-Wächter</h1>
<p class="muted" style="max-width:860px">Der Wächter merkt sich die Geräte und Orte, von denen sich Mitarbeiter anmelden (Web und App). Meldet sich jemand von einem <strong>neuen Gerät</strong> oder <strong>neuen Ort</strong> an, bekommt er eine Mail mit dem Link „Das war ich nicht“. Der Link macht das Passwort ungültig, beendet App-Anmeldungen und Browser-Sitzungen und informiert die Administratoren. Nach zu vielen <strong>Fehlversuchen</strong> wird die Anmeldung gesperrt; auch das steht im Protokoll und geht per Mail an Kontoinhaber und Administratoren. Als Ort gilt das Land (wenn Cloudflare es mitschickt), sonst das Netz der IP-Adresse; die erste Anmeldung eines Kontos wird still gemerkt.</p>
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:16px">
<?php foreach (['success', 'new_device', 'new_place', 'failed', 'locked', 'revoked'] as $k): ?>
<div class="card" style="margin:0;min-width:140px"><div class="muted" style="font-size:.8rem"><?= e($labels[$k]) ?> (24 Std.)</div><div style="font-size:1.6rem;font-weight:800;color:<?= $colors[$k] ?>"><?= (int) $counts[$k] ?></div></div>
<?php endforeach; ?>
</div>
<form method="post" class="card" style="max-width:860px;margin-bottom:16px"><?= csrf_field() ?><input type="hidden" name="action" value="save">
<h3 style="margin-top:0">Einstellungen</h3>
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:400"><input type="checkbox" name="login_guard" value="1" style="width:auto;margin-top:3px" <?= login_guard_enabled() ? 'checked' : '' ?>>
<span><strong>Anmelde-Wächter einschalten</strong> (Geräte merken, bei Neuem warnen, Protokoll führen)</span></label>
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:400;margin-top:8px"><input type="checkbox" name="login_guard_warn_place" value="1" style="width:auto;margin-top:3px" <?= get_setting('login_guard_warn_place', '1') === '1' ? 'checked' : '' ?>>
<span>Auch bei einem <strong>neuen Ort</strong> warnen (vom bekannten Gerät, z. B. unterwegs mit dem Handy). Ausgeschaltet warnt nur ein neues Gerät.</span></label>
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:400;margin-top:8px"><input type="checkbox" name="login_guard_notify_admins" value="1" style="width:auto;margin-top:3px" <?= get_setting('login_guard_notify_admins', '1') === '1' ? 'checked' : '' ?>>
<span>Administratoren per Mail informieren bei <strong>Sperren</strong> und bei „Konto gesichert“.</span></label>
<div style="display:flex;gap:18px;flex-wrap:wrap;margin-top:12px">
<div><label for="lg_max">Sperre nach … Fehlversuchen</label><input type="number" id="lg_max" name="login_guard_max_attempts" min="3" max="20" value="<?= login_guard_max_attempts() ?>" style="width:120px"></div>
<div><label for="lg_min">Sperre für … Minuten</label><input type="number" id="lg_min" name="login_guard_lock_minutes" min="5" max="240" value="<?= login_guard_lock_minutes() ?>" style="width:120px"></div>
<div><label for="lg_ret">Protokoll aufbewahren (Tage)</label><input type="number" id="lg_ret" name="login_guard_retention_days" min="7" max="1095" value="<?= login_guard_retention_days() ?>" style="width:120px"></div>
</div>
<div class="btn-row"><button class="btn" type="submit">Speichern</button></div>
</form>
<div class="card">
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:10px">
<h3 style="margin:0;flex:1">Protokoll</h3>
<form method="get" style="display:flex;gap:8px;flex-wrap:wrap;margin:0">
<select name="event" style="width:auto"><option value="">Alle Ereignisse</option><?php foreach ($labels as $k => $l): ?><option value="<?= e($k) ?>" <?= $filter === $k ? 'selected' : '' ?>><?= e($l) ?></option><?php endforeach; ?></select>
<input type="search" name="q" value="<?= e($q) ?>" placeholder="E-Mail, IP, Gerät …" style="width:200px">
<button class="btn secondary" type="submit">Filtern</button>
</form>
<form method="post" onsubmit="return confirm('Das ganze Protokoll löschen?')"><?= csrf_field() ?><input type="hidden" name="action" value="purge"><button class="btn secondary" type="submit" <?= $rows ? '' : 'disabled' ?>>Protokoll leeren</button></form>
</div>
<?php if (!$rows): ?>
<p class="muted">Keine Einträge<?= ($filter !== '' || $q !== '') ? ' zu diesem Filter' : '' ?>.</p>
<?php else: ?>
<div style="overflow-x:auto"><table>
<thead><tr><th>Zeit</th><th>Ereignis</th><th>Konto</th><th>Gerät</th><th>Ort / IP</th><th>Hinweis</th><th></th></tr></thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td style="white-space:nowrap"><?= e(date('d.m.Y H:i', strtotime((string) $r['created_at']))) ?></td>
<td><span style="color:<?= $colors[$r['event']] ?? '#475569' ?>;font-weight:700"><?= e($labels[$r['event']] ?? $r['event']) ?></span><div class="muted" style="font-size:.74rem"><?= $r['channel'] === 'app' ? 'App' : 'Web' ?></div></td>
<td><?= $r['staff_name'] ? e($r['staff_name']) . '<div class="muted" style="font-size:.76rem">' . e($r['email']) . '</div>' : e($r['email']) ?></td>
<td style="font-size:.85rem"><?= e($r['device']) ?></td>
<td style="font-size:.85rem"><?= e($r['place']) ?><div class="muted" style="font-size:.76rem"><?= e($r['ip']) ?></div></td>
<td style="font-size:.82rem"><?= e($r['detail']) ?></td>
<td>
<?php if ($r['staff_id'] && in_array($r['event'], ['new_device', 'new_place', 'revoked'], true)): ?>
<form method="post" onsubmit="return confirm('Alle gemerkten Geräte und Orte dieses Mitarbeiters vergessen?')"><?= csrf_field() ?><input type="hidden" name="action" value="forget_devices"><input type="hidden" name="staff_id" value="<?= (int) $r['staff_id'] ?>"><button class="btn secondary" type="submit" style="padding:3px 8px;font-size:.74rem">Geräte vergessen</button></form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table></div>
<p class="muted" style="font-size:.78rem;margin:10px 0 0">Zeigt die letzten 300 Einträge. Einträge werden nach <?= login_guard_retention_days() ?> Tagen automatisch gelöscht.</p>
<?php endif; ?>
</div>
<?php require __DIR__ . '/_footer.php'; ?>