Merge branch 'feature/set_reset_password_expiration' into dev

This commit is contained in:
2026-08-26 11:51:00 -03:00
10 changed files with 136 additions and 7 deletions

View File

@@ -22,10 +22,18 @@ class ValidateResetPasswordAttemptController extends Controller
{
$data = $request->validated();
if (! $this->resetPasswordAttemptService->validateCode(
$result = $this->resetPasswordAttemptService->validateCode(
$data['email'],
$data['codigo'],
)) {
);
if ($result === ResetPasswordAttemptService::CODE_EXPIRED) {
throw ValidationException::withMessages([
'codigo' => __('api.auth.reset_code_expired'),
]);
}
if ($result !== ResetPasswordAttemptService::CODE_VALID) {
throw ValidationException::withMessages([
'codigo' => __('api.auth.reset_code_invalid'),
]);

View File

@@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable(['user_id', 'codigo', 'reason', 'status'])]
#[Fillable(['user_id', 'codigo', 'reason', 'status', 'expires_at'])]
#[Hidden(['codigo'])]
class ResetPasswordAttempt extends Model
{
@@ -31,6 +31,7 @@ class ResetPasswordAttempt extends Model
{
return [
'user_id' => 'integer',
'expires_at' => 'datetime',
];
}

View File

@@ -12,6 +12,12 @@ use Throwable;
class ResetPasswordAttemptService
{
public const CODE_VALID = 'valid';
public const CODE_INVALID = 'invalid';
public const CODE_EXPIRED = 'expired';
public function createForEmail(
string $email,
string $tenantCode,
@@ -146,12 +152,12 @@ class ResetPasswordAttemptService
);
}
public function validateCode(string $email, string $code): bool
public function validateCode(string $email, string $code): string
{
$emailFingerprint = $this->emailFingerprint($email);
try {
return DB::transaction(function () use ($email, $code, $emailFingerprint): bool {
return DB::transaction(function () use ($email, $code, $emailFingerprint): string {
$user = User::query()
->where('email', $email)
->lockForUpdate()
@@ -169,14 +175,27 @@ class ResetPasswordAttemptService
'email_fingerprint' => $emailFingerprint,
]);
return false;
return self::CODE_INVALID;
}
if ($attempt->expires_at?->isPast()) {
$attempt->update([
'status' => ResetPasswordAttempt::STATUS_EXPIRED,
]);
Log::info('Password reset code validation failed: attempt expired.', [
'email_fingerprint' => $emailFingerprint,
'attempt_id' => $attempt->getKey(),
]);
return self::CODE_EXPIRED;
}
$attempt->update([
'status' => ResetPasswordAttempt::STATUS_VALIDATED,
]);
return true;
return self::CODE_VALID;
});
} catch (Throwable $exception) {
Log::error('Failed to validate password reset code.', [
@@ -214,6 +233,19 @@ class ResetPasswordAttemptService
return false;
}
if ($attempt->expires_at?->isPast()) {
$attempt->update([
'status' => ResetPasswordAttempt::STATUS_EXPIRED,
]);
Log::info('Password reset failed: attempt expired.', [
'email_fingerprint' => $emailFingerprint,
'attempt_id' => $attempt->getKey(),
]);
return false;
}
$user->password = $password;
$user->failed_login_attempts = 0;
$user->last_failed_login_at = null;
@@ -270,6 +302,7 @@ class ResetPasswordAttemptService
'codigo' => $this->generateCode(),
'reason' => $reason,
'status' => ResetPasswordAttempt::STATUS_PENDING,
'expires_at' => now()->addMinutes((int) config('auth.passwords.users.expire')),
]);
return $attempt->getKey();