refractor: login module (#283)
* Refractor components. * Shift language and footer component to shared module. * Replace mat-icons by font-awesome icons. * Add comments. Fixes #284
This commit is contained in:
+13
-4
@@ -10,9 +10,9 @@
|
||||
],
|
||||
"selector-attribute-quotes": "always",
|
||||
"string-quotes": "double",
|
||||
"max-nesting-depth": 3,
|
||||
"selector-max-compound-selectors": 3,
|
||||
"selector-max-specificity": "0,3,2",
|
||||
"max-nesting-depth": 5,
|
||||
"selector-max-compound-selectors": 5,
|
||||
"selector-max-specificity": "1,3,2",
|
||||
"declaration-no-important": true,
|
||||
"at-rule-no-vendor-prefix": true,
|
||||
"media-feature-name-no-vendor-prefix": true,
|
||||
@@ -22,9 +22,18 @@
|
||||
"no-empty-source": null,
|
||||
"selector-class-pattern": "[a-z-]+",
|
||||
"selector-id-pattern": "[a-z-]+",
|
||||
"selector-max-id": 0,
|
||||
"selector-max-id": 1,
|
||||
"selector-no-qualifying-type": true,
|
||||
"selector-max-universal": 0,
|
||||
"selector-type-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignoreTypes": [
|
||||
"mat-spinner",
|
||||
"mat-radio-button"
|
||||
]
|
||||
}
|
||||
],
|
||||
"selector-pseudo-element-no-unknown": [
|
||||
true,
|
||||
{
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import { Injectable, EventEmitter } from '@angular/core';
|
||||
|
||||
export interface Alert {
|
||||
type: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AlertService {
|
||||
|
||||
public alertEvent: EventEmitter<any>;
|
||||
public alertEvent: EventEmitter<Alert>;
|
||||
|
||||
constructor() {
|
||||
this.alertEvent = new EventEmitter();
|
||||
}
|
||||
|
||||
alert(alertEvent: any) {
|
||||
alert(alertEvent: Alert) {
|
||||
this.alertEvent.emit(alertEvent);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,3 +11,4 @@ export * from './http/error-handler.interceptor';
|
||||
export * from './route.service';
|
||||
export * from './route-reusable-strategy';
|
||||
export * from './logger.service';
|
||||
export * from './alert.service';
|
||||
|
||||
@@ -1,27 +1,41 @@
|
||||
<form fxLayout="column" [formGroup]="loginForm" (ngSubmit)="login()">
|
||||
<mat-form-field fxFlexAlign="center" class="textfield">
|
||||
<span matPrefix><mat-icon>account_circle</mat-icon> </span>
|
||||
<input matInput placeholder="Username" autocomplete="off" formControlName="username">
|
||||
<form fxLayout="column" [formGroup]="loginForm" (ngSubmit)="login()" id="login-form">
|
||||
|
||||
<mat-form-field fxFlexAlign="center" class="login-input">
|
||||
<span matPrefix>
|
||||
<fa-icon icon="user-circle" size="lg"></fa-icon>
|
||||
</span>
|
||||
<mat-label>Username</mat-label>
|
||||
<input matInput type="text" autocomplete="off" formControlName="username">
|
||||
<mat-error *ngIf="loginForm.controls.username.hasError('required')">
|
||||
Username is <strong>required</strong>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlexAlign="center" class="textfield">
|
||||
<span matPrefix><mat-icon>lock</mat-icon> </span>
|
||||
<input matInput type="{{ passwordInputType }}" placeholder="Password" formControlName="password">
|
||||
|
||||
<mat-form-field fxFlexAlign="center" class="login-input">
|
||||
<span matPrefix>
|
||||
<fa-icon icon="lock" size="lg"></fa-icon>
|
||||
</span>
|
||||
<mat-label>Password</mat-label>
|
||||
<input matInput type="{{ passwordInputType }}" formControlName="password">
|
||||
<button mat-button *ngIf="loginForm.controls.password.value && !loading" matSuffix mat-icon-button
|
||||
aria-label="Clear" (mousedown)="passwordInputType = 'text'" (mouseup)="passwordInputType = 'password'">
|
||||
<mat-icon *ngIf="passwordInputType === 'password'">visibility</mat-icon>
|
||||
<mat-icon *ngIf="passwordInputType === 'text'">visibility_off</mat-icon>
|
||||
(mousedown)="passwordInputType = 'text'" (mouseup)="passwordInputType = 'password'">
|
||||
<fa-icon *ngIf="passwordInputType === 'password'" icon="eye"></fa-icon>
|
||||
<fa-icon *ngIf="passwordInputType === 'text'" icon="eye-slash"></fa-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="loginForm.controls.password.hasError('required')">
|
||||
Password is <strong>required</strong>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-checkbox fxFlexAlign="center" formControlName="remember" class="m-t-10">Remember me</mat-checkbox>
|
||||
<button mat-raised-button color="primary" fxFlexAlign="center" class="btn" [disabled]="!loginForm.valid">
|
||||
|
||||
<button mat-raised-button color="primary" fxFlexAlign="center" class="login-button" [disabled]="!loginForm.valid">
|
||||
Login
|
||||
<mat-spinner [diameter]="20" *ngIf="loading"></mat-spinner>
|
||||
</button>
|
||||
<button type="button" mat-button fxFlexAlign="center" class="btn" (click)="forgotPassword()" [disabled]="loading">Forgot Password?</button>
|
||||
|
||||
<button type="button" mat-button fxFlexAlign="center" class="login-button" (click)="forgotPassword()" [disabled]="loading">
|
||||
Forgot Password?
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
.textfield {
|
||||
width: 14rem;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
#login-form {
|
||||
.login-input {
|
||||
width: 14rem;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 14rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.login-button {
|
||||
width: 14rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
mat-spinner {
|
||||
z-index: 4;
|
||||
float: right;
|
||||
margin: 0.5rem 0;
|
||||
mat-spinner {
|
||||
float: right;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||
|
||||
/** rxjs Imports */
|
||||
import { finalize } from 'rxjs/operators';
|
||||
|
||||
import { Logger, AuthenticationService } from '../../core';
|
||||
|
||||
const log = new Logger('Login');
|
||||
/** Custom Services */
|
||||
import { AuthenticationService } from '../../core';
|
||||
|
||||
/**
|
||||
* Login form component.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-login-form',
|
||||
templateUrl: './login-form.component.html',
|
||||
@@ -14,37 +18,56 @@ const log = new Logger('Login');
|
||||
})
|
||||
export class LoginFormComponent implements OnInit {
|
||||
|
||||
/** Login form group. */
|
||||
loginForm: FormGroup;
|
||||
/** Password input field type. */
|
||||
passwordInputType: string;
|
||||
error: string;
|
||||
/** True if loading. */
|
||||
loading = false;
|
||||
authenticationEvent: any;
|
||||
|
||||
/**
|
||||
* @param {FormBuilder} formBuilder Form Builder.
|
||||
* @param {AuthenticationService} authenticationService Authentication Service.
|
||||
*/
|
||||
constructor(private formBuilder: FormBuilder,
|
||||
private authenticationService: AuthenticationService) { }
|
||||
|
||||
/**
|
||||
* Creates login form.
|
||||
*
|
||||
* Initializes password input field type.
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.createLoginForm();
|
||||
this.passwordInputType = 'password';
|
||||
}
|
||||
|
||||
public login() {
|
||||
/**
|
||||
* Authenticates the user if the credentials are valid.
|
||||
*/
|
||||
login() {
|
||||
this.loading = true;
|
||||
this.loginForm.disable();
|
||||
this.authenticationService.login(this.loginForm.value)
|
||||
.pipe(finalize(() => {
|
||||
this.loginForm.reset();
|
||||
this.loginForm.markAsPristine();
|
||||
// Bug: Validation errors won't get removed on reset
|
||||
// Angular Material Bug: Validation errors won't get removed on reset.
|
||||
this.loginForm.enable();
|
||||
this.loading = false;
|
||||
})).subscribe();
|
||||
}
|
||||
|
||||
public forgotPassword() {
|
||||
console.log('TODO: Forgot Password');
|
||||
/**
|
||||
* TODO: Decision to be taken on providing this feature.
|
||||
*/
|
||||
forgotPassword() {
|
||||
console.log('Forgot Password feature currently unavailable.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates login form.
|
||||
*/
|
||||
private createLoginForm() {
|
||||
this.loginForm = this.formBuilder.group({
|
||||
'username': ['', Validators.required],
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
/** Angular Imports */
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
/** Transalation Imports */
|
||||
import { extract } from '../core';
|
||||
|
||||
/** Custom Components */
|
||||
import { LoginComponent } from './login.component';
|
||||
|
||||
/** Login Routes */
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'login',
|
||||
@@ -12,6 +17,11 @@ const routes: Routes = [
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Login Routing Module
|
||||
*
|
||||
* Configures the login routes.
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
|
||||
@@ -1,78 +1,88 @@
|
||||
<!-- The viewport is divided into two parts having max-width 70% and 30% -->
|
||||
<div fxLayout="row">
|
||||
<!-- I want a row divided into 2 parts 70% and 30% -->
|
||||
|
||||
<!-- In 70% we have a cover pic, org/app name, description, contact information -->
|
||||
<!-- Column disappers on small screens -->
|
||||
<!--
|
||||
Content in 70%: (TODO: Allow customization by organization)
|
||||
Cover Image
|
||||
Organization/Application Name
|
||||
Organization/Application Description
|
||||
|
||||
This content will be hidden for smaller screens.
|
||||
-->
|
||||
|
||||
<div fxFlex="70%" fxLayout="column" fxLayoutAlign="center center" fxHide.lt-md class="cover">
|
||||
<div class="cover-content">
|
||||
<h1 class="mat-display-1 cover-title">Mifos</h1>
|
||||
<p class="mat-headline">
|
||||
Mifos X is designed by the <a href="http://www.openmf.org">Mifos Initiative</a>. A <a href="http://mifos.org/resources/community/">global community</a> that aims to speed the elimination of poverty by enabling Organizations to more effectively and efficiently deliver responsible financial services to the world’s poor and unbanked.
|
||||
Sounds interesting? <a href="http://mifos.org/take-action/volunteer/">Get involved!</a>
|
||||
<p class="mat-headline cover-description">
|
||||
Mifos X is designed by the <a href="http://www.openmf.org" target="_blank" class="cover-link">Mifos Initiative</a>. A <a href="http://mifos.org/resources/community/" target="_blank" class="cover-link">global community</a> that aims to speed the elimination of poverty by enabling Organizations to more effectively and efficiently deliver responsible financial services to the world’s poor and unbanked.
|
||||
Sounds interesting? <a href="http://mifos.org/take-action/volunteer/" target="_blank" class="cover-link">Get involved!</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- In 30% when gt md otherwise fully covering -->
|
||||
<!--
|
||||
Content in 30%: (TODO: Allow customization of Name, Logo and Contact Information by organization)
|
||||
Language Selector
|
||||
Organization/Application Name with Logo
|
||||
Form
|
||||
Contact Information/Resources
|
||||
Footer
|
||||
-->
|
||||
|
||||
<div fxLayout="column" fxFlex.gt-md="1 0 30%" class="login-container">
|
||||
|
||||
<!-- First row we have language selector in the right of row -->
|
||||
<!-- Language Selector to the extreme right -->
|
||||
<div fxLayout="row-reverse" fxFlex="1 0 auto">
|
||||
<mat-form-field class="p-r-10 p-t-10 language-selector">
|
||||
<mat-select placeholder="Language" [(ngModel)]="language">
|
||||
<mat-option *ngFor="let language of languages" [value]="language">
|
||||
{{ language }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mifosx-language-selector class="p-r-10 p-t-10"></mifosx-language-selector>
|
||||
</div>
|
||||
|
||||
<!-- Next we have the logo -->
|
||||
<!-- Logo with Organisation Name -->
|
||||
<div fxLayout="row" fxLayoutAlign="center center" fxFlex="1 0 auto">
|
||||
<img src="assets/images/mifos-logo-flat.png" alt="" class="img-container">
|
||||
<img src="assets/images/mifos-logo-flat.png" alt="Mifos X Logo" class="img-container">
|
||||
</div>
|
||||
|
||||
<!-- Next we have the type of form -->
|
||||
<!--
|
||||
Type of Form:
|
||||
Login
|
||||
Reset Password
|
||||
Two Factor Authentication
|
||||
Forgot Password (TODO: Decision to be taken on providing this feature.)
|
||||
-->
|
||||
<div fxLayout="row" fxFlex="3 0 auto" fxLayoutAlign="center start">
|
||||
<mifosx-login-form *ngIf="!resetPassword && !twoFactorAuthenticationRequired"></mifosx-login-form>
|
||||
<mifosx-reset-password *ngIf="resetPassword"></mifosx-reset-password>
|
||||
<mifosx-two-factor-authentication *ngIf="twoFactorAuthenticationRequired"></mifosx-two-factor-authentication>
|
||||
<!-- TODO: Discuss about providing a feature of Forgot password -->
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<mat-list fxHide.lt-md fxLayout="row" fxFlexAlign="center" fxFlex="3 0 auto" class="list">
|
||||
<!-- Contact Information/Resources (hidden on smaller screens) -->
|
||||
<mat-list fxHide.lt-md fxLayout="row" fxFlexAlign="center" fxFlex="3 0 auto" class="information-list">
|
||||
<mat-list-item><button mat-button [matMenuTriggerFor]="resourcesMenu">Resources</button></mat-list-item>
|
||||
<mat-list-item><button mat-button [matMenuTriggerFor]="communityMenu">Community</button></mat-list-item>
|
||||
<mat-list-item><button mat-button [matMenuTriggerFor]="contributeMenu">Contribute</button></mat-list-item>
|
||||
</mat-list>
|
||||
|
||||
<div fxLayout="column" fxFlexAlign="center" fxFlex="1 0 auto">
|
||||
<mat-divider class="divider"></mat-divider>
|
||||
<p fxFlexAlign="center">Version {{ version }}</p>
|
||||
<p fxFlexAlign="center">Mifos X is up to date</p>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<mifosx-footer></mifosx-footer>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Contact Information/Resources Menus -->
|
||||
<mat-menu #resourcesMenu="matMenu">
|
||||
<a href="https://mifosforge.jira.com/wiki/spaces/docs/pages/52035622/User+Manual" mat-menu-item>User Manual</a>
|
||||
<a href="https://cwiki.apache.org/confluence/display/FINERACT/Apache+Fineract+1.0+Functional+Specifications" mat-menu-item>Functional Specifications</a>
|
||||
<a href="https://cwiki.apache.org/confluence/display/FINERACT/Contributor%27s+Zone" mat-menu-item>Developer Zone</a>
|
||||
<a href="https://mifosforge.jira.com/wiki/spaces/docs/pages/52035622/User+Manual" target="_blank" mat-menu-item>User Manual</a>
|
||||
<a href="https://cwiki.apache.org/confluence/display/FINERACT/Apache+Fineract+1.0+Functional+Specifications" target="_blank" mat-menu-item>Functional Specifications</a>
|
||||
<a href="https://cwiki.apache.org/confluence/display/FINERACT/Contributor%27s+Zone" target="_blank" mat-menu-item>Developer Zone</a>
|
||||
</mat-menu>
|
||||
|
||||
<mat-menu #communityMenu="matMenu">
|
||||
<a href="https://groups.google.com/forum/#!forum/mifosusers" mat-menu-item>User Group</a>
|
||||
<a href="https://groups.google.com/forum/#!forum/mifosdeveloper" mat-menu-item>Developer Group</a>
|
||||
<a href="http://mifos.org/resources/community/communications/#mifos-irc" mat-menu-item>IRC</a>
|
||||
<a href="https://groups.google.com/forum/#!forum/mifosusers" target="_blank" mat-menu-item>User Group</a>
|
||||
<a href="https://groups.google.com/forum/#!forum/mifosdeveloper" target="_blank" mat-menu-item>Developer Group</a>
|
||||
<a href="http://mifos.org/resources/community/communications/#mifos-irc" target="_blank" mat-menu-item>IRC</a>
|
||||
</mat-menu>
|
||||
|
||||
<mat-menu #contributeMenu="matMenu">
|
||||
<a href="https://mifosforge.jira.com/wiki/spaces/MDZ/pages/92012624/Key+Design+Principles" mat-menu-item>Key Design Principles</a>
|
||||
<a href="https://sourceforge.net/projects/mifos/" mat-menu-item>Working with Code</a>
|
||||
<a href="http://mifos.org/donate/" mat-menu-item>Donate</a>
|
||||
<a href="https://mifosforge.jira.com/wiki/spaces/MDZ/pages/92012624/Key+Design+Principles" target="_blank" mat-menu-item>Key Design Principles</a>
|
||||
<a href="https://sourceforge.net/projects/mifos/" target="_blank" mat-menu-item>Working with Code</a>
|
||||
<a href="http://mifos.org/donate/" target="_blank" mat-menu-item>Donate</a>
|
||||
</mat-menu>
|
||||
|
||||
@@ -1,100 +1,56 @@
|
||||
.cover {
|
||||
background: url("../../assets/images/cover_image.jpg") no-repeat center center;
|
||||
background-size: cover;
|
||||
// opacity: 0.4
|
||||
position: relative;
|
||||
z-index: 0;;
|
||||
z-index: 0;
|
||||
max-width: 70vw;
|
||||
}
|
||||
|
||||
.cover::after {
|
||||
content: '';
|
||||
opacity: 0.4;
|
||||
background: linear-gradient(black, black);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.6;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: block;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
}
|
||||
&::after {
|
||||
content: "";
|
||||
opacity: 0.5;
|
||||
background: linear-gradient(#000, #000);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: block;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.cover-content {
|
||||
display: block;
|
||||
max-width: 80%;
|
||||
margin: 100px;
|
||||
color: white;
|
||||
text-align: justify;
|
||||
z-index: 2;
|
||||
.cover-content {
|
||||
display: block;
|
||||
max-width: 80%;
|
||||
margin: 100px;
|
||||
color: #fff;
|
||||
text-align: justify;
|
||||
z-index: 2;
|
||||
|
||||
p {
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: white;
|
||||
}
|
||||
.cover-title {
|
||||
border-bottom: 2px solid #fff;
|
||||
margin: 0 0 20px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: lightgray;
|
||||
}
|
||||
.cover-description {
|
||||
.cover-link {
|
||||
text-decoration: underline;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
color: #d3d3d3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cover-title {
|
||||
border-bottom: 2px solid white;
|
||||
margin: 0 0 20px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
box-shadow: 1px 1px 50px black;
|
||||
box-shadow: 1px 1px 50px #000;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
|
||||
.img-container {
|
||||
max-width: 18rem;
|
||||
}
|
||||
}
|
||||
|
||||
.language-selector {
|
||||
max-width: 4rem;
|
||||
}
|
||||
|
||||
.img-container {
|
||||
max-width: 18rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 10px 30px;
|
||||
}
|
||||
|
||||
// @import "../../theme/theme-variables";
|
||||
// @import "~bootstrap/scss/mixins/_breakpoints";
|
||||
|
||||
// .login-container {
|
||||
// position: absolute;
|
||||
// top: 0;
|
||||
// bottom: 0;
|
||||
// left: 0;
|
||||
// right: 0;
|
||||
// }
|
||||
|
||||
// .login-box {
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// justify-content: center;
|
||||
// align-items: center;
|
||||
// width: 100%;
|
||||
// min-height: 100%;
|
||||
// }
|
||||
|
||||
// .ng-invalid.ng-touched:not(form) {
|
||||
// border-left: 4px solid theme-color("danger");
|
||||
// }
|
||||
|
||||
// @include media-breakpoint-down(xs) {
|
||||
// .container {
|
||||
// width: 100%;
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { CoreModule } from '../core';
|
||||
import { LoginComponent } from './login.component';
|
||||
|
||||
describe('LoginComponent', () => {
|
||||
describe('LanguageSelectorComponent', () => {
|
||||
let component: LoginComponent;
|
||||
let fixture: ComponentFixture<LoginComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule,
|
||||
TranslateModule.forRoot(),
|
||||
ReactiveFormsModule,
|
||||
CoreModule
|
||||
],
|
||||
declarations: [LoginComponent]
|
||||
declarations: [ LoginComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { I18nService } from '../core/i18n.service';
|
||||
import { AlertService } from '../core/alert.service';
|
||||
/** rxjs Imports */
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { environment } from '../../environments/environment';
|
||||
/** Custom Services and Models */
|
||||
import { AlertService, Alert } from '../core';
|
||||
|
||||
/**
|
||||
* Login component.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-login',
|
||||
templateUrl: './login.component.html',
|
||||
@@ -13,19 +18,25 @@ import { environment } from '../../environments/environment';
|
||||
})
|
||||
export class LoginComponent implements OnInit, OnDestroy {
|
||||
|
||||
version: string = environment.version;
|
||||
languages = ['en-US', 'fr-FR'];
|
||||
language = this.languages[0];
|
||||
/** True if password requires a reset. */
|
||||
resetPassword = false;
|
||||
/** True if user requires two factor authentication. */
|
||||
twoFactorAuthenticationRequired = false;
|
||||
alert$: any;
|
||||
/** Subscription to alerts. */
|
||||
alert$: Subscription;
|
||||
|
||||
constructor(private i18nService: I18nService,
|
||||
private alertService: AlertService,
|
||||
/**
|
||||
* @param {AlertService} alertService Alert Service.
|
||||
* @param {Router} router Router for navigation.
|
||||
*/
|
||||
constructor(private alertService: AlertService,
|
||||
private router: Router) { }
|
||||
|
||||
/**
|
||||
* Subscribes to alert event of alert service.
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.alert$ = this.alertService.alertEvent.subscribe((alertEvent: any) => {
|
||||
this.alert$ = this.alertService.alertEvent.subscribe((alertEvent: Alert) => {
|
||||
const alertType = alertEvent.type;
|
||||
if (alertType === 'Password Expired') {
|
||||
this.twoFactorAuthenticationRequired = false;
|
||||
@@ -41,18 +52,9 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
setLanguage(language: string) {
|
||||
this.i18nService.language = language;
|
||||
}
|
||||
|
||||
get currentLanguage(): string {
|
||||
return this.i18nService.language;
|
||||
}
|
||||
|
||||
// get languages(): string[] {
|
||||
// return this.i18nService.supportedLanguages;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Unsubscribes from alerts.
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this.alert$.unsubscribe();
|
||||
}
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
/** Angular Imports */
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
|
||||
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
/** Custom Modules */
|
||||
import { SharedModule } from '../shared';
|
||||
import { LoginRoutingModule } from './login-routing.module';
|
||||
import { LoginComponent } from './login.component';
|
||||
import { CoreModule } from '../core/core.module';
|
||||
import { LoginFormComponent } from './login-form/login-form.component';
|
||||
import { TwoFactorAuthenticationComponent } from './two-factor-authentication/two-factor-authentication.component';
|
||||
import { ResetPasswordComponent } from './reset-password/reset-password.component';
|
||||
|
||||
/** Custom Components */
|
||||
import { LoginComponent } from './login.component';
|
||||
import { LoginFormComponent } from './login-form/login-form.component';
|
||||
import { ResetPasswordComponent } from './reset-password/reset-password.component';
|
||||
import { TwoFactorAuthenticationComponent } from './two-factor-authentication/two-factor-authentication.component';
|
||||
|
||||
/**
|
||||
* Login Module
|
||||
*
|
||||
* All components related to user authentication should be declared here.
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
CoreModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
TranslateModule,
|
||||
SharedModule,
|
||||
LoginRoutingModule
|
||||
],
|
||||
declarations: [
|
||||
LoginComponent,
|
||||
LoginFormComponent,
|
||||
TwoFactorAuthenticationComponent,
|
||||
ResetPasswordComponent
|
||||
ResetPasswordComponent,
|
||||
TwoFactorAuthenticationComponent
|
||||
]
|
||||
})
|
||||
export class LoginModule { }
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/** Angular Imports */
|
||||
import { FormGroup, ValidatorFn, ValidationErrors } from '@angular/forms';
|
||||
|
||||
/** Validates that the values of password and confirm password are same. */
|
||||
export const confirmPasswordValidator: ValidatorFn = (resetPasswordForm: FormGroup): ValidationErrors | null => {
|
||||
const password = resetPasswordForm.get('password');
|
||||
const confirmPassword = resetPasswordForm.get('repeatPassword');
|
||||
return password && confirmPassword && password.value !== confirmPassword.value ? { 'passwordsDoNotMatch': true } : null;
|
||||
};
|
||||
@@ -1,31 +1,44 @@
|
||||
<p>Please update your password:</p>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
<form fxLayout="column" [formGroup]="resetPasswordForm" (ngSubmit)="resetPassword()">
|
||||
<mat-form-field fxFlexAlign="center" class="textfield">
|
||||
<span matPrefix><mat-icon>lock</mat-icon> </span>
|
||||
<input type="{{ passwordInputType }}" matInput placeholder="Password" formControlName="password">
|
||||
|
||||
<form fxLayout="column" [formGroup]="resetPasswordForm" (ngSubmit)="resetPassword()" id="reset-password-form">
|
||||
|
||||
<mat-form-field fxFlexAlign="center" class="reset-password-input">
|
||||
<span matPrefix>
|
||||
<fa-icon icon="lock" size="lg"></fa-icon>
|
||||
</span>
|
||||
<mat-label>Password</mat-label>
|
||||
<input type="{{ passwordInputType }}" matInput required formControlName="password">
|
||||
<button mat-button *ngIf="resetPasswordForm.controls.password.value && !loading" matSuffix mat-icon-button
|
||||
aria-label="Clear" (mousedown)="passwordInputType = 'text'" (mouseup)="passwordInputType = 'password'">
|
||||
<mat-icon *ngIf="passwordInputType === 'password'">visibility</mat-icon>
|
||||
<mat-icon *ngIf="passwordInputType === 'text'">visibility_off</mat-icon>
|
||||
(mousedown)="passwordInputType = 'text'" (mouseup)="passwordInputType = 'password'">
|
||||
<fa-icon *ngIf="passwordInputType === 'password'" icon="eye"></fa-icon>
|
||||
<fa-icon *ngIf="passwordInputType === 'text'" icon="eye-slash"></fa-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="resetPasswordForm.controls.password.hasError('required')">
|
||||
Password is <strong>required</strong>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlexAlign="center" class="textfield">
|
||||
<span matPrefix><mat-icon>check_circle</mat-icon> </span>
|
||||
<input type="password" matInput placeholder="Confirm Password" formControlName="repeatPassword">
|
||||
|
||||
<mat-form-field fxFlexAlign="center" class="reset-password-input">
|
||||
<span matPrefix>
|
||||
<fa-icon icon="check-circle" size="lg"></fa-icon>
|
||||
</span>
|
||||
<mat-label>Confirm Password</mat-label>
|
||||
<input type="password" matInput required formControlName="repeatPassword">
|
||||
<mat-error *ngIf="resetPasswordForm.controls.repeatPassword.hasError('required')">
|
||||
Confirm Password is <strong>required</strong>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<p class="error" *ngIf="resetPasswordForm.errors?.passwordsDoNotMatch &&
|
||||
(!resetPasswordForm.controls.password.hasError('required') && !resetPasswordForm.controls.repeatPassword.hasError('required'))">
|
||||
|
||||
<p class="reset-password-error" *ngIf="resetPasswordForm.errors?.passwordsDoNotMatch &&
|
||||
(!resetPasswordForm.controls.password.hasError('required') && !resetPasswordForm.controls.repeatPassword.hasError('required'))">
|
||||
Passwords <strong>do not match</strong>
|
||||
</p>
|
||||
<button mat-raised-button color="primary" fxFlexAlign="center" class="btn" [disabled]="!resetPasswordForm.valid">
|
||||
|
||||
<button mat-raised-button color="primary" fxFlexAlign="center" class="reset-password-button" [disabled]="!resetPasswordForm.valid">
|
||||
Reset Password
|
||||
<mat-spinner [diameter]="20" *ngIf="loading"></mat-spinner>
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
.textfield {
|
||||
width: 14rem;
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
#reset-password-form {
|
||||
.reset-password-input {
|
||||
width: 14rem;
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 14rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.reset-password-button {
|
||||
width: 14rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #f44336;
|
||||
font-size: 80%;
|
||||
margin: 0;
|
||||
}
|
||||
.reset-password-error {
|
||||
color: #f44336;
|
||||
font-size: 80%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
mat-spinner {
|
||||
z-index: 4;
|
||||
float: right;
|
||||
margin: 0.5rem 0;
|
||||
mat-spinner {
|
||||
float: right;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { FormGroup, FormBuilder, Validators, ValidatorFn, ValidationErrors } from '@angular/forms';
|
||||
import { AuthenticationService } from '../../core';
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||
|
||||
/** rxjs Imports */
|
||||
import { finalize } from 'rxjs/operators';
|
||||
|
||||
const confirmPasswordValidator: ValidatorFn = (resetPasswordForm: FormGroup): ValidationErrors | null => {
|
||||
const password = resetPasswordForm.controls.password;
|
||||
const confirmPassword = resetPasswordForm.controls.repeatPassword;
|
||||
return password && confirmPassword && password.value !== confirmPassword.value ? { 'passwordsDoNotMatch': true } : null;
|
||||
};
|
||||
/** Custom Services */
|
||||
import { AuthenticationService } from '../../core';
|
||||
|
||||
/** Custom Validators */
|
||||
import { confirmPasswordValidator } from './confirm-password.validator';
|
||||
|
||||
/**
|
||||
* Reset password component.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-reset-password',
|
||||
templateUrl: './reset-password.component.html',
|
||||
@@ -16,25 +21,33 @@ const confirmPasswordValidator: ValidatorFn = (resetPasswordForm: FormGroup): Va
|
||||
})
|
||||
export class ResetPasswordComponent implements OnInit {
|
||||
|
||||
/** Reset password form group. */
|
||||
resetPasswordForm: FormGroup;
|
||||
loading = false;
|
||||
/** Password input field type. */
|
||||
passwordInputType: string;
|
||||
/** True if loading. */
|
||||
loading = false;
|
||||
|
||||
/**
|
||||
* @param {FormBuilder} formBuilder Form Builder.
|
||||
* @param {AuthenticationService} authenticationService Authentication Service.
|
||||
*/
|
||||
constructor(private formBuilder: FormBuilder,
|
||||
private authenticationService: AuthenticationService) { }
|
||||
|
||||
/**
|
||||
* Creates reset password form.
|
||||
*
|
||||
* Initializes password input field type.
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.createResetPasswordForm();
|
||||
this.passwordInputType = 'password';
|
||||
}
|
||||
|
||||
private createResetPasswordForm() {
|
||||
this.resetPasswordForm = this.formBuilder.group({
|
||||
'password': ['', Validators.required],
|
||||
'repeatPassword': ['', Validators.required]
|
||||
}, { validator: confirmPasswordValidator });
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the password of user.
|
||||
*/
|
||||
resetPassword() {
|
||||
this.loading = true;
|
||||
this.resetPasswordForm.disable();
|
||||
@@ -42,10 +55,20 @@ export class ResetPasswordComponent implements OnInit {
|
||||
.pipe(finalize(() => {
|
||||
this.resetPasswordForm.reset();
|
||||
this.resetPasswordForm.markAsPristine();
|
||||
// Bug: Validation errors won't get removed on reset
|
||||
// Angular Material Bug: Validation errors won't get removed on reset.
|
||||
this.resetPasswordForm.enable();
|
||||
this.loading = false;
|
||||
})).subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates reset password form.
|
||||
*/
|
||||
private createResetPasswordForm() {
|
||||
this.resetPasswordForm = this.formBuilder.group({
|
||||
'password': ['', Validators.required],
|
||||
'repeatPassword': ['', Validators.required]
|
||||
}, { validator: confirmPasswordValidator });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,37 +1,60 @@
|
||||
<p><strong>Two Factor Authentication</strong></p>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<!-- Select delivery method to receive OTP -->
|
||||
<p *ngIf="!otpRequested">Please select a delivery method:</p>
|
||||
<form fxLayout="column" *ngIf="!otpRequested" [formGroup]="twoFactorAuthenticationDeliveryMethodForm" (ngSubmit)="requestOTP()">
|
||||
|
||||
<form fxLayout="column" *ngIf="!otpRequested" [formGroup]="twoFactorAuthenticationDeliveryMethodForm" class="two-factor-auth-form"
|
||||
(ngSubmit)="requestOTP()">
|
||||
|
||||
<mat-radio-group fxLayout="column" fxFlexAlign="center" formControlName="twoFactorAuthenticationDeliveryMethod">
|
||||
<mat-radio-button *ngFor="let twoFactorAuthenticationDeliveryMethod of twoFactorAuthenticationDeliveryMethods"
|
||||
[value]="twoFactorAuthenticationDeliveryMethod">
|
||||
Send {{ twoFactorAuthenticationDeliveryMethod.name }} to {{ twoFactorAuthenticationDeliveryMethod.target }}
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
|
||||
<button mat-raised-button color="primary" fxFlexAlign="center" fxFlexFill [disabled]="!twoFactorAuthenticationDeliveryMethodForm.valid">
|
||||
Request OTP
|
||||
<mat-spinner [diameter]="20" *ngIf="loading"></mat-spinner>
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- show input for otp request -->
|
||||
<!-- Show input for OTP -->
|
||||
<p *ngIf="otpRequested">Please enter the OTP:</p>
|
||||
<form fxLayout="column" *ngIf="otpRequested" [formGroup]="twoFactorAuthenticationForm" (ngSubmit)="validateOTP()">
|
||||
<mat-form-field fxFlexAlign="center" class="textfield">
|
||||
<span matPrefix><mat-icon>verified_user</mat-icon> </span>
|
||||
<input matInput placeholder="OTP" autocomplete="off" formControlName="otp">
|
||||
<mat-hint align="start"><strong>Delivery Method:</strong> {{ selectedTwoFactorAuthenticationDeliveryMethod.name }} </mat-hint>
|
||||
<mat-hint align="end"><strong>Validity:</strong> {{ tokenValidityTime / 60 }} mins </mat-hint>
|
||||
|
||||
<form fxLayout="column" *ngIf="otpRequested" [formGroup]="twoFactorAuthenticationForm" class="two-factor-auth-form"
|
||||
(ngSubmit)="validateOTP()">
|
||||
|
||||
<mat-form-field fxFlexAlign="center" class="two-factor-auth-input">
|
||||
<span matPrefix>
|
||||
<fa-icon icon="user-shield"></fa-icon>
|
||||
</span>
|
||||
<mat-label>OTP</mat-label>
|
||||
<input type="password" matInput autocomplete="off" required formControlName="otp">
|
||||
<mat-hint align="start">
|
||||
<strong>Delivery Method:</strong> {{ selectedTwoFactorAuthenticationDeliveryMethod.name }}
|
||||
</mat-hint>
|
||||
<mat-hint align="end">
|
||||
<strong>Validity:</strong> {{ tokenValidityTime / 60 }} mins
|
||||
</mat-hint>
|
||||
<mat-error *ngIf="twoFactorAuthenticationForm.controls.otp.hasError('required')">
|
||||
OTP is <strong>required</strong>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" fxFlexAlign="center" class="btn" [disabled]="!twoFactorAuthenticationForm.valid">
|
||||
|
||||
<button mat-raised-button color="primary" fxFlexAlign="center" class="two-factor-auth-button"
|
||||
[disabled]="!twoFactorAuthenticationForm.valid">
|
||||
Validate OTP
|
||||
<mat-spinner [diameter]="20" *ngIf="loading"></mat-spinner>
|
||||
</button>
|
||||
<button type="button" mat-button fxFlexAlign="center" class="btn" (click)="resendOTP()" [disabled]="loading || resendOTPLoading">
|
||||
|
||||
<button type="button" mat-button fxFlexAlign="center" class="two-factor-auth-button" (click)="resendOTP()"
|
||||
[disabled]="loading || resendOTPLoading">
|
||||
Resend OTP
|
||||
<mat-spinner [diameter]="20" *ngIf="resendOTPLoading"></mat-spinner>
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
.btn {
|
||||
width: 14rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.two-factor-auth-form {
|
||||
mat-radio-button {
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
mat-radio-button {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
mat-radio-button:last-child {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.two-factor-auth-button {
|
||||
width: 14rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.textfield {
|
||||
width: 14rem;
|
||||
}
|
||||
.two-factor-auth-input {
|
||||
width: 14rem;
|
||||
}
|
||||
|
||||
mat-spinner {
|
||||
z-index: 4;
|
||||
float: right;
|
||||
margin: 0.5rem 0;
|
||||
mat-spinner {
|
||||
float: right;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||
import { AuthenticationService } from '../../core';
|
||||
|
||||
/** rxjs Imports */
|
||||
import { finalize } from 'rxjs/operators';
|
||||
|
||||
/** Custom Services */
|
||||
import { AuthenticationService } from '../../core';
|
||||
|
||||
/**
|
||||
* Two factor authentication component.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-two-factor-authentication',
|
||||
templateUrl: './two-factor-authentication.component.html',
|
||||
@@ -10,32 +18,46 @@ import { finalize } from 'rxjs/operators';
|
||||
})
|
||||
export class TwoFactorAuthenticationComponent implements OnInit {
|
||||
|
||||
/** Available delivery methods to receive OTP. */
|
||||
twoFactorAuthenticationDeliveryMethods: any;
|
||||
/** Delivery method selected to receive OTP. */
|
||||
selectedTwoFactorAuthenticationDeliveryMethod: any;
|
||||
loading = false;
|
||||
resendOTPLoading = false;
|
||||
/** True if OTP is requested. */
|
||||
otpRequested = false;
|
||||
/** Time for which OTP is valid. */
|
||||
tokenValidityTime: number;
|
||||
/** Two factor authentication delivery method form group. */
|
||||
twoFactorAuthenticationDeliveryMethodForm: FormGroup;
|
||||
/** Two factor authentication form group. */
|
||||
twoFactorAuthenticationForm: FormGroup;
|
||||
/** True if loading. */
|
||||
loading = false;
|
||||
/** True if loading. */
|
||||
resendOTPLoading = false;
|
||||
|
||||
/**
|
||||
* @param {FormBuilder} formBuilder Form Builder.
|
||||
* @param {AuthenticationService} authenticationService Authentication Service.
|
||||
*/
|
||||
constructor(private formBuilder: FormBuilder,
|
||||
private authenticationService: AuthenticationService) { }
|
||||
|
||||
/**
|
||||
* Creates two factor authentication delivery method form.
|
||||
*
|
||||
* Gets the delivery methods available to receive OTP.
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.createTwoFactorAuthenticationDeliveryMethodForm();
|
||||
this.authenticationService.getDeliveryMethods()
|
||||
.subscribe(deliveryMethods => {
|
||||
.subscribe((deliveryMethods: any) => {
|
||||
this.twoFactorAuthenticationDeliveryMethods = deliveryMethods;
|
||||
});
|
||||
}
|
||||
|
||||
private createTwoFactorAuthenticationDeliveryMethodForm() {
|
||||
this.twoFactorAuthenticationDeliveryMethodForm = this.formBuilder.group({
|
||||
'twoFactorAuthenticationDeliveryMethod': ['', Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests OTP via the selected delivery method.
|
||||
*/
|
||||
requestOTP() {
|
||||
this.loading = true;
|
||||
this.twoFactorAuthenticationDeliveryMethodForm.disable();
|
||||
@@ -46,7 +68,7 @@ export class TwoFactorAuthenticationComponent implements OnInit {
|
||||
.pipe(finalize(() => {
|
||||
this.twoFactorAuthenticationDeliveryMethodForm.reset();
|
||||
this.twoFactorAuthenticationDeliveryMethodForm.markAsPristine();
|
||||
// Bug: Validation errors won't get removed on reset
|
||||
// Angular Material Bug: Validation errors won't get removed on reset.
|
||||
this.twoFactorAuthenticationDeliveryMethodForm.enable();
|
||||
this.loading = false;
|
||||
}))
|
||||
@@ -57,12 +79,9 @@ export class TwoFactorAuthenticationComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
private createTwoFactorAuthenticationForm() {
|
||||
this.twoFactorAuthenticationForm = this.formBuilder.group({
|
||||
'otp': ['', Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the OTP and authenticates the user.
|
||||
*/
|
||||
validateOTP() {
|
||||
this.loading = true;
|
||||
this.twoFactorAuthenticationForm.disable();
|
||||
@@ -70,12 +89,15 @@ export class TwoFactorAuthenticationComponent implements OnInit {
|
||||
.pipe(finalize(() => {
|
||||
this.twoFactorAuthenticationForm.reset();
|
||||
this.twoFactorAuthenticationForm.markAsPristine();
|
||||
// Bug: Validation errors won't get removed on reset
|
||||
// Angular Material Bug: Validation errors won't get removed on reset.
|
||||
this.twoFactorAuthenticationForm.enable();
|
||||
this.loading = false;
|
||||
})).subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resends OTP via the selected delivery method.
|
||||
*/
|
||||
resendOTP() {
|
||||
this.resendOTPLoading = true;
|
||||
this.twoFactorAuthenticationForm.disable();
|
||||
@@ -83,9 +105,28 @@ export class TwoFactorAuthenticationComponent implements OnInit {
|
||||
.pipe(finalize(() => {
|
||||
this.twoFactorAuthenticationForm.reset();
|
||||
this.twoFactorAuthenticationForm.markAsPristine();
|
||||
// Bug: Validation errors won't get removed on reset
|
||||
// Angular Material Bug: Validation errors won't get removed on reset.
|
||||
this.twoFactorAuthenticationForm.enable();
|
||||
this.resendOTPLoading = false;
|
||||
})).subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates two factor authentication delivery method form.
|
||||
*/
|
||||
private createTwoFactorAuthenticationDeliveryMethodForm() {
|
||||
this.twoFactorAuthenticationDeliveryMethodForm = this.formBuilder.group({
|
||||
'twoFactorAuthenticationDeliveryMethod': ['', Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates two factor authentication form.
|
||||
*/
|
||||
private createTwoFactorAuthenticationForm() {
|
||||
this.twoFactorAuthenticationForm = this.formBuilder.group({
|
||||
'otp': ['', Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
|
||||
export class DeleteDialogComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* @param dialogRef Component reference to dialog.
|
||||
* @param data Provides a deleteContext.
|
||||
* @param {MatDialogRef} dialogRef Component reference to dialog.
|
||||
* @param {any} data Provides a deleteContext.
|
||||
*/
|
||||
constructor(public dialogRef: MatDialogRef<DeleteDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any) { }
|
||||
|
||||
@@ -21,7 +21,7 @@ export class FileUploadComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* Uploads a file.
|
||||
* @param {any} $event The file input change event.
|
||||
* @param {any} event The file input change event.
|
||||
*/
|
||||
onFileSelected($event: any) {
|
||||
this.fileName = $event.target.files[0].name;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<div fxLayout="column" id="footer" class="m-b-20">
|
||||
<mat-divider fxFlexAlign="center" class="divider"></mat-divider>
|
||||
<p fxFlexAlign="center" class="footer-content">Version {{ version }}</p>
|
||||
<p fxFlexAlign="center" class="footer-content">Mifos X is up to date</p>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
#footer {
|
||||
.divider {
|
||||
margin: 1rem 1rem 0.5rem;
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FooterComponent } from './footer.component';
|
||||
|
||||
describe('FooterComponent', () => {
|
||||
let component: FooterComponent;
|
||||
let fixture: ComponentFixture<FooterComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ FooterComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FooterComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
/** Environment Configuration */
|
||||
import { environment } from 'environments/environment';
|
||||
|
||||
/**
|
||||
* Footer component.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-footer',
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrls: ['./footer.component.scss']
|
||||
})
|
||||
export class FooterComponent implements OnInit {
|
||||
|
||||
/** Mifos X version. */
|
||||
version: string = environment.version;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,18 +5,31 @@ import { NgModule } from '@angular/core';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import {
|
||||
faCheckCircle,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFileUpload,
|
||||
faSearch
|
||||
faLock,
|
||||
faSearch,
|
||||
faUserCircle,
|
||||
faUserShield
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
/** Add icons to the library for convenient access in other components. */
|
||||
library.add(
|
||||
faCheckCircle,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFileUpload,
|
||||
faSearch
|
||||
faLock,
|
||||
faSearch,
|
||||
faUserCircle,
|
||||
faUserShield
|
||||
);
|
||||
|
||||
/**
|
||||
* Icons Module
|
||||
*
|
||||
* Angular Font Awesome module is exported here.
|
||||
*/
|
||||
@NgModule({
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './shared.module';
|
||||
export { SharedModule } from './shared.module';
|
||||
export { DeleteDialogComponent } from './delete-dialog/delete-dialog.component';
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<mat-form-field id="language-selector">
|
||||
<mat-label>Language</mat-label>
|
||||
<mat-select [formControl]="languageSelector">
|
||||
<mat-option *ngFor="let language of languages" [value]="language">
|
||||
{{ language }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@@ -0,0 +1,3 @@
|
||||
#language-selector {
|
||||
max-width: 4rem;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LanguageSelectorComponent } from './language-selector.component';
|
||||
|
||||
describe('LanguageSelectorComponent', () => {
|
||||
let component: LanguageSelectorComponent;
|
||||
let fixture: ComponentFixture<LanguageSelectorComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LanguageSelectorComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LanguageSelectorComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
|
||||
/** Custom Services */
|
||||
import { I18nService } from 'app/core';
|
||||
|
||||
/**
|
||||
* Language selector component.
|
||||
*
|
||||
* TODO: Decision to be taken on using ngx-translate or angular-internationalization
|
||||
* to provide language support in the application.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-language-selector',
|
||||
templateUrl: './language-selector.component.html',
|
||||
styleUrls: ['./language-selector.component.scss']
|
||||
})
|
||||
export class LanguageSelectorComponent implements OnInit {
|
||||
|
||||
/** Language selector form control. */
|
||||
languageSelector = new FormControl();
|
||||
|
||||
/**
|
||||
* Sets the language of the application in the selector on initial setup.
|
||||
* @param {I18nService} i18nService Internationalization Service.
|
||||
*/
|
||||
constructor(private i18nService: I18nService) {
|
||||
this.languageSelector.setValue(this.currentLanguage);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new language to be used by the application.
|
||||
* @param {string} language New language.
|
||||
*/
|
||||
setLanguage(language: string) {
|
||||
this.i18nService.language = language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current language used by the application.
|
||||
* @returns {string} Current language.
|
||||
*/
|
||||
get currentLanguage(): string {
|
||||
return this.i18nService.language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the languages supported by the application.
|
||||
* @return {string[]} Supported languages.
|
||||
*/
|
||||
get languages(): string[] {
|
||||
return this.i18nService.supportedLanguages;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
|
||||
/**
|
||||
* Material Module
|
||||
*
|
||||
* Angular CDK, Angular Material and Flex Layout modules are exported here.
|
||||
*/
|
||||
@NgModule({
|
||||
|
||||
@@ -1,34 +1,44 @@
|
||||
/** Angular Imports */
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
/** Custom Components */
|
||||
import { FileUploadComponent } from './file-upload/file-upload.component';
|
||||
import { DeleteDialogComponent } from './delete-dialog/delete-dialog.component';
|
||||
import { FileUploadComponent } from './file-upload/file-upload.component';
|
||||
import { FooterComponent } from './footer/footer.component';
|
||||
import { LanguageSelectorComponent } from './language-selector/language-selector.component';
|
||||
|
||||
/** Custom Modules */
|
||||
import { MaterialModule } from './material.module';
|
||||
import { IconsModule } from './icons.module';
|
||||
import { MaterialModule } from './material.module';
|
||||
|
||||
/**
|
||||
* Shared Module
|
||||
*
|
||||
* Modules and components that are shared throughout the application should be here.
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IconsModule,
|
||||
MaterialModule
|
||||
MaterialModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
declarations: [
|
||||
DeleteDialogComponent,
|
||||
FileUploadComponent
|
||||
FileUploadComponent,
|
||||
FooterComponent,
|
||||
LanguageSelectorComponent
|
||||
],
|
||||
exports: [
|
||||
FileUploadComponent,
|
||||
FooterComponent,
|
||||
LanguageSelectorComponent,
|
||||
CommonModule,
|
||||
IconsModule,
|
||||
MaterialModule
|
||||
MaterialModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
entryComponents: [
|
||||
DeleteDialogComponent
|
||||
|
||||
Reference in New Issue
Block a user