getTimestampMs(); // Ohne Model-Events und ohne updated_at: Die Marke ist keine // inhaltliche Änderung am Konto und soll keine Observer auslösen. $user->newQuery()->whereKey($user->getKey())->update(['sessions_revoked_at_ms' => $revokedAt]); $user->forceFill(['sessions_revoked_at_ms' => $revokedAt])->syncOriginalAttribute('sessions_revoked_at_ms'); return $revokedAt; } /** * Alle anderen Sessions des Kontos werden ungültig, die übergebene * (aktuelle) bleibt angemeldet: Sie erhält einen Anmeldezeitpunkt nach * der Sperre und eine neue Session-ID (Sicherheitsaudit 07.10.2026, S-04). */ public function revokeOthersFor(User $user, Session $current): void { $revokedAt = $this->revokeAllFor($user); $current->put(self::SESSION_KEY, $revokedAt + 1); $current->migrate(true); } /** * „Andere Geräte abmelden“ auf der Sicherheitsseite: Remember-Cookies * anderer Geräte werden ungültig (neuer `remember_token`), alle anderen * Sessions enden beim nächsten Request, die aktuelle bleibt angemeldet. */ public function logoutOtherDevices(User $user, Session $current): void { $user->forceFill(['remember_token' => Str::random(60)])->save(); $this->revokeOthersFor($user, $current); } /** * Listener für `Login`: Zeitpunkt der Anmeldung in der Session. * Die Methode heißt bewusst nicht `handle*` (Event-Discovery). */ public function stampLogin(Login $event): void { if (! app()->bound('request') || ! request()->hasSession()) { return; } request()->session()->put(self::SESSION_KEY, now()->getTimestampMs()); } /** * Session stammt aus der Zeit vor der letzten Sperre dieses Kontos. * Ohne Anmeldezeitpunkt (Session von vor dem Deploy) gilt sie als alt. */ public function isRevoked(User $user, Session $session): bool { $revokedAt = (int) $user->sessions_revoked_at_ms; if ($revokedAt === 0) { return false; } return (int) $session->get(self::SESSION_KEY, 0) < $revokedAt; } }