Angular-ElectronJS – canActivate, interceptors, custom themes et tailwindcss


Dans l'article précédent, nous avons vu comment valider un formulaire d’identification. On sait aussi comment enregistrer le token envoyé par le serveur si la connexion réussit. On est capable également de gérer les différents types de messages d’erreurs, soit au niveau de chaque champ (identifiant et mot de passe) ou lorsqu’on reçoit un retour qui ne nous plait pas de la part du serveur.

Nous allons cette fois-ci protéger certaines pages. En effet, celles-ci doivent être visible uniquement par les personnes connectées. D’où l’utilité de notre page d’identification.

Protéger implique 2 actions simples :

  • Si on essaie d’y accéder à la page protégée. On doit être redirigé vers une autre page (en l’occurence la page d’identification).
  • Si on est déja connecté et qu’on va sur la page d’identification. On peut choisir d’afficher un message au lieu du formulaire ou alors de rediriger l’utilisateur vers une page réservée aux personnes authentifiées. Nous allons opter pour la deuxième solution .

Pour décrire le scénario :

  1. La personne saisit son identifiant et mot de passe. ✔️ (cf article)
  2. Elle clique sur le bouton de connexion ✔️ (cf article)
  3. Une fois connectée, elle sera redirigée vers la page d’accueil . ✔️
    1. Une barre d’outil s’affichera. ✔️
      1. Il lui sera possible de cliquer sur un bouton logout pour se deconnecter. ✔️
    2. Un menu burger permettra d’afficher les autres pages protégées. ✔️
  4. Nous allons peaufiner l’afficher en créant un thème personnalisé pour material
  5. Nous ajouterons également un couche css à nos éléments en utilisant tailwindcss

Redirection vers la page d’accueil une fois connecté

Ceci a déjà été abordée dans l'article précedent. Pour résumer rapidement, cette redirection se passe dans la méthode onSubmit de la classe LoginComponent –> /src/app/components/login/login.component.ts dont voici un extrait du code

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

Si on reçoit un token de la part du serveur, on considère comme étant authentifié.

On doit aussi faire l’opération inverse. Si l’utilisateur accède à la page d’identifiant en étant déja authentifié. Il faut lui rediriger à nouveau vers la page réservée aux personnes authentifiées. Ceci a également été abordée toujours dans la même classe. Et cette fois ci dans le constructeur de la classe.

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

💡 On se pose donc la question suivante : pour toutes les pages dont on veut protéger l’accès, faut-il ajouter à chaque fois une condition similaire ?

… C’est faisable et celà va marcher correctement. Mais on peut faire beaucoup plus simple. On utilisera l’interface canActivate du Router de Angular

canActivate

Il s’agit d’ajouter cette propriété dans la définition des routes – dans le fichier /src/app/app-routing.module.ts

import { AuthGuard } from './providers/auth';
const routes: Routes = [
    {
        path: '',
        component: HomeComponent,
        canActivate: [AuthGuard]
    },
    {
      path: 'login',
      component: LoginComponent
    },
    {
      path: 'event',
      component: EventComponent,
      canActivate: [AuthGuard]
    },
    {
      path: 'lieux',
      component: LieuxComponent,
      canActivate: [AuthGuard]
    }
];

Dans le bout de code ci-dessus, les routes suivants sont protégées : home, event, et lieux . Seule la route login est accessible au utilisateur anonyme.

Créons la classe AuthGuard. dans /src/app/providers/auth.ts. Voici son contenu

import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

import { LoginService } from './login.service';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(
    private router: Router,
    private loginService: LoginService
  ) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    const currentUser = this.loginService.currentUserValue;
    if (currentUser) {
      // authorised so return true
      return true;
    }

    // not logged in so redirect to login page with the return url
    this.router.navigate(['/login'], { queryParams: { returnUrl: state.url }});
    return false;
  }
}

Il est utile d’injecter la class LoginService dans cette class AuthGard. En effet, cette classe LoginService nous permet de savoir si un utilisateur est connecté ou non. Tout simplement, la methode canActivateretourne true (ligne 15-17) si un utilisateur est connecté.

C’est aussi grâce aux lignes 20-23 de cette class AuthGard qui permettra de rediriger l’utilisateur vers la page d’identification s’il n’est pas authentifié.

… Page d’accueil

Cette page se présente comme ceci

Notre nom d’utilisateur est cachée derrière le bloc rouge En cliquant sur le bouton logout dans la toolbar, on est déconnecté et redirigé vers la page d’identification. Ce bouton est affiché dans le fichier html /src/app/app.component.html comme ceci

<button id="button-logout" mat-button (click)="logout()">logout</button>

La methode logout()est définie dans /src/app/app.component.ts

logout() {
    this.loginService.logout();
    this.router.navigate(['/login']);
}

Voyons comment afficher ce nom d’utilisateur.

Cela se passe dans la method ngOnInit de la class HomeComponent /src/app/components/home/home.component.ts. Le code complet de cette classe est le suivant

import { Component, OnInit } from '@angular/core';
import { first } from 'rxjs/operators';

import { User } from '../../models/user';
import { UserService } from '../../providers/user.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {

  loading = false;
  users: User[];
  me: User;

  constructor(
    private userService: UserService
  ) {}

  ngOnInit() {
    this.loading = true;

    this.userService.me().pipe(first()).subscribe(me => {
      this.loading = false;
      this.me = me;
    });
  }

}

Nous avons déja créé la class userService mais on ne sait pas ce qu’elle contient ? /src/app/providers/user.service.ts

Pour l’instant, elle ne contient qu’une méthode me() qui aura pour rôle d’afficher les informations de l’utilisateur authentifié. Voici son contenu

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { AppConfig } from '../../environments/environment';
import { User } from '../models/user';

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

  constructor(private http: HttpClient) { }

  me() {
    console.log(`${AppConfig.apiUrl}/user/me`); // http://localhost:3008/api/user/me
    return this.http.get<User>(`${AppConfig.apiUrl}/user/me`);
  }
}

Pour rappel, la methode me() fait appel à une application en nodejs-express qui tourne sur le port 3008. Le fichier /src/environments/environment.ts contient le code suivant :

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

Dans le code html du fichier /src/app/components/home/home.component.html. On a ce code :

<div class="m-8">
   <div *ngIf="me">
     <p>Connecté en tant que : <strong>{{ me.data.username }}</strong></p>
     <p class="text-center"><a routerLink="/login" [queryParams]="{logout: 1}" class="color--white">Déconnexion</a></p>
   </div>
 </div>
{{ me.data.username }}

contient le nom d’utilisateur de la personne authentifiée.

💡 Dans l’URL http://localhost:3008/api/user/me, on ne donne pas d’identifiant ni de mot de passe. Et en plus si on actualise la page autant de fois qu’on le souhaite, on affiche toujours correctement l’utilisateur connecté. Quelle est la magie derrière ça ?

Eh ben, il s’agit d’une autre fonctionnalité … les interceptors

Interceptors

C’est un moyen d’intercepter ou de modifier la requete http globale d’une application avant le déclenchement de l’url appelée par cette requête.

Pour mettre en place, on ajoute dans /src/app/app.module.ts

import { HTTP_INTERCEPTORS  } from '@angular/common/http';
import { Interceptor } from './providers/interceptors/interceptor';
import { ErrorInterceptor } from './providers/interceptors/errorinterceptor';

Puis dans

@NgModule({
providers: [
    { provide: HTTP_INTERCEPTORS,  useClass: Interceptor, multi: true },
    { provide: HTTP_INTERCEPTORS,  useClass: ErrorInterceptor, multi: true }
  ],
  bootstrap: [AppComponent]
})

On peut ajouter autant d’interceptor qu’on veut. Nous allons créer 2 interceptors:

  • Pour ajouter le token Bearer dans l’entete des pages avant d’activer une page protégée.
  • Pour intercepter le code d’erreur . Cas d’une page 404 par exemple ou 401 (forbidden)

Interceptor Bearer

Créons le fichier /src/app/providers/interceptors/interceptor.ts

Il contient le code suivant

import { Injectable } from '@angular/core';

import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor
} from '@angular/common/http';

import { LoginService } from '../login.service';
import { Observable } from 'rxjs';

@Injectable()
export class Interceptor implements HttpInterceptor {

  constructor(private service: LoginService) {}

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // add authorization header with jwt token if available
    let currentUser = this.service.currentUserValue;
    if (currentUser && currentUser.token) {
      request = request.clone({
        setHeaders: {
          Authorization: `Bearer ${currentUser.token}`
        }
      });
    }

    return next.handle(request);
  }

}

Cette class dépend aussi de la class LoginService qui contient les informations sur un utilisateur authentifié.

Grâce à cette information, il nous est possible de récupérer le token de cet utilisateur. Et d’ajouter ce token à Authorization: Bearer dans l’entete de chaque URL à protéger. Voici ce que cela donne en inspectant l’entete de l’URL

Menu Burger à gauche dans la barre d’outil

Au clic sur le burger

On afficher d’autres pages protégées .

Thème personnalisé

La création d’un thème personnalisé (ou plutôt modifier les couleurs du thème indigo-pink.css) se fait très facilement.

Nous allons faire cela dans un fichier scss personnalisé qu’on placera dans /src/assets/css/custom.scss

Il faut ajouter ce fichier au fichier /src/styles.scss en ajoutant tout simplement comme ceci

@import './assets/css/custom';

Ensuite dans /src/assets/css/custom.scss

@import '~@angular/material/theming';

@include mat-core();

$custom-primary: mat-palette($mat-red);
$custom-accent:  mat-palette($mat-pink);

$custom-theme: mat-light-theme($custom-primary, $custom-accent);

@include angular-material-theme($custom-theme);

La ligne

@include mat-core();

sert à inclure les styles communs à material

Les lignes

$custom-primary: mat-palette($mat-red);
$custom-accent: mat-palette($mat-pink);

définissent les palettes pour notre thème Material.

Tailwindcss;

Tailwindcss est un framework css. Il est différent des autres framework connus tels que Boostrap, bulma … La différence notoire se situe dans la façon de procéder. Avec tailwindcss, on décide de ce qu’on veut mettre dans les propriétés css.

Installons cette librairie puissante pour en profiter de la mise en forme définie dans les propriétés css.

npm install tailwindcss --save-dev

Lançons la commande suivante pour créer un fichier de config utile à tailwindcss

npx tailwind init

On crée un fichier /src/tailwindcss-build.scss et on ajoute ceci

@tailwind components;
@tailwind utilities;

On compile le fichier /src/tailwind.scss qu’on appellera ensuite dans Angular. La compilation se fait grâce à la commande suivante :

npx tailwind build ./src/tailwind-build.scss -o ./src/tailwind.scss

A partir de là, on peut importer ce fichier /src/tailwind.scss dans /src/styles.scss comme ceci

@import './tailwind';

Et on va l’utiliser par exemple dans notre page /src/app/components/event/event.component.html pour ajouter une marge à la <div>

Comme ceci

<div class="m-8">
    Event page
</div>

Ok je vous avoue qu’on aurait pu se passer de tailwindcss pour cet exemple si simple. On aurait pu ajouter cette propriété css dans notre styles.scss et le tour est joué. Le choix de Tailwindcss dans cet exemple, n’a pas vraiment de sens. La ou il joue un rôle essentiel est quand il s’agit de réutiliser une propriété multiple à une balise.