From 0f087326d4273c30ac7909a1046667129d9ae9bc Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 27 Nov 2017 11:57:52 +0100 Subject: [PATCH] MOBILE-2253 login: Implement credentials page --- src/app/app.scss | 39 ++- .../login/pages/credentials/credentials.html | 53 ++++ .../pages/credentials/credentials.module.ts | 35 +++ .../login/pages/credentials/credentials.scss | 34 +++ .../login/pages/credentials/credentials.ts | 274 ++++++++++++++++++ src/providers/events.ts | 2 + src/theme/variables.scss | 4 + 7 files changed, 438 insertions(+), 3 deletions(-) create mode 100644 src/core/login/pages/credentials/credentials.html create mode 100644 src/core/login/pages/credentials/credentials.module.ts create mode 100644 src/core/login/pages/credentials/credentials.scss create mode 100644 src/core/login/pages/credentials/credentials.ts diff --git a/src/app/app.scss b/src/app/app.scss index e4bb994a4..8442bfc77 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -65,13 +65,46 @@ .mm-center-view .scroll-content { margin: 0 auto; max-width: 600px; - display: table !important; + /* display: table !important; */ width: 100% !important; height: 100% !important; .mm-view-content { - display: table-cell; - vertical-align: middle; + /* display: table-cell; + vertical-align: middle; */ + display: block; // Added this style and commented some others to make scroll work. Box isn't centered vertically. } } } +// Define an alternative way to set a heading in an item without using a heading tag. +// This is done for accessibility reasons when a heading is semantically incorrect. +.item .item-heading { + @extend h6; +} + +.mm-oauth-icon, .item.mm-oauth-icon, .list .item.mm-oauth-icon { + padding: ($content-padding / 2); + border: 1px solid $list-border-color; + + img, span { + max-height: 32px; + vertical-align: middle; + } + img { + max-width: 32px; + } + span { + margin-left: 5px; + color: $gray-darker; + } + + .label { + margin: 0; + } +} + +.icon-accessory, +ion-icon.icon-accessory { + color: $item-icon-accessory-color; + font-size: $item-icon-accessory-font-size; +} diff --git a/src/core/login/pages/credentials/credentials.html b/src/core/login/pages/credentials/credentials.html new file mode 100644 index 000000000..e8d899933 --- /dev/null +++ b/src/core/login/pages/credentials/credentials.html @@ -0,0 +1,53 @@ + + + {{ 'mm.login.login' | translate }} + + + + + + + + + + + +

{{siteUrl}}

+ +

{{siteName}}

+

{{siteUrl}}

+
+
+ + + + + + + +
+ + +
+ +
+ +
+

{{ 'mm.login.potentialidps' | translate }}

+ + {{provider.name}} + {{provider.name}} + + +
+ +
+ +

{{ 'mm.login.firsttime' | translate }}

+

+
+ +
+
+
+
diff --git a/src/core/login/pages/credentials/credentials.module.ts b/src/core/login/pages/credentials/credentials.module.ts new file mode 100644 index 000000000..c69b2b8b4 --- /dev/null +++ b/src/core/login/pages/credentials/credentials.module.ts @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicPageModule } from 'ionic-angular'; +import { CoreLoginCredentialsPage } from './credentials'; +import { CoreLoginModule } from '../../login.module'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreDirectivesModule } from '../../../../directives/directives.module'; + +@NgModule({ + declarations: [ + CoreLoginCredentialsPage + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CoreLoginModule, + IonicPageModule.forChild(CoreLoginCredentialsPage), + TranslateModule.forChild() + ] +}) +export class CoreLoginCredentialsPageModule {} diff --git a/src/core/login/pages/credentials/credentials.scss b/src/core/login/pages/credentials/credentials.scss new file mode 100644 index 000000000..c708e6271 --- /dev/null +++ b/src/core/login/pages/credentials/credentials.scss @@ -0,0 +1,34 @@ +page-core-login-credentials { + .content { + .mm-ioninput-password { + padding-top: 0; + padding-bottom: 0; + } + background: -webkit-radial-gradient(white, $gray-light); + background: radial-gradient(white, $gray-light); + + img { + max-width: 100%; + } + + img.moodle-logo { + width: 90%; + max-width: 300px; + } + + .box { + padding: 16px; + background: $white; + border: 1px solid $gray; + } + + .mm-sitename, .mm-siteurl { + @if $mm-fixed-url { display: none; } + } + + .list .item-input { + border: 1px solid $list-border-color; + margin-bottom: 20px; + } + } +} diff --git a/src/core/login/pages/credentials/credentials.ts b/src/core/login/pages/credentials/credentials.ts new file mode 100644 index 000000000..64b814940 --- /dev/null +++ b/src/core/login/pages/credentials/credentials.ts @@ -0,0 +1,274 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 } from '@angular/core'; +import { IonicPage, NavController, NavParams } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreAppProvider } from '../../../../providers/app'; +import { CoreEventsProvider } from '../../../../providers/events'; +import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreUtilsProvider } from '../../../../providers/utils/utils'; +import { CoreLoginHelperProvider } from '../../providers/helper'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +/** + * Page to enter the user credentials. + */ +@IonicPage() +@Component({ + selector: 'page-core-login-credentials', + templateUrl: 'credentials.html', +}) +export class CoreLoginCredentialsPage { + credForm: FormGroup; + siteUrl: string; + siteChecked = false; + siteName: string; + logoUrl: string; + authInstructions: string; + canSignup: boolean; + identityProviders: any[]; + pageLoaded = false; + isBrowserSSO = false; + + protected siteConfig; + protected eventThrown = false; + protected viewLeft = false; + protected siteId: string; + protected urlToOpen: string; + + constructor(private navCtrl: NavController, navParams: NavParams, fb: FormBuilder, private appProvider: CoreAppProvider, + private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider, + private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private utils: CoreUtilsProvider, + private eventsProvider: CoreEventsProvider) { + + this.siteUrl = navParams.get('siteUrl'); + this.siteConfig = navParams.get('siteConfig'); + this.urlToOpen = navParams.get('urlToOpen'); + + this.credForm = fb.group({ + 'username': [navParams.get('username') || '', Validators.required], + 'password': ['', Validators.required] + }); + } + + /** + * View loaded. + */ + ionViewDidLoad() { + this.treatSiteConfig(); + + if (this.loginHelper.isFixedUrlSet()) { + // Fixed URL, we need to check if it uses browser SSO login. + this.checkSite(this.siteUrl); + } else { + this.siteChecked = true; + this.pageLoaded = true; + } + } + + /** + * View left. + */ + ionViewDidLeave() { + this.viewLeft = true; + this.eventsProvider.trigger(CoreEventsProvider.LOGIN_SITE_UNCHECKED, { + siteId: this.siteId, + config: this.siteConfig + }); + } + + /** + * 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 {string} siteUrl Site URL to check. + */ + protected checkSite(siteUrl: string) { + 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; + return this.sitesProvider.checkSite(siteUrl, protocol).then((result) => { + + this.siteChecked = true; + this.siteUrl = result.siteUrl; + + this.siteConfig = result.config; + this.treatSiteConfig(); + + if (result && result.warning) { + this.domUtils.showErrorModal(result.warning, true, 4000); + } + + if (this.loginHelper.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 (!this.appProvider.isSSOAuthenticationOngoing() && !this.viewLeft) { + this.loginHelper.confirmAndOpenBrowserForSSOLogin( + result.siteUrl, result.code, result.service, result.config && result.config.launchurl); + } + } else { + this.isBrowserSSO = false; + } + + }).catch((error) => { + this.domUtils.showErrorModal(error); + }).finally(() => { + this.pageLoaded = true; + }); + } + + /** + * Treat the site configuration (if it exists). + */ + protected treatSiteConfig() : void { + if (this.siteConfig) { + this.siteName = this.siteConfig.sitename; + this.logoUrl = this.siteConfig.logourl || this.siteConfig.compactlogourl; + this.authInstructions = this.siteConfig.authinstructions || this.translate.instant('mm.login.loginsteps'); + this.canSignup = this.siteConfig.registerauth == 'email' && !this.loginHelper.isEmailSignupDisabled(this.siteConfig); + this.identityProviders = this.loginHelper.getValidIdentityProviders(this.siteConfig); + + if (!this.eventThrown && !this.viewLeft) { + this.eventThrown = true; + this.eventsProvider.trigger(CoreEventsProvider.LOGIN_SITE_CHECKED, { + config: this.siteConfig + }); + } + } else { + this.siteName = null; + this.logoUrl = null; + this.authInstructions = null; + this.canSignup = false; + this.identityProviders = []; + } + } + + /** + * Tries to authenticate the user. + */ + login() : void { + this.appProvider.closeKeyboard(); + + // Get input data. + let siteUrl = this.siteUrl, + username = this.credForm.value.username, + 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. + this.checkSite(siteUrl).then(() => { + if (!this.isBrowserSSO) { + // Site doesn't use browser SSO, throw app's login again. + return this.login(); + } + }); + return; + } + + if (!username) { + this.domUtils.showErrorModal('mm.login.usernamerequired', true); + return; + } + if (!password) { + this.domUtils.showErrorModal('mm.login.passwordrequired', true); + return; + } + + if (!this.appProvider.isOnline()) { + this.domUtils.showErrorModal('mm.core.networkerrormsg', true); + return; + } + + let modal = this.domUtils.showModalLoading(); + + // Start the authentication process. + this.sitesProvider.getUserToken(siteUrl, username, password).then((data) => { + return this.sitesProvider.newSite(data.siteUrl, data.token, data.privateToken).then((id) => { + // Reset fields so the data is not in the view anymore. + this.credForm.controls['username'].reset(); + this.credForm.controls['password'].reset(); + + this.siteId = id; + + if (this.urlToOpen) { + // There's a content link to open. + // @todo: Implement this once content links delegate is implemented. + // return $mmContentLinksDelegate.getActionsFor(urlToOpen, undefined, username).then((actions) => { + // action = $mmContentLinksHelper.getFirstValidAction(actions); + // if (action && action.sites.length) { + // // Action should only have 1 site because we're filtering by username. + // action.action(action.sites[0]); + // } else { + // return $mmLoginHelper.goToSiteInitialPage(); + // } + // }); + } else { + return this.loginHelper.goToSiteInitialPage(this.navCtrl, true); + } + }); + }).catch((error) => { + this.loginHelper.treatUserTokenError(siteUrl, error); + }).finally(() => { + modal.dismiss(); + }); + } + + /** + * Forgotten password button clicked. + */ + forgottenPassword() : void { + if (this.siteConfig && this.siteConfig.forgottenpasswordurl) { + // URL set, open it. + this.utils.openInApp(this.siteConfig.forgottenpasswordurl); + return; + } + + // Check if password reset can be done through the app. + let modal = this.domUtils.showModalLoading(); + this.loginHelper.canRequestPasswordReset(this.siteUrl).then((canReset) => { + if (canReset) { + this.navCtrl.push('CoreLoginForgottenPasswordPage', { + siteUrl: this.siteUrl, username: this.credForm.value.username + }); + } else { + this.loginHelper.openForgottenPassword(this.siteUrl); + } + }).finally(() => { + modal.dismiss(); + }); + } + + /** + * An OAuth button was clicked. + * + * @param {any} provider The provider that was clicked. + */ + oauthClicked(provider) : void { + if (!this.loginHelper.openBrowserForOAuthLogin(this.siteUrl, provider, this.siteConfig.launchurl)) { + this.domUtils.showErrorModal('Invalid data.'); + } + } + + /** + * Signup button was clicked. + */ + signup() : void { + this.navCtrl.push('CoreLoginEmailSignupPage', {siteUrl: this.siteUrl}); + } +} diff --git a/src/providers/events.ts b/src/providers/events.ts index a7f0261bd..779f103dd 100644 --- a/src/providers/events.ts +++ b/src/providers/events.ts @@ -41,6 +41,8 @@ export class CoreEventsProvider { public static PACKAGE_STATUS_CHANGED = 'package_status_changed'; public static SECTION_STATUS_CHANGED = 'section_status_changed'; public static REMOTE_ADDONS_LOADED = 'remote_addons_loaded'; + public static LOGIN_SITE_CHECKED = 'login_site_checked'; + public static LOGIN_SITE_UNCHECKED = 'login_site_unchecked'; logger; observables = {}; diff --git a/src/theme/variables.scss b/src/theme/variables.scss index 23fdf718a..7295c1f41 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -126,6 +126,10 @@ $colors: ( // Moodle Mobile variables // -------------------------------------------------- +// Variables copied from Ionic 1. +$item-icon-accessory-color: #ccc !default; +$item-icon-accessory-font-size: 16px !default; + // Init screen. $mm-color-init-screen: #ff5c00; $mm-color-init-screen-alt: #ff7600;