→ JSON { typ, sub, class_id?, roles, created } (TTL, Sliding) * * Widerruf (Logout, Bann, „alle Geräte abmelden", Passwort-Wechsel) = DEL. * Da der Access-Token kurz lebt (JWT_TTL), wirkt der Widerruf binnen Minuten. * * Reine Bibliothek — wird erst im Cutover genutzt. predis ist pure PHP, * braucht KEINE Redis-Extension (läuft auf XAMPP wie im Prod-Image). */ use Predis\Client as PredisClient; class SessionStore { private static ?PredisClient $client = null; public static function client(): PredisClient { if (self::$client === null) { $url = defined('REDIS_URL') ? REDIS_URL : 'tcp://127.0.0.1:6379'; self::$client = new PredisClient($url); } return self::$client; } private static function key(string $sid): string { return 'sess:' . $sid; } private static function ttl(): int { return defined('JWT_REFRESH_TTL') ? JWT_REFRESH_TTL : 2592000; } /** Opake 256-bit Session-ID. */ public static function newSid(): string { return bin2hex(random_bytes(32)); } /** Neue Session anlegen → sid. */ public static function create(array $data, ?int $ttl = null): string { $sid = self::newSid(); $data['created'] = time(); self::client()->setex(self::key($sid), $ttl ?? self::ttl(), json_encode($data)); return $sid; } /** Session lesen (null wenn widerrufen/abgelaufen). Sliding-Expiry per default. */ public static function get(string $sid, bool $touch = true): ?array { $raw = self::client()->get(self::key($sid)); if ($raw === null) return null; if ($touch) self::client()->expire(self::key($sid), self::ttl()); $data = json_decode($raw, true); return is_array($data) ? $data : null; } /** Widerruf (Logout / Bann). */ public static function revoke(string $sid): void { self::client()->del([self::key($sid)]); } /** Verbindungstest — für Health-Check / Setup-Verifikation. */ public static function ping(): bool { try { return (string) self::client()->ping() !== ''; } catch (\Throwable $e) { return false; } } }