Angular-ElectronJS – Login API REST jwt


Following an article on the formatting of the Angular-Electronjs login page.

The following elements will be discussed:

  • Code cleaning and factoring
  • Error messages
  • HTML Form – Added Reactive forms.
  • Typescript – Validation of the form and connection to an api in nodeexpress-js

Code cleaning and factoring

Let’s clean up our code.

Additional Modules

First, we will change the use of fontawesome with the fontawesome module for Angular.

The classic code will be replaced by this one

<fa-icon icon="['fas', 'spinner']" spin="true"></fa-icon>

First, we uninstall @fortawesome/fontawesome-free

npm uninstall @fortawesome/fontawesome-free --save

Then in /src/styles.scss, delete the following entry

$fa-font-path : '../node_modules/@fortawesome/fontawesome-free/webfonts';
@import '../node_modules/@fortawesome/fontawesome-free/scss/fontawesome';

This replacement obviously requires the installation of additional packages.. And we install the following packages:

  • @fortawesome/fontawesome-svg-core
  • @fortawesome/free-solid-svg-icons
  • @fortawesome/angular-fontawesome

In one request :

npm install @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/angular-fontawesome --save 

To be able to use this package throughout the application, let’s modify src/app/app.module.ts and add fontawesome and other material components such as MatSnackBarModule and MatProgressBarModule.

import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { library } from '@fortawesome/fontawesome-svg-core';
import { fas } from '@fortawesome/free-solid-svg-icons';
import {
  MatSidenavModule,
  MatToolbarModule,
  MatIconModule,
  MatCardModule,
  MatInputModule,
  MatFormFieldModule,
  MatButtonModule,
  MatListModule,
  MatExpansionModule,
  MatSnackBarModule,
  MatProgressBarModule
} from '@angular/material';

…then add in NgModule

@NgModule({
    imports: [
        // Material
        MatSidenavModule,
        MatButtonModule,
        MatToolbarModule,
        MatIconModule,
        MatCardModule,
        MatListModule,
        MatInputModule,
        MatFormFieldModule,
        MatProgressBarModule,
        MatSnackBarModule,
        MatExpansionModule,
        FlexLayoutModule,
        ReactiveFormsModule,
        // Fontawesome
        FontAwesomeModule
    ]
});

Error messages

The error messages of the login form are displayed in 2 places:

  • in the file /src/app/components/login/login.component.html
  • and in the Typescript /src/app/components/login/login.component.ts

…File login.component.html

Let’s add an error message on the form validation. This message will appear when the username/password pair does not match the torque recorded in the database.

It will be managed by the next piece of code placed just after the opening tag <form>

<div *ngIf="errorForm" fxLayout="row" fxLayoutGap="5px" fxLayoutAlign="center">
        <div><mat-icon aria-hidden="false" color="warn" >error</mat-icon></div>
        <div>{{ 'error.form' | translate }}</div>
      </div>

Here’s an example:

Hexo

Using *ngIf="errorForm", this message block will display if the errorForm property is set to true. We will define this property a little further down.

The fx* attributes are specific to Angular flex layout. They make it possible to design the elements.

Error messages will also be displayed below each field of the form (username and password).

The ID field tag will be like this

<mat-form-field>
        <input [attr.disabled]="savedForm ? '' : null" [readonly]="savedForm" formControlName="username" matInput placeholder="{{ 'login.email_placeholder' | translate }}">
        <mat-error *ngIf="usernameControl.dirty && usernameControl.hasError('required')">{{ 'error.required' | translate }}</mat-error>
        <mat-error *ngIf="usernameControl.dirty && usernameControl.hasError('minlength')">{{ 'error.minlength' | translate }}</mat-error>
        <mat-error *ngIf="usernameControl.dirty && usernameControl.hasError('maxlength')">{{ 'error.maxlength' | translate }}</mat-error>
      </mat-form-field>

In this field, we have added two error messages that appear if the following validations are not respected:

  • required mandatory field
  • field containing less character than what is required minlength
  • field containing more character than what is required maxlength

Error messages in this field are conditioned by the true value of the usernameControl.dirty && usernameControl.hasError('required') conditions which can be summarized as follow:

  • Display this block of text if the field identifier and empty and contains a “mandatory” type error.

The same is true for the other 2 blocks of errors.

The same is done for the password field. This field has the same validation constraints as the ID field. The tag will be modified like this

<mat-form-field >
        <input [type]="hidePassword ? 'password' : 'text'" [attr.disabled]="savedForm ? '' : null" formControlName="password" matInput placeholder="{{ 'login.password_placeholder' | translate }}">
        <a mat-icon-button matSuffix (click)="hidePassword = !hidePassword" [attr.aria-label]="'Hide password'" [attr.aria-pressed]="hidePassword">
          <mat-icon>{{hidePassword ? 'visibility_off' : 'visibility'}}</mat-icon>
        </a>
        <mat-error *ngIf="passwordControl.dirty && passwordControl.hasError('required')">{{ 'error.required' | translate }}</mat-error>
        <mat-error *ngIf="passwordControl.dirty && passwordControl.hasError('minlength')">{{ 'error.minlength' | translate }}</mat-error>
        <mat-error *ngIf="passwordControl.dirty && passwordControl.hasError('maxlength')">{{ 'error.maxlength' | translate }}</mat-error>
      </mat-form-field>

In both cases, we added [attr.disabled]="savedForm ? '' : null" [readonly]="savedForm" which sets the field to read only and disables the field if the savedForm property is set to true (i.e., Form being processed).

HTML Form – Added Reactive forms.

As in the form fields seen above, a [disabled] property is added to the validation button. Here’s how the html code of this button looks.

<button mat-raised-button color="primary"  [disabled]="loginForm.invalid || savedForm">
        <fa-icon *ngIf="savedForm" [icon]="['fas', 'spinner']" [spin]="true"></fa-icon>
        {{ 'login.button' | translate }}
</button>

It’s logical to disable the validation button to avoid the uncommanded click on this button in these two cases.

The following code:

<fa-icon *ngIf="savedForm" [icon]="['fas', 'spinner']" [spin]="true"></fa-icon>

is an animated fontawesome icon to the left of the button text that appears only (thanks to the ngIf property*) if the form is validated.

This is a way to show the user that their application is being processed.

Typescript – Validation of the form and connection to an api in nodeexpress-js

As a reminder, there are two methods to control (create, validate) a form with Angular:

  • by a template
  • by the code

In the remainder of this article, we use the code-driven method.

Import ReactiveFormsModule into app/app.module.ts

import { ReactiveFormsModule } from '@angular/forms';

then add in NgModule

@NgModule({
  imports: [
    ReactiveFormsModule
  ]
})

Template html login

Modify our html file/src/app/components/login/login.component.html – add the <form> tag with the directive formGroup directive

<form novalidate
      [formGroup]="loginForm"
      (ngSubmit)="onSubmit()"
      [hidden]="hideForm"
>

The directive (ngSubmit)= »onSubmit() » defines an onsubmit() method to call when the form is validated.

Then modify the login and password fields by adding the formControlName`** directive..

Which gives

<input [attr.disabled]="savedForm" [readonly]="savedForm" formControlName="username" matInput placeholder="{{ 'login.email_placeholder' | translate }}">

Make a small change to the password field

<input [type]="hidePassword ? 'password' : 'text'" [attr.disabled]="savedForm" formControlName="password" matInput placeholder="{{ 'login.password_placeholder' | translate }}">
<button mat-icon-button matSuffix (click)="hidePassword = !hidePassword" [attr.aria-label]="'Hide password'" [attr.aria-pressed]="hidePassword">
   <mat-icon>{{hidePassword ? 'visibility_off' : 'visibility'}}</mat-icon>
</button>

The button to the right of the password field is a clickable icon that displays the entered password clearly. It allows hiding it then by clicking again on the same icon.

Hexo

The complete password field code should look like this after adding the error message above.

<mat-form-field >
        <input [type]="hidePassword ? 'password' : 'text'" [attr.disabled]="savedForm" formControlName="password" matInput placeholder="{{ 'login.password_placeholder' | translate }}">
        <button mat-icon-button matSuffix (click)="hidePassword = !hidePassword" [attr.aria-label]="'Hide password'" [attr.aria-pressed]="hidePassword">
          <mat-icon>{{hidePassword ? 'visibility_off' : 'visibility'}}</mat-icon>
        </button>
        <mat-error *ngIf="passwordControl.dirty && passwordControl.hasError('required')">{{ 'error.required' | translate }}</mat-error>
        <mat-error *ngIf="passwordControl.dirty && passwordControl.hasError('minlength')">{{ 'error.minlength' | translate }}</mat-error>
        <mat-error *ngIf="passwordControl.dirty && passwordControl.hasError('maxlength')">{{ 'error.maxlength' | translate }}</mat-error>
      </mat-form-field>

Let’s add a progress bar before the end of the form. This is displayed if the form is being processed.

<mat-progress-bar color="accent" *ngIf="hideForm || savedForm" mode="indeterminate"></mat-progress-bar>

Hexo

Don’t forget to close the form

</form>

Our complete file /src/app/components/login/login.component.html is as follows :

<form novalidate
      [formGroup]="loginForm"
      (ngSubmit)="onSubmit()"
>
  <div fxLayout="row" fxLayoutAlign="center" class="login-main">

    <mat-card >

    <mat-card-header>
      <mat-card-title>{{ 'login.header' | translate }}</mat-card-title>
    </mat-card-header>

    <mat-card-content fxLayout="column">

      <div *ngIf="errorForm" fxLayout="row" fxLayoutGap="5px" fxLayoutAlign="center">
        <div><mat-icon aria-hidden="false" color="warn" >error</mat-icon></div>
        <div>{{ 'error.form' | translate }}</div>
      </div>

      <mat-form-field>
        <input [attr.disabled]="savedForm" [readonly]="savedForm" formControlName="username" matInput placeholder="{{ 'login.email_placeholder' | translate }}">
        <mat-error *ngIf="usernameControl.dirty && usernameControl.hasError('required')">{{ 'error.required' | translate }}</mat-error>
        <mat-error *ngIf="usernameControl.dirty && usernameControl.hasError('minlength')">{{ 'error.minlength' | translate }}</mat-error>
        <mat-error *ngIf="usernameControl.dirty && usernameControl.hasError('maxlength')">{{ 'error.maxlength' | translate }}</mat-error>
      </mat-form-field>

      <mat-form-field >
        <input [type]="hidePassword ? 'password' : 'text'" [attr.disabled]="savedForm" formControlName="password" matInput placeholder="{{ 'login.password_placeholder' | translate }}">
        <button mat-icon-button matSuffix (click)="hidePassword = !hidePassword" [attr.aria-label]="'Hide password'" [attr.aria-pressed]="hidePassword">
          <mat-icon>{{hidePassword ? 'visibility_off' : 'visibility'}}</mat-icon>
        </button>
        <mat-error *ngIf="passwordControl.dirty && passwordControl.hasError('required')">{{ 'error.required' | translate }}</mat-error>
        <mat-error *ngIf="passwordControl.dirty && passwordControl.hasError('minlength')">{{ 'error.minlength' | translate }}</mat-error>
        <mat-error *ngIf="passwordControl.dirty && passwordControl.hasError('maxlength')">{{ 'error.maxlength' | translate }}</mat-error>
      </mat-form-field>
    </mat-card-content>

    <mat-card-actions align="end">
      <button mat-raised-button color="primary"  [disabled]="loginForm.invalid || savedForm">
        <fa-icon *ngIf="savedForm" [icon]="['fas', 'spinner']" [spin]="true"></fa-icon>
        {{ 'login.button' | translate }}
      </button>
    </mat-card-actions>

    <mat-progress-bar color="accent" *ngIf="hideForm || savedForm" mode="indeterminate"></mat-progress-bar>

  </mat-card>

</div>
</form>

… on the Typescript side

Let’s modify the file /src/app/components/login/login.components.ts and import useful classes to manage

import { FormBuilder, FormControl,  FormGroup, Validators } from '@angular/forms';

Add the following properties


hideForm: boolean;
savedForm: boolean;
errorForm: boolean;
hidePassword: boolean = true;

loginForm: FormGroup;
usernameControl: FormControl;
passwordControl: FormControl;

Import useful classes such as Router, ActivatedRoute, and MatSnackBar . Then in the constructor, inject the classes that will be useful to us. Which gives you this


import { Router, ActivatedRoute } from '@angular/router';
import { MatSnackBar } from '@angular/material';
import { TranslateService } from "@ngx-translate/core";

constructor(private fb: FormBuilder,
    public service:LoginService,
    private router: Router,
    private route: ActivatedRoute,
    private _snackBar: MatSnackBar,
    private translate: TranslateService)

Add a createform method

constructor(private fb: FormBuilder) {
    this.createForm();
  }

And let’s create this method

createForm() {
    this.loginForm = this.fb.group({
      username: this.fb.control('', [Validators.required, Validators.minLength(2), Validators.maxLength(25)]),
      password: this.fb.control('', [Validators.required, Validators.minLength(2), Validators.maxLength(25)])
    });

    this.loginForm.valueChanges
      .subscribe(data => this.onValueChanged(data));

    // Reset
    this.hideForm = false;

}

Two services are created in the providers folder:

  • login
    will be used for authentication (login, logout)
  • user
    will be used to display user information connected

The creation of these services is made thanks to the following Angular commands

ng g service providers/login
ng g service providers/user

The following files are created

  • /src/app/providers/login.service.spec.ts
  • /src/app/providers/login.service.ts
  • /src/app/providers/user.service.spec.ts
  • /src/app/providers/user.service.ts

In the constructor, add the following code

// if url "/login?logout=1"
let logoutParam = this.route.snapshot.queryParamMap.get('logout');
if (logoutParam == '1') {
    this.logout();
}

// redirect to home if already logged in
if (this.service.currentUserValue) {
    this.router.navigate(['/']);
}

Lines 2 to 6 of the previous code detect the presence of the URL “?logout=1” and initiate user logout if this URL is launched.

The home page accessible via the “/” URL is considered to be a page reserved for authenticated users. The code below (line 8 – 10) redirects the user to the “/” page if it is authenticated.

The logout method is added to the LoginComponent class.

logout () {
    this.service.logout();
}

Submission of the form

The submission of the form is managed using the following onSubmit method.

onSubmit() {

    // True == form en cours de traitement
    this.savedForm = true;

    // supprimer les messages d'erreurs
    this.errorForm = false;

    // Valeurs des champs du formulaire
    let values = this.loginForm.value;

    // Service LoginService
    this.service.login(values)
      .pipe(first())
      .subscribe((result) => {

      // Token existe dans retour API
      let tokenExists = typeof result.token !== 'undefined';

      // Cacher le formulaire et preparer la redirection vers la home
      this.hideForm = true;

      if (tokenExists) {
        this.router.navigate(['/']);
        return true;
      }

      // Afficher error en cas d'echec
      this.setFormError();

    }, (err) => {
      console.log(err);
        // Afficher error en cas d'echec
        this.setFormError();
      });

  }

setFormError() {
    // Afficher le formulaire
    this.hideForm = false;

    // Le formulaire n'est plus en cours de traitement
    this.savedForm = false;

    // Afficher les messages d'erreurs
    this.errorForm = true;

    // Afficher notif dans snackbar
    let msgSnack = this.translate.instant('error.form');
    this._snackBar.open(msgSnack, null, {
      duration: 5000,
    });
  }

Line 49 to 53 displays an error message as a notification at the bottom of the page as on this screenshot.

Hexo

Most of the processing is done in the LoginService service.

File /src/app/providers/login.service.ts

Add 2 login and logoutmethods.

The login method

This method connects to the URL ${AppConfig.apiUrl}/user/login to the POST method and passes the login information=&password= into the data variable. A token is returned by the URL in case of success. This token will be saved in the localStorage.

The variable ${AppConfig.apiUrl}is defined in the file/src/environments/environment.ts as apiUrl: 'http://localhost:3008/api'

The contents of this file are as follows:

export const AppConfig = {
  production: false,
  environment: 'LOCAL',
  apiUrl: 'http://localhost:3008/api'
};

the apiUrl variable calls an URL which is a nodejs application. Once the connection is successful, the token returned by the URL will be saved in localstorage via localStorage.setItem

login (data): Observable<any> {
    // console.log(data);
    return this.http.post<any>(`${AppConfig.apiUrl}/user/login`, data, httpOptions)
      .pipe(map(user => {
        // store user details and jwt token in local storage to keep user logged in between page refreshes
        localStorage.setItem('user', JSON.stringify(user));
        this.currentUserSubject.next(user);
        return user;
      }));
  }

The logout method

This method when it deletes the local variable in localStorage then destroys the variable currentUserSubject

logout() {
    // remove user from local storage to log user out
    localStorage.removeItem('user');
    this.currentUserSubject.next(null);
  }

Complete code /src/app/providers/login.service.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { User } from '../models/user';
import { map, tap  } from "rxjs/operators";

import { AppConfig } from '../../environments/environment';

const httpOptions = {
  headers: new HttpHeaders({
    'Content-Type':  'application/json'
  })
};

@Injectable({
  providedIn: 'root'
})
export class LoginService {

  private currentUserSubject: BehaviorSubject<User>;
  public currentUser: Observable<User>;

  constructor(private http: HttpClient) {
    this.currentUserSubject = new BehaviorSubject<User>(JSON.parse(localStorage.getItem('user')));
    this.currentUser = this.currentUserSubject.asObservable();
  }

  public get currentUserValue(): User {
    return this.currentUserSubject.value;
  }

  login (data): Observable<any> {
    // console.log(data);
    return this.http.post<any>(`${AppConfig.apiUrl}/user/login`, data, httpOptions)
      .pipe(map(user => {
        console.log(user);
        // store user details and jwt token in local storage to keep user logged in between page refreshes
        localStorage.setItem('user', JSON.stringify(user));
        this.currentUserSubject.next(user);
        return user;
      }));
  }

  logout() {
    // remove user from local storage to log user out
    localStorage.removeItem('user');
    this.currentUserSubject.next(null);
  }
}

Let’s create the/src/app/models/user.ts file

export class User {
  id: number;
  username: string;
  password: string;
  token?: string;
}

Here’s what our sign-in form should look like

Hexo

In case of error after validation

Hexo

If successful

Hexo

Sources: https://github.com/rabehasy/angular-electron/tree/step2

Translated by A.A