diff --git a/scripts/langindex.json b/scripts/langindex.json index 565b5b0bf..4d2070a51 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1934,6 +1934,8 @@ "core.login.errorupdatesite": "local_moodlemobileapp", "core.login.exceededloginattempts": "local_moodlemobileapp", "core.login.exceededloginattemptssupportsubject": "local_moodlemobileapp", + "core.login.exceededpasswordresetattempts": "local_moodlemobileapp", + "core.login.exceededpasswordresetattemptssupportsubject": "local_moodlemobileapp", "core.login.faqcannotconnectanswer": "local_moodlemobileapp", "core.login.faqcannotconnectquestion": "local_moodlemobileapp", "core.login.faqcannotfindmysiteanswer": "local_moodlemobileapp", diff --git a/src/core/features/login/lang.json b/src/core/features/login/lang.json index a8cc80245..bd53aaf6c 100644 --- a/src/core/features/login/lang.json +++ b/src/core/features/login/lang.json @@ -29,6 +29,8 @@ "errorupdatesite": "An error occurred while updating the site's token.", "exceededloginattempts": "Need help logging in? Try recovering your password or contact your site support", "exceededloginattemptssupportsubject": "I can't log in", + "exceededpasswordresetattempts": "It seems you are having trouble accessing your account. You can contact your administrator or try again later.", + "exceededpasswordresetattemptssupportsubject": "I can't reset my password", "faqcannotconnectanswer": "Please, contact your site administrator.", "faqcannotconnectquestion": "I typed my site address correctly but I still can't connect.", "faqcannotfindmysiteanswer": "Have you typed the name correctly? It's also possible that your site is not included in our public sites directory. If you still can't find it, please enter your site address instead.", diff --git a/src/core/features/login/login-lazy.module.ts b/src/core/features/login/login-lazy.module.ts index ec2e6fd9f..adef10700 100644 --- a/src/core/features/login/login-lazy.module.ts +++ b/src/core/features/login/login-lazy.module.ts @@ -19,6 +19,7 @@ import { CoreSharedModule } from '@/core/shared.module'; import { CoreLoginHasSitesGuard } from './guards/has-sites'; import { CoreLoginComponentsModule } from './components/components.module'; import { CoreLoginHelper } from './services/login-helper'; +import { CoreLoginForgottenPasswordPage } from '@features/login/pages/forgotten-password/forgotten-password'; const routes: Routes = [ { @@ -42,8 +43,7 @@ const routes: Routes = [ }, { path: 'forgottenpassword', - loadChildren: () => import('./pages/forgotten-password/forgotten-password.module') - .then( m => m.CoreLoginForgottenPasswordPageModule), + component: CoreLoginForgottenPasswordPage, }, { path: 'changepassword', @@ -70,5 +70,8 @@ const routes: Routes = [ CoreLoginComponentsModule, RouterModule.forChild(routes), ], + declarations: [ + CoreLoginForgottenPasswordPage, + ], }) export class CoreLoginLazyModule {} diff --git a/src/core/features/login/login.module.ts b/src/core/features/login/login.module.ts index 47102a5bf..ac69177bc 100644 --- a/src/core/features/login/login.module.ts +++ b/src/core/features/login/login.module.ts @@ -43,7 +43,7 @@ const appRoutes: Routes = [ { provide: APP_INITIALIZER, multi: true, - useValue: () => { + useValue: async () => { CoreCronDelegate.register(CoreLoginCronHandler.instance); CoreEvents.on(CoreEvents.SESSION_EXPIRED, (data) => { @@ -57,6 +57,8 @@ const appRoutes: Routes = [ CoreEvents.on(CoreEvents.SITE_POLICY_NOT_AGREED, (data) => { CoreLoginHelper.sitePolicyNotAgreed(data.siteId); }); + + await CoreLoginHelper.initialize(); }, }, ], diff --git a/src/core/features/login/pages/forgotten-password/forgotten-password.html b/src/core/features/login/pages/forgotten-password/forgotten-password.html index 5fe821695..b079fbcd3 100644 --- a/src/core/features/login/pages/forgotten-password/forgotten-password.html +++ b/src/core/features/login/pages/forgotten-password/forgotten-password.html @@ -11,6 +11,10 @@
+ + {{ 'core.login.exceededpasswordresetattempts' | translate }} + diff --git a/src/core/features/login/pages/forgotten-password/forgotten-password.module.ts b/src/core/features/login/pages/forgotten-password/forgotten-password.module.ts deleted file mode 100644 index eca47959e..000000000 --- a/src/core/features/login/pages/forgotten-password/forgotten-password.module.ts +++ /dev/null @@ -1,38 +0,0 @@ -// (C) Copyright 2015 Moodle Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; - -import { CoreSharedModule } from '@/core/shared.module'; -import { CoreLoginForgottenPasswordPage } from './forgotten-password'; - -const routes: Routes = [ - { - path: '', - component: CoreLoginForgottenPasswordPage, - }, -]; - -@NgModule({ - imports: [ - RouterModule.forChild(routes), - CoreSharedModule, - ], - declarations: [ - CoreLoginForgottenPasswordPage, - ], - exports: [RouterModule], -}) -export class CoreLoginForgottenPasswordPageModule {} diff --git a/src/core/features/login/pages/forgotten-password/forgotten-password.ts b/src/core/features/login/pages/forgotten-password/forgotten-password.ts index c88c9d283..e838f069d 100644 --- a/src/core/features/login/pages/forgotten-password/forgotten-password.ts +++ b/src/core/features/login/pages/forgotten-password/forgotten-password.ts @@ -22,6 +22,7 @@ import { CoreWSExternalWarning } from '@services/ws'; import { CoreNavigator } from '@services/navigator'; import { CoreForms } from '@singletons/form'; import { CorePlatform } from '@services/platform'; +import { CoreSitePublicConfigResponse } from '@classes/site'; /** * Page to recover a forgotten password. @@ -36,17 +37,16 @@ export class CoreLoginForgottenPasswordPage implements OnInit { myForm!: FormGroup; siteUrl!: string; + siteConfig?: CoreSitePublicConfigResponse; autoFocus!: boolean; + wasPasswordResetRequestedRecently = false; - constructor( - protected formBuilder: FormBuilder, - ) { - } + constructor(protected formBuilder: FormBuilder) {} /** * Initialize the component. */ - ngOnInit(): void { + async ngOnInit(): Promise { const siteUrl = CoreNavigator.getRouteParam('siteUrl'); if (!siteUrl) { CoreDomUtils.showErrorModal('Site URL not supplied.'); @@ -56,11 +56,14 @@ export class CoreLoginForgottenPasswordPage implements OnInit { } this.siteUrl = siteUrl; + this.siteConfig = CoreNavigator.getRouteParam('siteConfig'); this.autoFocus = CorePlatform.is('tablet'); this.myForm = this.formBuilder.group({ field: ['username', Validators.required], value: [CoreNavigator.getRouteParam('username') || '', Validators.required], }); + + this.wasPasswordResetRequestedRecently = await CoreLoginHelper.wasPasswordResetRequestedRecently(siteUrl); } /** @@ -101,8 +104,9 @@ export class CoreLoginForgottenPasswordPage implements OnInit { // Success. CoreForms.triggerFormSubmittedEvent(this.formElement, true); - CoreDomUtils.showAlert(Translate.instant('core.success'), response.notice); - CoreNavigator.back(); + await CoreDomUtils.showAlert(Translate.instant('core.success'), response.notice); + await CoreNavigator.back(); + await CoreLoginHelper.passwordResetRequested(this.siteUrl); } } catch (error) { CoreDomUtils.showErrorModal(error); diff --git a/src/core/features/login/services/login-helper.ts b/src/core/features/login/services/login-helper.ts index a2e14d6fb..45595c83c 100644 --- a/src/core/features/login/services/login-helper.ts +++ b/src/core/features/login/services/login-helper.ts @@ -39,6 +39,8 @@ import { CorePushNotifications } from '@features/pushnotifications/services/push import { CoreText } from '@singletons/text'; import { CorePromisedValue } from '@classes/promised-value'; +const PASSWORD_RESETS_CONFIG_KEY = 'password-resets'; + /** * Helper provider that provides some common features regarding authentication. */ @@ -63,6 +65,13 @@ export class CoreLoginHelperProvider { this.logger = CoreLogger.getInstance('CoreLoginHelper'); } + /** + * Initialize service. + */ + async initialize(): Promise { + this.cleanUpPasswordResets(); + } + /** * Accept site policy. * @@ -183,6 +192,7 @@ export class CoreLoginHelperProvider { await CoreNavigator.navigate('/login/forgottenpassword', { params: { siteUrl, + siteConfig, username, }, }); @@ -1457,6 +1467,65 @@ export class CoreLoginHelperProvider { return []; } + /** + * Record that a password reset has been requested for a given site. + * + * @param siteUrl Site url. + */ + async passwordResetRequested(siteUrl: string): Promise { + const passwordResets = await this.getPasswordResets(); + + passwordResets[siteUrl] = Date.now(); + + await CoreConfig.set(PASSWORD_RESETS_CONFIG_KEY, JSON.stringify(passwordResets)); + } + + /** + * Find out if a password reset has been requested recently for a given site. + * + * @param siteUrl Site url. + * @return Whether a password reset has been requested recently. + */ + async wasPasswordResetRequestedRecently(siteUrl: string): Promise { + const passwordResets = await this.getPasswordResets(); + + return siteUrl in passwordResets + && passwordResets[siteUrl] > Date.now() - CoreConstants.MILLISECONDS_HOUR; + } + + /** + * Clean up expired password reset records from the database. + */ + async cleanUpPasswordResets(): Promise { + const passwordResets = await this.getPasswordResets(); + const siteUrls = Object.keys(passwordResets); + + for (const siteUrl of siteUrls) { + if (passwordResets[siteUrl] > Date.now() - CoreConstants.MILLISECONDS_HOUR) { + continue; + } + + delete passwordResets[siteUrl]; + } + + if (Object.values(passwordResets).length === 0) { + await CoreConfig.delete(PASSWORD_RESETS_CONFIG_KEY); + } else { + await CoreConfig.set(PASSWORD_RESETS_CONFIG_KEY, JSON.stringify(passwordResets)); + } + } + + /** + * Get a record indexing the last time a password reset was requested for a site. + * + * @returns Password resets. + */ + protected async getPasswordResets(): Promise> { + const passwordResetsJson = await CoreConfig.get(PASSWORD_RESETS_CONFIG_KEY, '{}'); + + return CoreTextUtils.parseJSON>(passwordResetsJson, {}); + } + } export const CoreLoginHelper = makeSingleton(CoreLoginHelperProvider); diff --git a/src/core/features/login/tests/behat/basic_usage-311.feature b/src/core/features/login/tests/behat/basic_usage-311.feature new file mode 100644 index 000000000..a08791ffb --- /dev/null +++ b/src/core/features/login/tests/behat/basic_usage-311.feature @@ -0,0 +1,19 @@ +@auth @core_auth @app @javascript @lms_upto3.11 +Feature: Test basic usage of login in app + I need basic login functionality to work + + Background: + Given the following "users" exist: + | username | firstname | lastname | + | student1 | david | student | + + Scenario: Forgot password + When I enter the app + And I press "Forgotten your username or password?" in the app + And I set the field "Enter either username or email address" to "student1" + And I press "Search" in the app + Then I should find "Success" in the app + + When I press "OK" in the app + And I press "Forgotten your username or password?" in the app + Then I should not find "Contact support" in the app diff --git a/src/core/features/login/tests/behat/basic_usage.feature b/src/core/features/login/tests/behat/basic_usage.feature index 07b6e8f19..1f9d41c9a 100755 --- a/src/core/features/login/tests/behat/basic_usage.feature +++ b/src/core/features/login/tests/behat/basic_usage.feature @@ -139,3 +139,15 @@ Feature: Test basic usage of login in app When I press "Reconnect" in the app Then I should find "Acceptance test site" in the app + + @lms_from4.0 + Scenario: Forgot password + When I enter the app + And I press "Forgotten your username or password?" in the app + And I set the field "Enter either username or email address" to "student1" + And I press "Search" in the app + Then I should find "Success" in the app + + When I press "OK" in the app + And I press "Forgotten your username or password?" in the app + Then I should find "Contact support" in the app