Files
slim-blog/src/Auth/Infrastructure/PdoLoginAttemptRepository.php
2026-03-16 14:50:58 +01:00

68 lines
2.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Auth\Infrastructure;
use App\Auth\LoginAttemptRepositoryInterface;
use PDO;
class PdoLoginAttemptRepository implements LoginAttemptRepositoryInterface
{
public function __construct(private readonly PDO $db)
{
}
public function findByIp(string $ip): ?array
{
$stmt = $this->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]);
}
}