diff --git a/ProcessMaker/Http/Controllers/Auth/ForgotPasswordController.php b/ProcessMaker/Http/Controllers/Auth/ForgotPasswordController.php index 8d2cc2cc9b..63e4aecf96 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 or inactive 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' || $user->status === 'INACTIVE')) { + 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..28a3088ffc 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,44 @@ 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')]); + } + + if ($user->status === 'INACTIVE') { + return redirect()->route('password.request') + ->withErrors(['email' => __('passwords.inactive')]); + } + + return view('auth.passwords.reset', [ + 'username' => $user->username, + 'token' => $token, + 'email' => $request->input('email'), + ]); + } + + /** + * Reset the given user's password. + * Blocked or inactive 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'); + } + + if ($user && $user->status === 'INACTIVE') { + return $this->sendResetFailedResponse($request, 'passwords.inactive'); + } - 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..bc9f2f9592 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -1546,6 +1546,8 @@ "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.", + "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 f1223bd726..7e77c49c87 100644 --- a/resources/lang/en/passwords.php +++ b/resources/lang/en/passwords.php @@ -18,5 +18,7 @@ '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.', + 'inactive' => 'Your account is inactive. 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')) diff --git a/tests/Feature/Auth/PasswordResetTest.php b/tests/Feature/Auth/PasswordResetTest.php new file mode 100644 index 0000000000..528323de78 --- /dev/null +++ b/tests/Feature/Auth/PasswordResetTest.php @@ -0,0 +1,181 @@ +create([ + 'email' => 'blocked-forgot@example.com', + 'status' => 'BLOCKED', + ]); + + $response = $this->post(route('password.email'), [ + 'email' => $user->email, + ]); + + $response->assertSessionHas('status'); + 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(); + + $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' => __('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 + { + $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 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 */ + $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'); + } +}