MOBILE-4059 login: Improve forgotten password
Suggest contacting support if password was already reset recentlymain
parent
28fd894aab
commit
d859af122e
|
@ -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",
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 {}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue