From 317444049c88b5ed90c9a53a2867867addbb06cb Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 19 Oct 2020 15:37:16 +0200 Subject: [PATCH] MOBILE-3565 login: Initial implementation of credentials page --- src/app/components/components.module.ts | 10 +- src/app/core/login/login-routing.module.ts | 5 + src/app/core/login/login.module.ts | 4 + .../login/pages/credentials/credentials.html | 75 ++++ .../pages/credentials/credentials.page.ts | 326 ++++++++++++++++++ src/app/core/login/pages/site/site.page.ts | 5 + src/app/core/login/services/helper.ts | 6 +- src/app/services/ws.ts | 4 +- 8 files changed, 429 insertions(+), 6 deletions(-) create mode 100644 src/app/core/login/pages/credentials/credentials.html create mode 100644 src/app/core/login/pages/credentials/credentials.page.ts diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index dfb7b2b51..afc95a694 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -13,11 +13,15 @@ // limitations under the License. import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; import { CoreIconComponent } from './icon/icon'; import { CoreLoadingComponent } from './loading/loading'; import { CoreShowPasswordComponent } from './show-password/show-password'; +import { CoreDirectivesModule } from '@app/directives/directives.module'; +import { CorePipesModule } from '@app/pipes/pipes.module'; @NgModule({ declarations: [ @@ -26,7 +30,11 @@ import { CoreShowPasswordComponent } from './show-password/show-password'; CoreShowPasswordComponent, ], imports: [ - IonicModule, + CommonModule, + IonicModule.forRoot(), + TranslateModule.forChild(), + CoreDirectivesModule, + CorePipesModule, ], exports: [ CoreIconComponent, diff --git a/src/app/core/login/login-routing.module.ts b/src/app/core/login/login-routing.module.ts index f71d29648..775de336d 100644 --- a/src/app/core/login/login-routing.module.ts +++ b/src/app/core/login/login-routing.module.ts @@ -15,6 +15,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { CoreLoginCredentialsPage } from './pages/credentials/credentials.page'; import { CoreLoginInitPage } from './pages/init/init.page'; import { CoreLoginSitePage } from './pages/site/site.page'; @@ -27,6 +28,10 @@ const routes: Routes = [ path: 'site', component: CoreLoginSitePage, }, + { + path: 'credentials', + component: CoreLoginCredentialsPage, + }, ]; @NgModule({ diff --git a/src/app/core/login/login.module.ts b/src/app/core/login/login.module.ts index f3e05da4d..0c18f5430 100644 --- a/src/app/core/login/login.module.ts +++ b/src/app/core/login/login.module.ts @@ -20,8 +20,10 @@ import { IonicModule } from '@ionic/angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@/app/components/components.module'; +import { CoreDirectivesModule } from '@/app/directives/directives.module'; import { CoreLoginRoutingModule } from './login-routing.module'; +import { CoreLoginCredentialsPage } from './pages/credentials/credentials.page'; import { CoreLoginInitPage } from './pages/init/init.page'; import { CoreLoginSitePage } from './pages/site/site.page'; import { CoreLoginHelperProvider } from './services/helper'; @@ -35,8 +37,10 @@ import { CoreLoginHelperProvider } from './services/helper'; FormsModule, ReactiveFormsModule, CoreComponentsModule, + CoreDirectivesModule, ], declarations: [ + CoreLoginCredentialsPage, CoreLoginInitPage, CoreLoginSitePage, ], diff --git a/src/app/core/login/pages/credentials/credentials.html b/src/app/core/login/pages/credentials/credentials.html new file mode 100644 index 000000000..13d91cb9d --- /dev/null +++ b/src/app/core/login/pages/credentials/credentials.html @@ -0,0 +1,75 @@ + + + + + + + {{ 'core.login.login' | translate }} + + + + + + + + +
+ + +

+

{{siteUrl}}

+
+ + + + + + + + + +
+
diff --git a/src/app/core/login/pages/credentials/credentials.page.ts b/src/app/core/login/pages/credentials/credentials.page.ts new file mode 100644 index 000000000..57fa2913e --- /dev/null +++ b/src/app/core/login/pages/credentials/credentials.page.ts @@ -0,0 +1,326 @@ +// (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 { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { NavController } from '@ionic/angular'; + +import { CoreApp } from '@services/app'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreLoginHelper, CoreLoginHelperProvider } from '@core/login/services/helper'; +import CoreConfigConstants from '@app/config.json'; +import { Translate } from '@singletons/core.singletons'; +import { CoreSiteIdentityProvider, CoreSitePublicConfigResponse } from '@/app/classes/site'; +import { CoreEvents, CoreEventsProvider } from '@/app/services/events'; + +/** + * Page that displays a "splash screen" while the app is being initialized. + */ +@Component({ + selector: 'page-core-login-credentials', + templateUrl: 'credentials.html', +}) +export class CoreLoginCredentialsPage implements OnInit, OnDestroy { + + @ViewChild('credentialsForm') formElement: ElementRef; + + credForm: FormGroup; + siteUrl: string; + siteChecked = false; + siteName: string; + logoUrl: string; + authInstructions: string; + canSignup: boolean; + identityProviders: CoreSiteIdentityProvider[]; + pageLoaded = false; + isBrowserSSO = false; + isFixedUrlSet = false; + showForgottenPassword = true; + showScanQR: boolean; + + protected siteConfig: CoreSitePublicConfigResponse; + protected eventThrown = false; + protected viewLeft = false; + protected siteId: string; + protected urlToOpen: string; + + constructor( + protected fb: FormBuilder, + protected route: ActivatedRoute, + protected navCtrl: NavController, + ) { + + const canScanQR = CoreUtils.instance.canScanQR(); + if (canScanQR) { + if (typeof CoreConfigConstants['displayqroncredentialscreen'] == 'undefined') { + this.showScanQR = CoreLoginHelper.instance.isFixedUrlSet(); + } else { + this.showScanQR = !!CoreConfigConstants['displayqroncredentialscreen']; + } + } else { + this.showScanQR = false; + } + } + + /** + * Initialize the component. + */ + ngOnInit(): void { + this.route.queryParams.subscribe(params => { + this.siteUrl = params['siteUrl']; + this.siteName = params['siteName'] || null; + this.logoUrl = !CoreConfigConstants.forceLoginLogo && params['logoUrl'] || null; + this.siteConfig = params['siteConfig']; + this.urlToOpen = params['urlToOpen']; + + this.credForm = this.fb.group({ + username: [params['username'] || '', Validators.required], + password: ['', Validators.required], + }); + }); + + this.treatSiteConfig(); + this.isFixedUrlSet = CoreLoginHelper.instance.isFixedUrlSet(); + + if (this.isFixedUrlSet) { + // Fixed URL, we need to check if it uses browser SSO login. + this.checkSite(this.siteUrl); + } else { + this.siteChecked = true; + this.pageLoaded = true; + } + } + + /** + * Check if a site uses local_mobile, requires SSO login, etc. + * This should be used only if a fixed URL is set, otherwise this check is already performed in CoreLoginSitePage. + * + * @param siteUrl Site URL to check. + * @return Promise resolved when done. + */ + protected async checkSite(siteUrl: string): Promise { + this.pageLoaded = false; + + // If the site is configured with http:// protocol we force that one, otherwise we use default mode. + const protocol = siteUrl.indexOf('http://') === 0 ? 'http://' : undefined; + + try { + const result = await CoreSites.instance.checkSite(siteUrl, protocol); + + this.siteChecked = true; + this.siteUrl = result.siteUrl; + + this.siteConfig = result.config; + this.treatSiteConfig(); + + if (result && result.warning) { + CoreDomUtils.instance.showErrorModal(result.warning, true, 4000); + } + + if (CoreLoginHelper.instance.isSSOLoginNeeded(result.code)) { + // SSO. User needs to authenticate in a browser. + this.isBrowserSSO = true; + + // Check that there's no SSO authentication ongoing and the view hasn't changed. + if (!CoreApp.instance.isSSOAuthenticationOngoing() && !this.viewLeft) { + CoreLoginHelper.instance.confirmAndOpenBrowserForSSOLogin( + result.siteUrl, result.code, result.service, result.config?.launchurl); + } + } else { + this.isBrowserSSO = false; + } + + } catch (error) { + CoreDomUtils.instance.showErrorModal(error); + } finally { + this.pageLoaded = true; + } + } + + /** + * Treat the site configuration (if it exists). + */ + protected treatSiteConfig(): void { + if (this.siteConfig) { + this.siteName = CoreConfigConstants.sitename ? CoreConfigConstants.sitename : this.siteConfig.sitename; + this.logoUrl = CoreLoginHelper.instance.getLogoUrl(this.siteConfig); + this.authInstructions = this.siteConfig.authinstructions || Translate.instance.instant('core.login.loginsteps'); + + const disabledFeatures = CoreLoginHelper.instance.getDisabledFeatures(this.siteConfig); + this.identityProviders = CoreLoginHelper.instance.getValidIdentityProviders(this.siteConfig, disabledFeatures); + this.canSignup = this.siteConfig.registerauth == 'email' && + !CoreLoginHelper.instance.isEmailSignupDisabled(this.siteConfig, disabledFeatures); + this.showForgottenPassword = !CoreLoginHelper.instance.isForgottenPasswordDisabled(this.siteConfig, disabledFeatures); + + if (!this.eventThrown && !this.viewLeft) { + this.eventThrown = true; + CoreEvents.instance.trigger(CoreEventsProvider.LOGIN_SITE_CHECKED, { config: this.siteConfig }); + } + } else { + this.authInstructions = null; + this.canSignup = false; + this.identityProviders = []; + } + } + + /** + * Tries to authenticate the user. + * + * @param e Event. + * @return Promise resolved when done. + */ + async login(e?: Event): Promise { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } + + CoreApp.instance.closeKeyboard(); + + // Get input data. + const siteUrl = this.siteUrl; + const username = this.credForm.value.username; + const password = this.credForm.value.password; + + if (!this.siteChecked || this.isBrowserSSO) { + // Site wasn't checked (it failed) or a previous check determined it was SSO. Let's check again. + await this.checkSite(siteUrl); + + if (!this.isBrowserSSO) { + // Site doesn't use browser SSO, throw app's login again. + return this.login(); + } + + return; + } + + if (!username) { + CoreDomUtils.instance.showErrorModal('core.login.usernamerequired', true); + + return; + } + if (!password) { + CoreDomUtils.instance.showErrorModal('core.login.passwordrequired', true); + + return; + } + + if (!CoreApp.instance.isOnline()) { + CoreDomUtils.instance.showErrorModal('core.networkerrormsg', true); + + return; + } + + const modal = await CoreDomUtils.instance.showModalLoading(); + + // Start the authentication process. + try { + const data = await CoreSites.instance.getUserToken(siteUrl, username, password); + + const id = await CoreSites.instance.newSite(data.siteUrl, data.token, data.privateToken); + + // Reset fields so the data is not in the view anymore. + this.credForm.controls['username'].reset(); + this.credForm.controls['password'].reset(); + + this.siteId = id; + + await CoreLoginHelper.instance.goToSiteInitialPage(undefined, undefined, undefined, undefined, this.urlToOpen); + } catch (error) { + CoreLoginHelper.instance.treatUserTokenError(siteUrl, error, username, password); + + if (error.loggedout) { + // @todo Go to sites page. + } else if (error.errorcode == 'forcepasswordchangenotice') { + // Reset password field. + this.credForm.controls.password.reset(); + } + } finally { + modal.dismiss(); + + CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, true); + } + } + + /** + * Forgotten password button clicked. + */ + forgottenPassword(): void { + CoreLoginHelper.instance.forgottenPasswordClicked( + this.navCtrl, this.siteUrl, this.credForm.value.username, this.siteConfig); + } + + /** + * An OAuth button was clicked. + * + * @param provider The provider that was clicked. + */ + oauthClicked(provider: CoreSiteIdentityProvider): void { + if (!CoreLoginHelper.instance.openBrowserForOAuthLogin(this.siteUrl, provider, this.siteConfig.launchurl)) { + CoreDomUtils.instance.showErrorModal('Invalid data.'); + } + } + + /** + * Signup button was clicked. + */ + signup(): void { + // @todo Go to signup. + } + + /** + * Show instructions and scan QR code. + */ + showInstructionsAndScanQR(): void { + // Show some instructions first. + CoreDomUtils.instance.showAlertWithOptions({ + header: Translate.instance.instant('core.login.faqwhereisqrcode'), + message: Translate.instance.instant('core.login.faqwhereisqrcodeanswer', + { $image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML }), + buttons: [ + { + text: Translate.instance.instant('core.cancel'), + role: 'cancel', + }, + { + text: Translate.instance.instant('core.next'), + handler: (): void => { + this.scanQR(); + }, + }, + ], + }); + } + + /** + * Scan a QR code and put its text in the URL input. + * + * @return Promise resolved when done. + */ + async scanQR(): Promise { + // @todo Scan for a QR code. + } + + /** + * View destroyed. + */ + ngOnDestroy(): void { + this.viewLeft = true; + CoreEvents.instance.trigger(CoreEventsProvider.LOGIN_SITE_UNCHECKED, { config: this.siteConfig }, this.siteId); + } + +} diff --git a/src/app/core/login/pages/site/site.page.ts b/src/app/core/login/pages/site/site.page.ts index 8183d1e68..465181352 100644 --- a/src/app/core/login/pages/site/site.page.ts +++ b/src/app/core/login/pages/site/site.page.ts @@ -28,6 +28,7 @@ import CoreConfigConstants from '@app/config.json'; import { Translate } from '@singletons/core.singletons'; import { CoreUrl } from '@singletons/url'; import { CoreUrlUtils } from '@/app/services/utils/url'; +import { NavController } from '@ionic/angular'; /** * Page that displays a "splash screen" while the app is being initialized. @@ -58,6 +59,7 @@ export class CoreLoginSitePage implements OnInit { constructor( protected route: ActivatedRoute, protected formBuilder: FormBuilder, + protected navCtrl: NavController, ) { let url = ''; @@ -368,6 +370,9 @@ export class CoreLoginSitePage implements OnInit { } // @todo Navigate to credentials. + this.navCtrl.navigateForward('/login/credentials', { + queryParams: pageParams, + }); } } diff --git a/src/app/core/login/services/helper.ts b/src/app/core/login/services/helper.ts index c37c2807c..a4256866e 100644 --- a/src/app/core/login/services/helper.ts +++ b/src/app/core/login/services/helper.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { Location } from '@angular/common'; -import { Router, Params } from '@angular/router'; +import { Params } from '@angular/router'; import { NavController } from '@ionic/angular'; import { Md5 } from 'ts-md5/dist/md5'; @@ -55,7 +55,6 @@ export class CoreLoginHelperProvider { waitingForBrowser = false; constructor( - protected router: Router, private location: Location, ) { this.logger = CoreLogger.getInstance('CoreLoginHelper'); @@ -162,6 +161,7 @@ export class CoreLoginHelperProvider { * @param siteConfig Site config. */ async forgottenPasswordClicked( + navCtrl: NavController, siteUrl: string, username: string, siteConfig?: CoreSitePublicConfigResponse, @@ -180,7 +180,7 @@ export class CoreLoginHelperProvider { const canReset = await this.canRequestPasswordReset(siteUrl); if (canReset) { - await this.router.navigate(['/login/forgottenpassword'], { + await navCtrl.navigateForward(['/login/forgottenpassword'], { queryParams: { siteUrl, username, diff --git a/src/app/services/ws.ts b/src/app/services/ws.ts index 51145b979..a97eccea7 100644 --- a/src/app/services/ws.ts +++ b/src/app/services/ws.ts @@ -113,9 +113,9 @@ export class CoreWSProvider { if (this.retryCalls.length > 0) { this.logger.warn('Calls locked, trying later...'); - return this.addToRetryQueue(method, siteUrl, data, preSets); + return this.addToRetryQueue(method, siteUrl, dataToSend, preSets); } else { - return this.performPost(method, siteUrl, data, preSets); + return this.performPost(method, siteUrl, dataToSend, preSets); } }