db->prepare('SELECT * FROM login_attempts WHERE ip = :ip'); $stmt->execute([':ip' => $ip]); /** @var array{ip: string, attempts: int, locked_until: string|null, updated_at: string}|false $row */ $row = $stmt->fetch(PDO::FETCH_ASSOC); return $row ?: null; } public function recordFailure(string $ip, int $maxAttempts, int $lockMinutes): void { $now = (new \DateTime())->format('Y-m-d H:i:s'); $lockUntil = (new \DateTime())->modify("+{$lockMinutes} minutes")->format('Y-m-d H:i:s'); $stmt = $this->db->prepare( 'INSERT INTO login_attempts (ip, attempts, locked_until, updated_at) VALUES (:ip, 1, CASE WHEN 1 >= :max1 THEN :lock1 ELSE NULL END, :now1) ON CONFLICT(ip) DO UPDATE SET attempts = login_attempts.attempts + 1, locked_until = CASE WHEN login_attempts.attempts + 1 >= :max2 THEN :lock2 ELSE NULL END, updated_at = :now2' ); $stmt->execute([ ':ip' => $ip, ':max1' => $maxAttempts, ':lock1' => $lockUntil, ':now1' => $now, ':max2' => $maxAttempts, ':lock2' => $lockUntil, ':now2' => $now, ]); } public function resetForIp(string $ip): void { $stmt = $this->db->prepare('DELETE FROM login_attempts WHERE ip = :ip'); $stmt->execute([':ip' => $ip]); } public function deleteExpired(): void { $now = (new \DateTime())->format('Y-m-d H:i:s'); $stmt = $this->db->prepare( 'DELETE FROM login_attempts WHERE locked_until IS NOT NULL AND locked_until < :now' ); $stmt->execute([':now' => $now]); } }