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:
Abhay Chawla
2018-08-09 22:28:17 +05:30
committed by GitHub
parent 9eead37c8c
commit 2330f70f2c
33 changed files with 629 additions and 313 deletions
+13 -4
View File
@@ -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,
{
+7 -2
View File
@@ -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);
}
+1
View File
@@ -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>&nbsp;&nbsp;</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>&nbsp;&nbsp;
</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>&nbsp;&nbsp;</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>&nbsp;&nbsp;
</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],
+10
View File
@@ -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],
+45 -35
View File
@@ -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>
+40 -84
View File
@@ -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%;
// }
// }
+2 -13
View File
@@ -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();
}));
+24 -22
View File
@@ -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();
}
+17 -16
View File
@@ -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>&nbsp;&nbsp;</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>&nbsp;&nbsp;
</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>&nbsp;&nbsp;</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>&nbsp;&nbsp;
</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>&nbsp;&nbsp;</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>&nbsp;&nbsp;
</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();
});
});
+25
View File
@@ -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() {
}
}
+15 -2
View File
@@ -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 -1
View File
@@ -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;
}
}
+1
View File
@@ -41,6 +41,7 @@ import {
/**
* Material Module
*
* Angular CDK, Angular Material and Flex Layout modules are exported here.
*/
@NgModule({
+15 -5
View File
@@ -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