Angular-ElectronJS — Login API REST jwt

  • Nettoi du code et factorisation
  • Messages d’erreurs
  • Formulaire HTML — Ajout du reactive forms.
  • Typescript — Validation du formulaire et connexion à une api en nodeexpress-js

Nettoi du code et factorisation

Faisons un peu de ménage dans notre code.

Modules supplémentaires

En premier lieu, nous allons modifier l’utilisation de fontawesome grâce au module fontawesome pour Angular .

<fa-icon icon="['fas', 'spinner']" spin="true"></fa-icon>
npm uninstall @fortawesome/fontawesome-free --save
$fa-font-path : '../node_modules/@fortawesome/fontawesome-free/webfonts';
@import '../node_modules/@fortawesome/fontawesome-free/scss/fontawesome';
  • @fortawesome/fontawesome-svg-core
  • @fortawesome/free-solid-svg-icons
  • @fortawesome/angular-fontawesome
npm install @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/angular-fontawesome --save
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';
@NgModule({
imports: [
// Material
MatSidenavModule,
MatButtonModule,
MatToolbarModule,
MatIconModule,
MatCardModule,
MatListModule,
MatInputModule,
MatFormFieldModule,
MatProgressBarModule,
MatSnackBarModule,
MatExpansionModule,
FlexLayoutModule,
ReactiveFormsModule,
// Fontawesome
FontAwesomeModule
]
});

Messages d’erreurs

L’affichage des messages d’erreurs du formulaire de connexion se fait à 2 endroits:

  • dans le fichier /src/app/components/login/login.component.html
  • et dans typescript du component /src/app/components/login/login.component.ts

…Dans le fichier login.component.html

Ajoutons un message d’erreur sur la validation du formulaire. Ce message se présentera lorsque le couple identifiant/mot de passe ne correspondent pas au couple enregistré en base de données.

<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 ? '' : 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>
  • champ obligatoire required
  • champ contenant moins de caractère que ce qui est requis minlength
  • champ contenant plus de caractère que ce qui est requis maxlength
  • Afficher ce bloc de texte si le champ identifiant et vide et contient une erreur de type “obligatoire”
<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>

Formulaire HTML — Ajout du reactive forms.

Comme dans les champs de formulaire vus plus haut, on ajoute une propriété [disabled] au bouton de validation. Voici comment se présente le code html de ce bouton.

<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>
  • loginForm.invalid
  • Formulaire invalide
  • savedForm Formulaire en cours de traitement.
<fa-icon *ngIf="savedForm" [icon]="['fas', 'spinner']" [spin]="true"></fa-icon>

Typescript — Validation du formulaire et connexion à une api en nodeexpress-js

Pour rappel, il existe deux méthodes permettant de piloter (créer, valider) un formulaire avec Angular:

  • par un template
  • par le code
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
imports: [
ReactiveFormsModule
]
})

Template html login

Modifions notre fichier html /src/app/components/login/login.component.html — ajoutons la balise <form> avec la directive formGroup

<form novalidate
[formGroup]="loginForm"
(ngSubmit)="onSubmit()"
[hidden]="hideForm"
>
<input [attr.disabled]="savedForm" [readonly]="savedForm" formControlName="username" matInput placeholder="{{ 'login.email_placeholder' | translate }}">
<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-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-progress-bar color="accent" *ngIf="hideForm || savedForm" mode="indeterminate"></mat-progress-bar>
</form>
<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>

… côté Typescript

Modifions le fichier /src/app/components/login/login.components.ts et importons les class utiles permettant de gérer les formulaires

import { FormBuilder, FormControl,  FormGroup, Validators } from '@angular/forms';
hideForm: boolean;
savedForm: boolean;
errorForm: boolean;
hidePassword: boolean = true;
loginForm: FormGroup;
usernameControl: FormControl;
passwordControl: FormControl;
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)
constructor(private fb: FormBuilder) {
this.createForm();
}
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;
}
  • login servira à l’authentification (login, logout)
  • user servira à afficher les informations de l’utilisateur connecté
ng g service providers/login
ng g service providers/user
  • /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
// 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(['/']);
}
logout () {
this.service.logout();
}

Soumission du formulaire

La soumission du formulaire se gère grâce à la méthode onSubmitsuivante.

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,
});
}

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

On ajoute 2 méthodes login et logout.

La methode login

Cette méthode se connecte à l’URL ${AppConfig.apiUrl}/user/login en méthode POST et passe les informations login=&password= dans la variable data. Un token est retournée par l'URL en cas de succes. Ce token sera enregistrée dans le localStorage.

export const AppConfig = {
production: false,
environment: 'LOCAL',
apiUrl: 'http://localhost:3008/api'
};
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;
}));
}

La méthode logout

Cette méthode quand à elle supprime la variable locale dans localStorage ensuite détruit la variable currentUserSubject

logout() {
// remove user from local storage to log user out
localStorage.removeItem('user');
this.currentUserSubject.next(null);
}
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);
}
}
export class User {
id: number;
username: string;
password: string;
token?: string;
}

--

--

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store