diff --git a/app/Domains/Auth/Controllers/ValidateResetPasswordAttemptController.php b/app/Domains/Auth/Controllers/ValidateResetPasswordAttemptController.php index 07fc705..64d5066 100644 --- a/app/Domains/Auth/Controllers/ValidateResetPasswordAttemptController.php +++ b/app/Domains/Auth/Controllers/ValidateResetPasswordAttemptController.php @@ -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'), ]); diff --git a/app/Domains/Auth/Services/ResetPasswordAttemptService.php b/app/Domains/Auth/Services/ResetPasswordAttemptService.php index 1a0e92f..9fa15eb 100644 --- a/app/Domains/Auth/Services/ResetPasswordAttemptService.php +++ b/app/Domains/Auth/Services/ResetPasswordAttemptService.php @@ -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(); diff --git a/lang/en/api.php b/lang/en/api.php index 1ba97e4..d05ff2c 100644 --- a/lang/en/api.php +++ b/lang/en/api.php @@ -12,6 +12,7 @@ return [ 'password_reset_invalid' => 'The password recovery request is invalid or has already been used.', 'password_updated' => 'Password updated successfully.', 'reset_code_invalid' => 'The code you entered is invalid.', + 'reset_code_expired' => 'The password recovery code expired. Request a new one.', 'reset_code_valid' => 'Code validated successfully.', 'invalid_tenant_or_return_url' => 'The tenant or return URL is invalid.', 'request_expired' => 'The authentication request expired. Please try again.', diff --git a/lang/es/api.php b/lang/es/api.php index b1ee881..6ccb5ca 100644 --- a/lang/es/api.php +++ b/lang/es/api.php @@ -12,6 +12,7 @@ return [ 'password_reset_invalid' => 'La solicitud de recuperación es inválida o ya fue utilizada.', 'password_updated' => 'Contraseña modificada correctamente.', 'reset_code_invalid' => 'El código ingresado es inválido.', + 'reset_code_expired' => 'El código de recuperación expiró. Solicitá uno nuevo.', 'reset_code_valid' => 'Código validado correctamente.', 'invalid_tenant_or_return_url' => 'El tenant o la URL de retorno no son válidos.', 'request_expired' => 'La solicitud de autenticación expiró. Intenta nuevamente.',