Files
slim-blog/src/Auth/PasswordResetRepository.php
2026-03-16 01:47:07 +01:00

75 lines
2.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Auth;
use PDO;
final class PasswordResetRepository implements PasswordResetRepositoryInterface
{
public function __construct(private readonly PDO $db)
{
}
public function create(int $userId, string $tokenHash, string $expiresAt): void
{
$stmt = $this->db->prepare('
INSERT INTO password_resets (user_id, token_hash, expires_at, created_at)
VALUES (:user_id, :token_hash, :expires_at, :created_at)
');
$stmt->execute([
':user_id' => $userId,
':token_hash' => $tokenHash,
':expires_at' => $expiresAt,
':created_at' => date('Y-m-d H:i:s'),
]);
}
public function findActiveByHash(string $tokenHash): ?array
{
$stmt = $this->db->prepare(
'SELECT * FROM password_resets WHERE token_hash = :token_hash AND used_at IS NULL'
);
$stmt->execute([':token_hash' => $tokenHash]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
public function invalidateByUserId(int $userId): void
{
$stmt = $this->db->prepare(
'UPDATE password_resets SET used_at = :used_at WHERE user_id = :user_id AND used_at IS NULL'
);
$stmt->execute([':used_at' => date('Y-m-d H:i:s'), ':user_id' => $userId]);
}
/**
* Atomically consume a token and return the affected row.
* Uses UPDATE ... RETURNING to avoid SELECT+UPDATE race conditions.
*/
public function consumeActiveToken(string $tokenHash, string $usedAt): ?array
{
$stmt = $this->db->prepare(
'UPDATE password_resets
SET used_at = :used_at
WHERE token_hash = :token_hash
AND used_at IS NULL
AND expires_at >= :now
RETURNING *'
);
$stmt->execute([
':used_at' => $usedAt,
':token_hash' => $tokenHash,
':now' => $usedAt,
]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
}