From 67195cb7adc570b9b69654825c96fa7c4ad5137c Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Tue, 24 Mar 2026 10:54:50 -0400 Subject: [PATCH 1/3] Implement account status checks in password reset flow - Added logic to prevent password reset for blocked users in ForgotPasswordController and ResetPasswordController. - Updated response messages for blocked accounts in language files. - Modified reset password view to retain email input value after validation errors. --- .../Auth/ForgotPasswordController.php | 29 +++++++++++++++ .../Auth/ResetPasswordController.php | 35 +++++++++++++++++-- resources/lang/en.json | 1 + resources/lang/en/passwords.php | 1 + .../views/auth/passwords/reset.blade.php | 2 +- 5 files changed, 64 insertions(+), 4 deletions(-) diff --git a/ProcessMaker/Http/Controllers/Auth/ForgotPasswordController.php b/ProcessMaker/Http/Controllers/Auth/ForgotPasswordController.php index 8d2cc2cc9b..8cf7dda5aa 100644 --- a/ProcessMaker/Http/Controllers/Auth/ForgotPasswordController.php +++ b/ProcessMaker/Http/Controllers/Auth/ForgotPasswordController.php @@ -3,7 +3,10 @@ namespace ProcessMaker\Http\Controllers\Auth; use Illuminate\Foundation\Auth\SendsPasswordResetEmails; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Password; use ProcessMaker\Http\Controllers\Controller; +use ProcessMaker\Models\User; class ForgotPasswordController extends Controller { @@ -29,4 +32,30 @@ public function __construct() { $this->middleware('guest'); } + + /** + * Send a reset link to the given user. + * Blocked users will not receive the reset email for security reasons. + * + * @param Request $request + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse + */ + public function sendResetLinkEmail(Request $request) + { + $this->validateEmail($request); + + $user = User::where('email', $request->input('email'))->first(); + + if ($user && $user->status === 'BLOCKED') { + return $this->sendResetLinkResponse($request, Password::RESET_LINK_SENT); + } + + $response = $this->broker()->sendResetLink( + $this->credentials($request) + ); + + return $response == Password::RESET_LINK_SENT + ? $this->sendResetLinkResponse($request, $response) + : $this->sendResetLinkFailedResponse($request, $response); + } } diff --git a/ProcessMaker/Http/Controllers/Auth/ResetPasswordController.php b/ProcessMaker/Http/Controllers/Auth/ResetPasswordController.php index d1e0efa770..a482c7f4f8 100644 --- a/ProcessMaker/Http/Controllers/Auth/ResetPasswordController.php +++ b/ProcessMaker/Http/Controllers/Auth/ResetPasswordController.php @@ -20,7 +20,9 @@ class ResetPasswordController extends Controller | */ - use ResetsPasswords; + use ResetsPasswords { + reset as protected performPasswordReset; + } /** * Where to redirect users after resetting their password. @@ -46,8 +48,35 @@ public function __construct() */ public function showResetForm(Request $request, $token) { - $username = User::where('email', $request->input('email'))->firstOrFail()->username; + $user = User::where('email', $request->input('email'))->firstOrFail(); + + if ($user->status === 'BLOCKED') { + return redirect()->route('password.request') + ->withErrors(['email' => __('passwords.blocked')]); + } + + return view('auth.passwords.reset', [ + 'username' => $user->username, + 'token' => $token, + 'email' => $request->input('email'), + ]); + } + + /** + * Reset the given user's password. + * Blocked users cannot reset their password. + * + * @param Request $request + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse + */ + public function reset(Request $request) + { + $user = User::where('email', $request->input('email'))->first(); + + if ($user && $user->status === 'BLOCKED') { + return $this->sendResetFailedResponse($request, 'passwords.blocked'); + } - return view('auth.passwords.reset', compact('username', 'token')); + return $this->performPasswordReset($request); } } diff --git a/resources/lang/en.json b/resources/lang/en.json index e580e5b49b..37e245720e 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -1546,6 +1546,7 @@ "passwords.sent": "We have e-mailed your password reset link!", "passwords.token": "This password reset token is invalid.", "passwords.user": "We can't find a user with that e-mail address.", + "passwords.blocked": "Your account has been blocked. Please contact your administrator.", "Pause Start Timer Events": "Pause Start Timer Events", "Pause Timer Start Events": "Pause Timer Start Events", "per page": "per page", diff --git a/resources/lang/en/passwords.php b/resources/lang/en/passwords.php index f1223bd726..d900e2ce54 100644 --- a/resources/lang/en/passwords.php +++ b/resources/lang/en/passwords.php @@ -18,5 +18,6 @@ 'throttled' => 'Please wait before retrying.', 'token' => 'This password reset token is invalid.', 'user' => "We can't find a user with that email address.", + 'blocked' => 'Your account has been blocked. Please contact your administrator.', ]; diff --git a/resources/views/auth/passwords/reset.blade.php b/resources/views/auth/passwords/reset.blade.php index 2892e74904..0059a8aa13 100644 --- a/resources/views/auth/passwords/reset.blade.php +++ b/resources/views/auth/passwords/reset.blade.php @@ -15,7 +15,7 @@
- + @if ($errors->has('email')) From 7e6b7b5da46d8fce6ba0151d825513703fc5a5b0 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Tue, 24 Mar 2026 10:55:55 -0400 Subject: [PATCH 2/3] Add PasswordResetTest to validate password reset flow for blocked and active users --- tests/Feature/Auth/PasswordResetTest.php | 127 +++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 tests/Feature/Auth/PasswordResetTest.php diff --git a/tests/Feature/Auth/PasswordResetTest.php b/tests/Feature/Auth/PasswordResetTest.php new file mode 100644 index 0000000000..70a8bc5dc9 --- /dev/null +++ b/tests/Feature/Auth/PasswordResetTest.php @@ -0,0 +1,127 @@ +create([ + 'email' => 'blocked-forgot@example.com', + 'status' => 'BLOCKED', + ]); + + $response = $this->post(route('password.email'), [ + 'email' => $user->email, + ]); + + $response->assertSessionHas('status'); + Notification::assertNothingSent(); + } + + public function testForgotPasswordSendsNotificationToActiveUser(): void + { + Notification::fake(); + + $user = User::factory()->create([ + 'email' => 'active-forgot@example.com', + 'status' => 'ACTIVE', + ]); + + $response = $this->post(route('password.email'), [ + 'email' => $user->email, + ]); + + $response->assertSessionHas('status'); + Notification::assertSentTo($user, ResetPasswordNotification::class); + } + + public function testShowResetFormRedirectsBlockedUserToRequestForm(): void + { + $user = User::factory()->create([ + 'email' => 'blocked-reset-form@example.com', + 'status' => 'BLOCKED', + ]); + + $url = route('password.reset', ['token' => 'unused-token']); + $response = $this->get($url . '?email=' . urlencode($user->email)); + + $response->assertRedirect(route('password.request')); + $response->assertSessionHasErrors('email'); + } + + public function testShowResetFormDisplaysForActiveUser(): void + { + $user = User::factory()->create([ + 'email' => 'active-reset-form@example.com', + 'status' => 'ACTIVE', + 'username' => 'active_reset_user', + ]); + + $url = route('password.reset', ['token' => 'some-token']); + $response = $this->get($url . '?email=' . urlencode($user->email)); + + $response->assertOk(); + $response->assertViewIs('auth.passwords.reset'); + $response->assertViewHas('email', $user->email); + $response->assertViewHas('username', $user->username); + $response->assertViewHas('token', 'some-token'); + } + + public function testResetPasswordRejectsBlockedUser(): void + { + $user = User::factory()->create([ + 'email' => 'blocked-reset-post@example.com', + 'status' => 'BLOCKED', + ]); + + $response = $this->from(route('password.request'))->post('/password/reset', [ + 'token' => 'will-not-be-used', + 'email' => $user->email, + 'password' => 'NewPassword123!', + 'password_confirmation' => 'NewPassword123!', + ]); + + $response->assertSessionHasErrors([ + 'email' => __('passwords.blocked'), + ]); + } + + public function testResetPasswordUpdatesPasswordForActiveUser(): void + { + /** @var User $user */ + $user = User::factory()->create([ + 'email' => 'active-reset-post@example.com', + 'status' => 'ACTIVE', + ]); + + /** @var ConcretePasswordBroker $broker */ + $broker = Password::broker(); + $token = $broker->createToken($user); + $plaintextSecret = 'NewSecurePass123!'; + + $response = $this->post('/password/reset', [ + 'token' => $token, + 'email' => $user->email, + 'password' => $plaintextSecret, + 'password_confirmation' => $plaintextSecret, + ]); + + $response->assertRedirect('/password/success'); + $response->assertSessionHas('status'); + + $user->refresh(); + $this->assertTrue(Hash::check($plaintextSecret, $user->password)); + $this->assertAuthenticatedAs($user, 'web'); + } +} From e41939ca9b7aa3becbec3410c5def467452a0fb3 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Tue, 24 Mar 2026 11:36:45 -0400 Subject: [PATCH 3/3] Enhance password reset flow to include checks for inactive user --- .../Auth/ForgotPasswordController.php | 4 +- .../Auth/ResetPasswordController.php | 11 +++- resources/lang/en.json | 1 + resources/lang/en/passwords.php | 1 + tests/Feature/Auth/PasswordResetTest.php | 56 ++++++++++++++++++- 5 files changed, 69 insertions(+), 4 deletions(-) diff --git a/ProcessMaker/Http/Controllers/Auth/ForgotPasswordController.php b/ProcessMaker/Http/Controllers/Auth/ForgotPasswordController.php index 8cf7dda5aa..63e4aecf96 100644 --- a/ProcessMaker/Http/Controllers/Auth/ForgotPasswordController.php +++ b/ProcessMaker/Http/Controllers/Auth/ForgotPasswordController.php @@ -35,7 +35,7 @@ public function __construct() /** * Send a reset link to the given user. - * Blocked users will not receive the reset email for security reasons. + * Blocked or inactive users will not receive the reset email for security reasons. * * @param Request $request * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse @@ -46,7 +46,7 @@ public function sendResetLinkEmail(Request $request) $user = User::where('email', $request->input('email'))->first(); - if ($user && $user->status === 'BLOCKED') { + if ($user && ($user->status === 'BLOCKED' || $user->status === 'INACTIVE')) { return $this->sendResetLinkResponse($request, Password::RESET_LINK_SENT); } diff --git a/ProcessMaker/Http/Controllers/Auth/ResetPasswordController.php b/ProcessMaker/Http/Controllers/Auth/ResetPasswordController.php index a482c7f4f8..28a3088ffc 100644 --- a/ProcessMaker/Http/Controllers/Auth/ResetPasswordController.php +++ b/ProcessMaker/Http/Controllers/Auth/ResetPasswordController.php @@ -55,6 +55,11 @@ public function showResetForm(Request $request, $token) ->withErrors(['email' => __('passwords.blocked')]); } + if ($user->status === 'INACTIVE') { + return redirect()->route('password.request') + ->withErrors(['email' => __('passwords.inactive')]); + } + return view('auth.passwords.reset', [ 'username' => $user->username, 'token' => $token, @@ -64,7 +69,7 @@ public function showResetForm(Request $request, $token) /** * Reset the given user's password. - * Blocked users cannot reset their password. + * Blocked or inactive users cannot reset their password. * * @param Request $request * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse @@ -77,6 +82,10 @@ public function reset(Request $request) return $this->sendResetFailedResponse($request, 'passwords.blocked'); } + if ($user && $user->status === 'INACTIVE') { + return $this->sendResetFailedResponse($request, 'passwords.inactive'); + } + return $this->performPasswordReset($request); } } diff --git a/resources/lang/en.json b/resources/lang/en.json index 37e245720e..bc9f2f9592 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -1547,6 +1547,7 @@ "passwords.token": "This password reset token is invalid.", "passwords.user": "We can't find a user with that e-mail address.", "passwords.blocked": "Your account has been blocked. Please contact your administrator.", + "passwords.inactive": "Your account is inactive. Please contact your administrator.", "Pause Start Timer Events": "Pause Start Timer Events", "Pause Timer Start Events": "Pause Timer Start Events", "per page": "per page", diff --git a/resources/lang/en/passwords.php b/resources/lang/en/passwords.php index d900e2ce54..7e77c49c87 100644 --- a/resources/lang/en/passwords.php +++ b/resources/lang/en/passwords.php @@ -19,5 +19,6 @@ 'token' => 'This password reset token is invalid.', 'user' => "We can't find a user with that email address.", 'blocked' => 'Your account has been blocked. Please contact your administrator.', + 'inactive' => 'Your account is inactive. Please contact your administrator.', ]; diff --git a/tests/Feature/Auth/PasswordResetTest.php b/tests/Feature/Auth/PasswordResetTest.php index 70a8bc5dc9..528323de78 100644 --- a/tests/Feature/Auth/PasswordResetTest.php +++ b/tests/Feature/Auth/PasswordResetTest.php @@ -29,6 +29,23 @@ public function testForgotPasswordDoesNotNotifyBlockedUser(): void Notification::assertNothingSent(); } + public function testForgotPasswordDoesNotNotifyInactiveUser(): void + { + Notification::fake(); + + $user = User::factory()->create([ + 'email' => 'inactive-forgot@example.com', + 'status' => 'INACTIVE', + ]); + + $response = $this->post(route('password.email'), [ + 'email' => $user->email, + ]); + + $response->assertSessionHas('status'); + Notification::assertNothingSent(); + } + public function testForgotPasswordSendsNotificationToActiveUser(): void { Notification::fake(); @@ -57,7 +74,25 @@ public function testShowResetFormRedirectsBlockedUserToRequestForm(): void $response = $this->get($url . '?email=' . urlencode($user->email)); $response->assertRedirect(route('password.request')); - $response->assertSessionHasErrors('email'); + $response->assertSessionHasErrors([ + 'email' => __('passwords.blocked'), + ]); + } + + public function testShowResetFormRedirectsInactiveUserToRequestForm(): void + { + $user = User::factory()->create([ + 'email' => 'inactive-reset-form@example.com', + 'status' => 'INACTIVE', + ]); + + $url = route('password.reset', ['token' => 'unused-token']); + $response = $this->get($url . '?email=' . urlencode($user->email)); + + $response->assertRedirect(route('password.request')); + $response->assertSessionHasErrors([ + 'email' => __('passwords.inactive'), + ]); } public function testShowResetFormDisplaysForActiveUser(): void @@ -97,6 +132,25 @@ public function testResetPasswordRejectsBlockedUser(): void ]); } + public function testResetPasswordRejectsInactiveUser(): void + { + $user = User::factory()->create([ + 'email' => 'inactive-reset-post@example.com', + 'status' => 'INACTIVE', + ]); + + $response = $this->from(route('password.request'))->post('/password/reset', [ + 'token' => 'will-not-be-used', + 'email' => $user->email, + 'password' => 'NewPassword123!', + 'password_confirmation' => 'NewPassword123!', + ]); + + $response->assertSessionHasErrors([ + 'email' => __('passwords.inactive'), + ]); + } + public function testResetPasswordUpdatesPasswordForActiveUser(): void { /** @var User $user */