query('SELECT id FROM departments ORDER BY id LIMIT 1')->fetchColumn(); $prio = (int) $pdo->query('SELECT id FROM priorities ORDER BY id LIMIT 1')->fetchColumn(); $pdo->exec('DELETE FROM staff'); $ins = $pdo->prepare('INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES (?,?,?,?,?,1,?)'); $ins->execute(['Chef', 'chef@test.de', 'x', 'admin', $dept, $now]); $chef = (int) $pdo->lastInsertId(); $ins->execute(['Eva', 'eva@test.de', 'x', 'agent', $dept, $now]); $eva = (int) $pdo->lastInsertId(); $ins->execute(['Max', 'max@test.de', 'x', 'agent', $dept, $now]); $max = (int) $pdo->lastInsertId(); $tok = function (int $sid, string $name, string $hash, ?string $revoked = null) use ($pdo, $now): void { $pdo->prepare('INSERT INTO mobile_api_tokens (staff_id, token_hash, device_name, expires_at, last_used_at, revoked_at, created_at) VALUES (?,?,?,?,?,?,?)') ->execute([$sid, $hash, $name, date('Y-m-d H:i:s', time() + 86400), $now, $revoked, $now]); }; $tok($eva, 'Android Pixel', str_repeat('a', 64)); $tok($eva, 'Windows-Programm', str_repeat('b', 64)); $tok($eva, 'Chrome', str_repeat('c', 64)); $tok($max, 'Max Handy', str_repeat('d', 64)); $pdo->prepare('INSERT INTO push_subscriptions (staff_id, endpoint, p256dh, auth) VALUES (?,?,?,?)')->execute([$eva, 'https://push.example/1', 'k', 'a']); $pdo->prepare('INSERT INTO push_subscriptions (staff_id, endpoint, p256dh, auth) VALUES (?,?,?,?)')->execute([$max, 'https://push.example/2', 'k', 'a']); $pdo->prepare('INSERT INTO push_devices (staff_id, token, token_hash, platform, device_name, created_at) VALUES (?,?,?,?,?,?)')->execute([$eva, 'fcm-eva', str_repeat('e', 64), 'android', 'Pixel', $now]); $pdo->prepare('INSERT INTO push_devices (staff_id, token, token_hash, platform, device_name, created_at) VALUES (?,?,?,?,?,?)')->execute([$max, 'fcm-max', str_repeat('f', 64), 'android', 'Max', $now]); $pdo->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, locked_by, locked_at, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)') ->execute(['S-1', 'Gesperrt', 'K', 'k@example.org', $dept, $prio, 'open', 'web', $eva, $now, $now, $now]); $pdo->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, locked_by, locked_at, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)') ->execute(['S-2', 'Von Max gesperrt', 'K', 'k@example.org', $dept, $prio, 'open', 'web', $max, $now, $now, $now]); $check(count(staff_app_logins($pdo, $eva)) === 3 && count(staff_app_logins($pdo, $max)) === 1, 'Vorher: Evas drei App-Anmeldungen sind sichtbar'); $tok($eva, 'Abgelaufen', str_repeat('9', 64), $now); $check(count(staff_app_logins($pdo, $eva)) === 3, 'bereits widerrufene Anmeldungen werden nicht angezeigt'); // Eine Browser-Sitzung, die vor dem Widerruf angemeldet wurde, ist danach ungültig $_SESSION['staff_login_at'] = time() - 600; $check(!login_guard_session_revoked($eva), 'Vorher: Sitzung gültig'); $r = staff_sign_out_everywhere($pdo, $eva, $chef); $check($r['apps'] === 4 && $r['browsers'] === 1 && $r['devices'] === 1 && $r['locks'] === 1, 'Zahlen: ' . json_encode($r)); $check(login_guard_session_revoked($eva), 'Danach: ältere Browser-Sitzung ist ungültig'); $_SESSION['staff_login_at'] = time() + 5; $check(!login_guard_session_revoked($eva), 'eine neue Anmeldung danach ist gültig'); $check((int) $pdo->query("SELECT COUNT(*) FROM mobile_api_tokens WHERE staff_id = {$eva}")->fetchColumn() === 0, 'App-Anmeldungen von Eva gelöscht'); $check((int) $pdo->query("SELECT COUNT(*) FROM push_subscriptions WHERE staff_id = {$eva}")->fetchColumn() === 0 && (int) $pdo->query("SELECT COUNT(*) FROM push_devices WHERE staff_id = {$eva}")->fetchColumn() === 0, 'Push von Eva gelöscht (Browser und App)'); $check($pdo->query("SELECT locked_by FROM tickets WHERE ticket_number = 'S-1'")->fetchColumn() === null, 'Ticket-Sperre von Eva freigegeben'); // Andere bleiben unberührt $check((int) $pdo->query("SELECT COUNT(*) FROM mobile_api_tokens WHERE staff_id = {$max}")->fetchColumn() === 1, 'Max behält seine App-Anmeldung'); $check((int) $pdo->query("SELECT COUNT(*) FROM push_subscriptions WHERE staff_id = {$max}")->fetchColumn() === 1 && (int) $pdo->query("SELECT COUNT(*) FROM push_devices WHERE staff_id = {$max}")->fetchColumn() === 1, 'Max behält sein Push'); $check((int) $pdo->query("SELECT locked_by FROM tickets WHERE ticket_number = 'S-2'")->fetchColumn() === $max, 'Max behält seine Ticket-Sperre'); $_SESSION['staff_login_at'] = time() - 600; $check(!login_guard_session_revoked($max), 'Maxens Browser-Sitzung bleibt gültig'); // Protokoll $ev = $pdo->query("SELECT * FROM login_events WHERE event = 'signout' AND staff_id = {$eva}")->fetch(PDO::FETCH_ASSOC); $check($ev && str_contains((string) $ev['detail'], 'Administrator'), 'Protokolleintrag „Überall abgemeldet“ mit Hinweis auf den Administrator'); $check(isset(login_guard_event_labels()['signout']), 'Ereignisart ist beschriftet'); $check((int) $pdo->query("SELECT COUNT(*) FROM admin_audit_log WHERE action LIKE 'Mitarbeiter #{$eva} von allen Geräten abgemeldet%'")->fetchColumn() === 1, 'Audit-Log-Eintrag'); // Selbst abmelden $r2 = staff_sign_out_everywhere($pdo, $max, $max); $ev2 = $pdo->query("SELECT detail FROM login_events WHERE event = 'signout' AND staff_id = {$max}")->fetchColumn(); $check($r2['apps'] === 1 && str_contains((string) $ev2, 'eigene Veranlassung'), 'eigene Abmeldung wird als solche protokolliert'); // Unbekannter Mitarbeiter, zweimal hintereinander $check(staff_sign_out_everywhere($pdo, 999999, $chef)['at'] === 0, 'unbekannter Mitarbeiter: nichts passiert, kein Fehler'); $r3 = staff_sign_out_everywhere($pdo, $eva, $chef); $check($r3['apps'] === 0 && $r3['browsers'] === 0, 'zweites Mal: nichts mehr zu beenden, kein Fehler'); if ($failures) { fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n"); exit(1); } echo "Sign out everywhere checks passed ({$total} Prüfungen).\n";