MOBILE-2253 login: Implement credentials page
parent
a4adffe94d
commit
0f087326d4
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 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 = {};
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue