MOBILE-2253 login: Implement credentials page

main
Dani Palou 2017-11-27 11:57:52 +01:00
parent a4adffe94d
commit 0f087326d4
7 changed files with 438 additions and 3 deletions

View File

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

View File

@ -0,0 +1,53 @@
<ion-header>
<ion-navbar>
<ion-title>{{ 'mm.login.login' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding class="mm-center-view">
<core-loading [hideUntil]="pageLoaded" class="mm-view-content">
<ion-list no-lines class="box">
<ion-item text-wrap class="text-center">
<!-- Show site logo or a default image. -->
<img *ngIf="logoUrl" [src]="logoUrl" role="presentation">
<img *ngIf="!logoUrl" src="assets/img/logo.png" class="moodle-logo" role="presentation">
<!-- If no sitename show big siteurl. -->
<p *ngIf="!siteName" padding class="item-heading mm-siteurl">{{siteUrl}}</p>
<!-- If sitename, show big sitename and small siteurl. -->
<p *ngIf="siteName" padding class="item-heading mm-sitename">{{siteName}}</p>
<p *ngIf="siteName" class="mm-siteurl">{{siteUrl}}</p>
</ion-item>
<form [formGroup]="credForm" (ngSubmit)="login()">
<ion-item *ngIf="siteChecked && !isBrowserSSO">
<ion-input type="text" name="username" placeholder="{{ 'mm.login.username' | translate }}" formControlName="username" autocapitalize="none" autocorrect="off"></ion-input>
</ion-item>
<ion-item *ngIf="siteChecked && !isBrowserSSO">
<ion-input class="mm-ioninput-password" name="password" type="password" placeholder="{{ 'mm.login.password' | translate }}" formControlName="password" mm-show-password></ion-input>
</ion-item>
<button ion-button block color="primary" [disabled]="siteChecked && !isBrowserSSO && !credForm.valid">{{ 'mm.login.loginbutton' | translate }}</button>
</form>
<!-- Forgotten password button. -->
<div padding-top>
<button ion-button block (click)="forgottenPassword()">{{ 'mm.login.forgotten' | translate }}</button>
</div>
<div *ngIf="identityProviders && identityProviders.length" padding-top>
<p>{{ 'mm.login.potentialidps' | translate }}</p>
<ion-item *ngFor="let provider of identityProviders" text-wrap class="mm-oauth-icon" (click)="oauthClicked(provider)" title="{{provider.name}}">
<img [src]="provider.iconurl" alt="{{provider.name}}">
<span>{{provider.name}}</span>
<ion-icon class="icon-accessory" name="arrow-forward" md="ios-arrow-forward" item-end></ion-icon>
</ion-item>
</div>
<div *ngIf="canSignup">
<ion-item text-wrap>
<p class="item-heading">{{ 'mm.login.firsttime' | translate }}</p>
<p *ngIf="authInstructions"><core-format-text text="{{authInstructions}}"></core-format-text></p>
</ion-item>
<button ion-button block (click)="signup()">{{ 'mm.login.startsignup' | translate }}</button>
</div>
</ion-list>
</core-loading>
</ion-content>

View File

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

View File

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

View File

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

View File

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

View File

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