68 lines
2.1 KiB
PHP
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]);
|
|
}
|
|
}
|