MOBILE-4059 login: Improve forgotten password

Suggest contacting support if password was already reset recently
main
Noel De Martin 2022-10-05 13:48:48 +02:00
parent 28fd894aab
commit d859af122e
10 changed files with 127 additions and 48 deletions

View File

@ -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",

View File

@ -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.",

View File

@ -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 {}

View File

@ -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();
},
},
],

View File

@ -11,6 +11,10 @@
</ion-header>
<ion-content>
<div class="list-item-limited-width">
<core-login-exceeded-attempts *ngIf="wasPasswordResetRequestedRecently" [siteConfig]="siteConfig" [siteUrl]="siteUrl"
[supportSubject]="'core.login.exceededpasswordresetattemptssupportsubject' | translate">
{{ 'core.login.exceededpasswordresetattempts' | translate }}
</core-login-exceeded-attempts>
<ion-list>
<ion-item class="ion-text-wrap">

View File

@ -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 {}

View File

@ -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<void> {
const siteUrl = CoreNavigator.getRouteParam<string>('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<CoreSitePublicConfigResponse>('siteConfig');
this.autoFocus = CorePlatform.is('tablet');
this.myForm = this.formBuilder.group({
field: ['username', Validators.required],
value: [CoreNavigator.getRouteParam<string>('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);

View File

@ -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<void> {
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<void> {
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<boolean> {
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<void> {
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<Record<string, number>> {
const passwordResetsJson = await CoreConfig.get(PASSWORD_RESETS_CONFIG_KEY, '{}');
return CoreTextUtils.parseJSON<Record<string, number>>(passwordResetsJson, {});
}
}
export const CoreLoginHelper = makeSingleton(CoreLoginHelperProvider);

View File

@ -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

View File

@ -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