75 lines
2.1 KiB
PHP
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;
|
|
}
|
|
}
|