Authentication avec ngrx angular 16 et bootstrap

Nous allons voir comment installer et configurer une application angular avec ngrx et bootstrap.

Configuration

Création du workspace angular

ng new workspace –create-application=false

Ajout de l’application

Dans le workspace, ajouter une application : 

ng g app back-office.

Installation de ngrx

ng add @ngrx/store

Installation de ngrx

Ajout de bootstrap

ng add ngx-bootstrap

Création des différentes classes

Ajouter du reducer principal

import { ActionReducerMap } from "@ngrx/store";




export interface ApplicationState {
}


export const reducers: ActionReducerMap<ApplicationState> = {
}
Installation de ngrx effect
ng add @ngrx/effects

Puis ajoute le dans le module parent

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    StoreModule.forRoot({
    }, {}),
    BrowserAnimationsModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
Création du module dédié à l’authentification

ng g m features/authentication

Ajout du composant de login

ng g c features/authentification/login

Puis en ajoutant le ReactiveFormModule dans le module d’authentification 

@NgModule({
  declarations: [
    LoginComponent
  ],
  imports: [
    CommonModule,
    ReactiveFormsModule,
  ]
})
export class AuthenticationModule { }

Configure le formulaire pour avoir un formGroup

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent {
  loginForm = inject(FormBuilder).group({
    username: ['', Validators.required],
    password: ['', Validators.required]
  });


}

Configuration des routes 

Création du guard d’authentification, avec redirection

Ajoute un guard qui va contrôler l’authentification

ng g guard features/authentification/is-authenticated

Ajout de la route de login

Dans le module de routing pour l’authentication, ajoute la route pour le login

const routes: Routes = [{
  path: 'login',
  component: LoginComponent
}];


@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class AuthenticationRoutingModule { }
@NgModule({
  declarations: [
    LoginComponent
  ],
  imports: [
    CommonModule,
    ReactiveFormsModule,
    AuthenticationRoutingModule
  ]
})
export class AuthenticationModule { }

Ajout des routes enfants protégées par un guard

Dans le AppRoutingModule, ajoute les routes de protection avec le guard

const routes: Routes = [
  { path: '', redirectTo: 'games', pathMatch: 'full' },
  {
    path: '',
    canActivate: [IsAuthenticatedGuard],
    children: [
// on mettra les routes enfants protégées par login ici      
    ]
  }];


@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
Paramétrage

Création du service d’authentication

Le guard va appeler notre service d’authentification

ng g s features/authentification/auth

Ajout des actions de ngrx

Pour que tout 

Ajoutons un fichier dédié.

Mais avant ça, créons les interfaces nécessaires

export type ApiAuthUser = {
  access_token: string,
}


export type ApiError = {
  message: string,
  statusCode: number
}


export interface ToLogUser {
  username: string,
  password: string
}


export interface WithToken {
  token: string
}


export interface WithIsLogged {
  isLogged: boolean
}


export interface AuthenticatedUser extends ToLogUser, WithIsLogged {


}


export interface AuthenticateStateWithToken extends AuthenticatedUser, WithToken {
}


export interface ToLogUserWithToken extends ToLogUser, WithToken {
}


Création du fichier des actions

import { createAction, props } from "@ngrx/store";
import { ApiError, AuthenticateStateWithToken, ToLogUser } from "../../models";


export const isLogginAction = createAction('[Auth] Is Loggin', props<{ user: ToLogUser }>());
export const isLogginSuccessAction = createAction('[Effect] Is Loggin', props<{ user: AuthenticateStateWithToken }>());
export const isLogginFailureAction = createAction('[Effect] Is Loggin failed', props<{ error: ApiError }>());






export const isLogoutAction = createAction('[Auth] Is Logout');

Paramétrage du reducer d’authentication

Dans notre reducer, nous allons gérer la réussite d’authentification

et les erreurs d’authentification

import { isLogginFailureAction } from './actions/index';
import { createReducer, on } from '@ngrx/store';
import { ApiError, AuthenticatedUser, AuthenticateStateWithToken } from '../models';
import { isLogginAction, isLogginSuccessAction } from './actions';


export const authenticationFeatureKey = 'authentication';


export interface AuthenticationState {
  user?: AuthenticatedUser,
  error?: ApiError
}


export const initialState: AuthenticationState = {
  user: undefined
};


export const authenticationReducer = createReducer(
  initialState,
  on(isLogginSuccessAction, (state, { user }) => ({ ...state, user: { ...user, isLogged: true } })),
  on(isLogginFailureAction, (state, { error }) => ({ ...state, user: { username: '', password: '', isLogged: false }, error }))
);

Mise à jour du Guard d’authentification

Prenons le soin de créer un service dédié d’appel à notre api (sans map, ou autre operator).

ng g s features/authentication/services/auth-layer

import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiAuthUser, ToLogUser } from '../models';
import { HttpClient } from '@angular/common/http';


@Injectable({
  providedIn: 'root'
})
export class AuthLayerService {
  private readonly httpClient = inject(HttpClient);


  authenticate(user: ToLogUser): Observable<ApiAuthUser> {
    return this.httpClient.post<ApiAuthUser>('http://localhost:3000/auth/login', {
      username: user.username,
      password: user.password
    })
  }
}

Pour utiliser notre Guard, nous allons mettre à jour notre service d’authentification

import { inject, Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { map, Observable } from 'rxjs';
import { ToLogUser, ToLogUserWithToken } from '../models';
import { isLogginAction } from '../store/actions';
import { selectUserAuthWithFailure, selectUserIsLogged } from '../store/selectors';
import { AuthLayerService } from './auth-layer.service';


@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private readonly store = inject(Store);
  private readonly layer = inject(AuthLayerService);


  selectAuthError(): Observable<any | undefined> {
    return this.store.select(selectUserAuthWithFailure);
  }


  login(user: ToLogUser): void {
    this.store.dispatch(isLogginAction({ user }));
  }


  authenticate(user: ToLogUser): Observable<ToLogUserWithToken> {
    return this.layer.authenticate({
      username: user.username,
      password: user.password
    }).pipe(
      map(apiUser => ({
        token: apiUser.access_token,
        username: user.username,
        password: user.password
      })
    ));
  }


  get isLogged(): Observable<boolean | undefined> {
    return this.store.select(selectUserIsLogged);
  }
}

Il va être le proxy avec le store, et permettre l’appel à notre service d’appel à notre api d’authentication.

Nous pouvons appeler notre service dans notre Guard

import { inject, Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { map, Observable, take, tap } from 'rxjs';
import { AuthService } from '../services/auth.service';


@Injectable({
  providedIn: 'root'
})
export class IsAuthenticatedGuard implements CanActivate {
  private readonly authService = inject(AuthService);
  private router = inject(Router);


  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return this.authService.isLogged.pipe(
      take(1),
      map(isLogged => isLogged ? true : false),
      tap(isLogged => !isLogged && this.router.navigate(['/login']))
    );
  }
}


Notons ici la redirection si nous ne sommes pas connectés.

Ajout d’un effect pour se connecter à notre api et gérer la redirection vers la page d’accueil

import { inject, Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { concatMap, map, tap, catchError } from 'rxjs/operators';
import { AuthService } from '../../services/auth.service';
import { isLogginAction, isLogginFailureAction, isLogginSuccessAction } from '../actions';
@Injectable()
export class AuthEffect {
  private readonly authService = inject(AuthService);
  private readonly actions$ = inject(Actions);
  private readonly router = inject(Router);


  onLoggedIn$ = createEffect(() => this.actions$.pipe(
    ofType(isLogginAction),
    concatMap(action => this.authService.authenticate(action.user)),
    map(user => isLogginSuccessAction({ user: { ...user, isLogged: true } })),
    catchError(error => {
      return of(isLogginFailureAction({ error }));
    })
  ));


  onLoggedInSuccess$ = createEffect(() => this.actions$.pipe(
    ofType(isLogginSuccessAction),
    tap(action => this.router.navigate(['/'])
    )),
    { dispatch: false }
  );
}

Notons le dispatch à false dans le cas du deuxième effect, car nous ne renvoyons aucune action dans le pipe des actions.

Pour que l’effect soit pris en compte, pensez bien Ă  l’ajouter dans le module d’authentication

import { ReactiveFormsModule } from '@angular/forms';
import { EffectsModule } from '@ngrx/effects';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { StoreModule } from '@ngrx/store';
import { authenticationFeatureKey, authenticationReducer } from './store/store.reducer';
import { LoginComponent } from './login/login.component';
import { AuthenticationRoutingModule } from './authentication-routing.module';
import { AuthEffect } from './store/effects';
import { AlertModule } from 'ngx-bootstrap/alert';


@NgModule({
  declarations: [
    LoginComponent
  ],
  imports: [
    CommonModule,
    ReactiveFormsModule,
    StoreModule.forFeature(authenticationFeatureKey, authenticationReducer),
    EffectsModule.forFeature([AuthEffect]),
    AuthenticationRoutingModule,
    AlertModule.forRoot()
  ]
})
export class AuthenticationModule { }

Ici, on peut aussi voir l’appel Ă  l’AlertModule de bootstrap.

Paramétrage du composant de login

Pour se faire, nous allons avoir besoin de selectors, basés sur un selector feature (vu qu’on a pensé notre système sur du Feature Module / Feature Store).

import { createFeatureSelector, createSelector } from "@ngrx/store";
import { authenticationFeatureKey, AuthenticationState } from "../store.reducer";


export const selectAuthUserSelector = createFeatureSelector<AuthenticationState>(authenticationFeatureKey);
export const selectUserIsLogged = createSelector(selectAuthUserSelector, (state: AuthenticationState) => state.user?.isLogged);
export const selectUserAuthWithFailure = createSelector(selectAuthUserSelector, (state: AuthenticationState) => state.error);

Notons le dernier selector qui va afficher un message d’erreur quand l’effect va nous dispatcher l’action vers le reducer.

Côté template, nous avons un formGroup et une alert de type bootstrap qui va s’afficher si une erreur est survenue lors de l’authentification

<alert type="danger" *ngIf="errorOnLogin$ | async as error">
  <strong>Oh snap!</strong> {{error.message}}
</alert>


<div [formGroup]="loginForm" id="login" class="text-center">
  <form class="form-signin">
    <img class="mb-4" src="https://getbootstrap.com/docs/4.0/assets/brand/bootstrap-solid.svg" alt="" width="72"
      height="72">
    <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
    <label for="inputEmail" class="sr-only">Email address</label>
    <input type="email" id="inputEmail" class="form-control" placeholder="Email address" formControlName="username"
      autofocus>
    <label for="inputPassword" class="sr-only">Password</label>
    <input type="password" id="inputPassword" class="form-control" placeholder="Password" formControlName="password">
    <div class="checkbox mb-3">
      <label>
        <input type="checkbox" value="remember-me"> Remember me
      </label>
    </div>
    <button class="btn btn-lg btn-primary btn-block" type="button" (click)="clickToLogin()">Sign in</button>
    <p class="mt-5 mb-3 text-muted">&copy; 2017-2018</p>
  </form>
</div>

Notons ici que nous utilisons les pipe async.

Côté component, nous récupérons les observables, sans passer par OnInit, ou bien le constructor, directement via la méthode inject.

import { HttpClient } from '@angular/common/http';
import { Component, inject } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { ToLogUser } from '../models';
import { AuthService } from '../services/auth.service';


@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent {
  private readonly authService = inject(AuthService);
  private readonly router = inject(Router);
  loginForm = inject(FormBuilder).group({
    username: ['', Validators.required],
    password: ['', Validators.required]
  });
  errorOnLogin$ = this.authService.selectAuthError();


  private client = inject(HttpClient);


  clickToLogin(): void {
    this.authService.login(this.loginForm.value as ToLogUser);
  }
}

Et voila ! 🙂

Ca a Ă©veillĂ© ta curiositĂ© ? ça te dit d’aller plus loin avec une formation Angular avancĂ© ?

Notre adresse

1 rue du guesclin
44000 Nantes

Notre téléphone

+33 2 79 65 52 87

Société

DevToBeCurious SARL
84860163900018 - Nantes B 848 601 639