MOBILE-2253 login: Implement credentials page
parent
a4adffe94d
commit
0f087326d4
|
@ -65,13 +65,46 @@
|
||||||
.mm-center-view .scroll-content {
|
.mm-center-view .scroll-content {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
display: table !important;
|
/* display: table !important; */
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
.mm-view-content {
|
.mm-view-content {
|
||||||
display: table-cell;
|
/* display: table-cell;
|
||||||
vertical-align: middle;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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});
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,6 +41,8 @@ export class CoreEventsProvider {
|
||||||
public static PACKAGE_STATUS_CHANGED = 'package_status_changed';
|
public static PACKAGE_STATUS_CHANGED = 'package_status_changed';
|
||||||
public static SECTION_STATUS_CHANGED = 'section_status_changed';
|
public static SECTION_STATUS_CHANGED = 'section_status_changed';
|
||||||
public static REMOTE_ADDONS_LOADED = 'remote_addons_loaded';
|
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;
|
logger;
|
||||||
observables = {};
|
observables = {};
|
||||||
|
|
|
@ -126,6 +126,10 @@ $colors: (
|
||||||
// Moodle Mobile variables
|
// Moodle Mobile variables
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
// Variables copied from Ionic 1.
|
||||||
|
$item-icon-accessory-color: #ccc !default;
|
||||||
|
$item-icon-accessory-font-size: 16px !default;
|
||||||
|
|
||||||
// Init screen.
|
// Init screen.
|
||||||
$mm-color-init-screen: #ff5c00;
|
$mm-color-init-screen: #ff5c00;
|
||||||
$mm-color-init-screen-alt: #ff7600;
|
$mm-color-init-screen-alt: #ff7600;
|
||||||
|
|
Loading…
Reference in New Issue