119 lines
9.3 KiB
PHP
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'; ?>
|