diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php --- a/src/resources/lang/en/ui.php +++ b/src/resources/lang/en/ui.php @@ -295,6 +295,7 @@ ], 'password' => [ + 'link-invalid' => "The password reset code is expired or invalid.", 'reset' => "Password Reset", 'reset-step1' => "Enter your email address to reset your password.", 'reset-step1-hint' => "You may need to check your spam folder or unblock {email}.", @@ -426,6 +427,7 @@ 'pass-input' => "Enter password", 'pass-link' => "Set via link", 'pass-link-label' => "Link:", + 'pass-link-hint' => "Press Submit to activate the link", 'passwordpolicy' => "Password Policy", 'price' => "Price", 'profile-title' => "Your profile", diff --git a/src/resources/vue/PasswordReset.vue b/src/resources/vue/PasswordReset.vue --- a/src/resources/vue/PasswordReset.vue +++ b/src/resources/vue/PasswordReset.vue @@ -85,8 +85,7 @@ this.short_code = RegExp.$1 this.code = RegExp.$2 this.submitStep2(true) - } - else { + } else { this.$root.errorPage(404) } } @@ -109,23 +108,28 @@ }, // Submits the code to the API for verification submitStep2(bylink) { + let post = { + code: this.code, + short_code: this.short_code + } + + let params = {} + if (bylink === true) { - this.displayForm(2, false) + this.$root.startLoading() + params.ignoreErrors = true } this.$root.clearFormValidation($('#step2 form')) - axios.post('/api/auth/password-reset/verify', { - code: this.code, - short_code: this.short_code - }).then(response => { + axios.post('/api/auth/password-reset/verify', post, params).then(response => { + this.$root.stopLoading() this.userId = response.data.userId this.displayForm(3, true) }).catch(error => { if (bylink === true) { - // FIXME: display step 1, user can do nothing about it anyway - // Maybe we should display 404 error page? - this.displayForm(1, true) + this.$root.stopLoading() + this.$root.errorPage(404, '', this.$t('password.link-invalid')) } }) }, diff --git a/src/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue --- a/src/resources/vue/User/Info.vue +++ b/src/resources/vue/User/Info.vue @@ -78,6 +78,7 @@ +
{{ $t('user.pass-link-hint') }}
diff --git a/src/tests/Browser/Pages/PasswordReset.php b/src/tests/Browser/Pages/PasswordReset.php --- a/src/tests/Browser/Pages/PasswordReset.php +++ b/src/tests/Browser/Pages/PasswordReset.php @@ -25,8 +25,7 @@ */ public function assert($browser) { - $browser->assertPathIs('/password-reset'); - $browser->assertPresent('@step1'); + $browser->assertPathBeginsWith('/password-reset'); } /** diff --git a/src/tests/Browser/PasswordResetTest.php b/src/tests/Browser/PasswordResetTest.php --- a/src/tests/Browser/PasswordResetTest.php +++ b/src/tests/Browser/PasswordResetTest.php @@ -5,6 +5,7 @@ use App\User; use App\VerificationCode; use Tests\Browser; +use Tests\Browser\Components\Menu; use Tests\Browser\Pages\Dashboard; use Tests\Browser\Pages\Home; use Tests\Browser\Pages\PasswordReset; @@ -39,13 +40,11 @@ public function testLinkOnLogon(): void { $this->browse(function (Browser $browser) { - $browser->visit(new Home()); - - $browser->assertSeeLink('Forgot password?'); - $browser->clickLink('Forgot password?'); - - $browser->on(new PasswordReset()); - $browser->assertVisible('@step1'); + $browser->visit(new Home()) + ->assertSeeLink('Forgot password?') + ->clickLink('Forgot password?') + ->on(new PasswordReset()) + ->assertVisible('@step1'); }); } @@ -285,6 +284,44 @@ } /** + * Test password-reset via a link + */ + public function testResetViaLink(): void + { + $user = $this->getTestUser('passwordresettestdusk@' . \config('app.domain')); + $user->setSetting('external_email', 'external@domain.tld'); + + $code = new VerificationCode(['mode' => 'password-reset']); + $user->verificationcodes()->save($code); + + $this->browse(function (Browser $browser) use ($code) { + // Test a valid link + $browser->visit("/password-reset/{$code->short_code}-{$code->code}") + ->on(new PasswordReset()) + ->waitFor('@step3') + ->assertMissing('@step1') + ->assertMissing('@step2') + ->with('@step3', function ($step) { + $step->type('#reset_password', 'A2345678') + ->type('#reset_password_confirmation', 'A2345678') + ->click('[type=submit]'); + }) + ->waitUntilMissing('@step3') + // At this point we should be auto-logged-in to dashboard + ->on(new Dashboard()) + ->within(new Menu(), function ($browser) { + $browser->clickMenuItem('logout'); + }); + + $this->assertNull(VerificationCode::find($code->code)); + + // Test an invalid link + $browser->visit("/password-reset/{$code->short_code}-{$code->code}") + ->assertErrorPage(404, 'The password reset code is expired or invalid.'); + }); + } + + /** * Test password reset process for a user with 2FA enabled. */ public function testResetWith2FA(): void diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php --- a/src/tests/Browser/UsersTest.php +++ b/src/tests/Browser/UsersTest.php @@ -323,7 +323,8 @@ ->assertVisible('#password-link button.text-danger') ->assertVisible('#password-link button:not(.text-danger)') ->assertAttribute('#password-link button:not(.text-danger)', 'title', 'Copy') - ->assertAttribute('#password-link button.text-danger', 'title', 'Delete'); + ->assertAttribute('#password-link button.text-danger', 'title', 'Delete') + ->assertMissing('#password-link div.form-text'); // Test deleting an existing password reset link $browser->click('#password-link button.text-danger') @@ -341,7 +342,8 @@ ->assertMissing('#password') ->assertMissing('#password_confirmation') ->waitFor('#password-link code') - ->assertSeeIn('#password-link code', $link); + ->assertSeeIn('#password-link code', $link) + ->assertSeeIn('#password-link div.form-text', "Press Submit to activate the link"); // Test copy to clipboard /* TODO: Figure out how to give permission to do this operation