forked from fa/breadcrumb-the-shire
- Add single-line class docblocks to all 59 repository classes and interfaces describing scope and responsibility - Add multi-line docblocks to key services documenting business rules: AuthService (6-step login cascade), ImportService (3-phase CSV workflow), TenantScopeService (strict/permissive modes), PermissionService (RBAC resolution + two-tier caching), UserAccountService (atomicity + audit) - Add transaction(callable) wrapper to DatabaseSessionRepository to DRY up begin/commit/rollback boilerplate Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
106 lines
3.0 KiB
PHP
106 lines
3.0 KiB
PHP
<?php
|
|
|
|
namespace MintyPHP\Repository\Auth;
|
|
|
|
use MintyPHP\DB;
|
|
|
|
/** Stores password reset codes with attempt counters, expiry, and completion history. */
|
|
class PasswordResetRepository implements PasswordResetRepositoryInterface
|
|
{
|
|
private function unwrapList($rows): array
|
|
{
|
|
if (!is_array($rows)) {
|
|
return [];
|
|
}
|
|
$list = [];
|
|
foreach ($rows as $row) {
|
|
$data = $row['password_resets'] ?? $row;
|
|
if (is_array($data)) {
|
|
$list[] = $data;
|
|
}
|
|
}
|
|
return $list;
|
|
}
|
|
|
|
public function create(int $userId, string $codeHash, string $expiresAt): ?int
|
|
{
|
|
$id = DB::insert(
|
|
'insert into password_resets (user_id, code_hash, expires_at, attempts, used_at, created) values (?,?,?,?,NULL,NOW())',
|
|
(string) $userId,
|
|
$codeHash,
|
|
$expiresAt,
|
|
0
|
|
);
|
|
return $id ? (int) $id : null;
|
|
}
|
|
|
|
public function invalidateForUser(int $userId): bool
|
|
{
|
|
$result = DB::update(
|
|
'update password_resets set used_at = NOW() where user_id = ? and used_at is null',
|
|
(string) $userId
|
|
);
|
|
return $result !== false;
|
|
}
|
|
|
|
public function findActiveByUserId(int $userId): ?array
|
|
{
|
|
$row = DB::selectOne(
|
|
'select id, user_id, code_hash, expires_at, attempts, used_at from password_resets where user_id = ? and used_at is null and expires_at > UTC_TIMESTAMP() order by id desc limit 1',
|
|
(string) $userId
|
|
);
|
|
if (!$row || !isset($row['password_resets'])) {
|
|
return null;
|
|
}
|
|
return $row['password_resets'];
|
|
}
|
|
|
|
public function findById(int $id): ?array
|
|
{
|
|
$row = DB::selectOne(
|
|
'select id, user_id, code_hash, expires_at, attempts, used_at from password_resets where id = ? limit 1',
|
|
(string) $id
|
|
);
|
|
if (!$row || !isset($row['password_resets'])) {
|
|
return null;
|
|
}
|
|
return $row['password_resets'];
|
|
}
|
|
|
|
public function incrementAttempts(int $id): bool
|
|
{
|
|
$result = DB::update(
|
|
'update password_resets set attempts = attempts + 1 where id = ?',
|
|
(string) $id
|
|
);
|
|
return $result !== false;
|
|
}
|
|
|
|
public function markUsed(int $id): bool
|
|
{
|
|
$result = DB::update(
|
|
'update password_resets set used_at = NOW() where id = ?',
|
|
(string) $id
|
|
);
|
|
return $result !== false;
|
|
}
|
|
|
|
public function listByUserId(int $userId, int $limit = 20): array
|
|
{
|
|
if ($userId <= 0) {
|
|
return [];
|
|
}
|
|
if ($limit < 1) {
|
|
$limit = 20;
|
|
} elseif ($limit > 100) {
|
|
$limit = 100;
|
|
}
|
|
$rows = DB::select(
|
|
'select id, user_id, expires_at, attempts, used_at, created from password_resets where user_id = ? order by id desc limit ?',
|
|
(string) $userId,
|
|
(string) $limit
|
|
);
|
|
return $this->unwrapList($rows);
|
|
}
|
|
}
|