12. Login / Interceptor / Guard

0

Lors du chapitre précédent, nous avons vu comment utiliser Angular Material. Aujourd’hui, nous allons voir comment intégrer un login à notre application. Pour cela, j’ai préparé une API très simple que vous pouvez retrouver ici:

https://gitlab.com/simpletechprod1/playing_cards_backend

Au programme d’aujourd’hui:

  • On va commencer par créer une page de login;
  • je vous présenterai l’API que j’ai préparé pour notre exercice;
  • on va créer un service qui va interagir avec notre API à l’aide de HttpClient
  • on verra comment intercepter les requêtes en direction de notre API pour y injecter des entêtes d’authentification;
  • et pour finir, on regardera comment protéger notre liste de monstres d’un accès non authentifié, via des gardes de routes.

Pour rappel, cette série de vidéos s’inscrit dans une longue lignée de vidéos dont le fil rouge est la création d’une application de visualisation et gestion de cartes à collectionner de type Pokémon, Magic, Yu-Gi-Oh! ou autres.

Résultat final projet Angular

Pour commencer, on va créer notre page de login en tapant la commande suivante dans notre terminal:

Bash
ng g c pages/login

Ouvrons le fichier « login.component.html » qui vient d’être créé et écrivons notre formulaire de login:

login.component.html
<div id="container">
    <form (submit)="login()" [formGroup]="loginFormGroup">
        <h3>Login</h3>
        <mat-form-field>
            <mat-label>Username</mat-label>
            <input matInput formControlName="username">
        </mat-form-field>
        <mat-form-field>
            <mat-label>Password</mat-label>
            <input matInput type="password" formControlName="password">
        </mat-form-field>
        <button mat-flat-button [disabled]="loginFormGroup.invalid">Login</button>
        @if (invalidCredentials) {
            <mat-error>Invalid credentials</mat-error>
        }
    </form>
</div>

Ajoutons un peu de css au fichier « login.component.css »:

login.component.css
#container {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
}

form {
    background-color: white;
    padding: 20px;
    border-radius: 10px;
    display: flex;
    flex-direction: column;
    width: 400px;
}

mat-error {
    text-align: center;
}

Créons le « FormGroup » « loginFormGroup » ainsi que la méthode « login » dans le fichier « login.components.ts ». On aura aussi une propriété « invalidCredentials » qui est utilisé dans le fichier HTML afin d’afficher un message en cas d’erreur lors du login :

login.components.ts
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input';

@Component({
    selector: 'app-login',
    standalone: true,
    imports: [ReactiveFormsModule, MatInputModule, MatButtonModule],
    templateUrl: './login.component.html',
    styleUrl: './login.component.css'
})
export class LoginComponent {
    private formBuilder = inject(FormBuilder);
    loginFormGroup = this.formBuilder.group({
        'username': ['', [Validators.required]],
        'password': ['', [Validators.required]]
    });

    invalidCredentials = false;

    login() {
    }
}

N’oublions pas de créer une route pour ce composant dans notre fichier « app.routes.ts »:

app.routes.ts
import { Routes } from '@angular/router';
import { MonsterListComponent } from './pages/monster-list/monster-list.component';
import { MonsterComponent } from './pages/monster/monster.component';
import { NotFoundComponent } from './pages/not-found/not-found.component';
import { LoginComponent } from './pages/login/login.component';

export const routes: Routes = [{
    path: '',
    redirectTo: 'home',
    pathMatch: 'full'
},{
    path: 'home',
    component: MonsterListComponent
},{
    path: 'login',
    component: LoginComponent
}, {
    path: 'monster',
    children: [{
        path: '',
        component: MonsterComponent
    }, {
        path: ':id',
        component: MonsterComponent
    }]
}, {
    path: '**',
    component: NotFoundComponent
}];

Avant de passer à l’implémentation de notre service ajoutons encore un modèle à notre projet, qui représentera notre User. Pour cela, créez un fichier « user.model.ts » dans le dossier « models ». Dans ce fichier, on va créer une classe « User » qui aura trois propriétés: « username », « firstName » et « lastName »:

user.model.ts
export class User {
    username: string = '';
    firstName: string = '';
    lastName: string = '';
}

Nous pouvons maintenant passer à l’implémentation de notre service, qui va se charger d’interagir avec notre API. Commençons alors par jeter un coup d’oeil à cette API et aux appels qu’on va pouvoir utiliser dans notre service:

3 Login APIs

Comme vous pouvez le voir, nous avons trois appels API à notre disposition:

Login POST API

La première étant un POST à l’url « sessions/login/ » auquel on passe notre « username », « password » et qui nous retourne un token qu’on devra utiliser dans tous les autres appels API ainsi que les informations de l’utilisateur en question;

Logout API

Deuxièmement, on a l’API « session/logout/ » sur laquelle on pourra faire un GET afin d’invalider le token qu’on a reçu lors du login.

GET user API

Troisièmement, nous avons une URL « session/me/ » sur laquelle on pourra aussi faire un GET et qui nous retourne nos informations utilisateur.

Maintenant qu’on s’est familiarisé avec notre API, regardons comment configurer « HttpClient » afin de pouvoir l’utiliser pour interagir avec cette API. Pour commencer, on va devoir ajouter le provider “provideHttpClient” à la liste de “providers” de notre « appConfig » dans le fichier « app.config.ts ».

“provideHttpClient” est à importer de “@angular/common/http”:

app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideAnimationsAsync(), provideHttpClient()]
};

Ce provider va nous mettre le service « HttpClient » à disposition et va nous permettre de le configurer, mais ça on le verra un peu plus tard.

Une fois notre provider configuré, on va pouvoir créer notre service. Pour cela, on retourne dans notre terminal et on tape:

Bash
ng g s services/login/login

Ouvrons le fichier « login.service.ts » qui vient d’être créé. Pour commencer, on va injecter notre « HttpClient » dans une propriété qu’on va appeler « http », et on va aussi créer un signal « user », qui va être de type “User | null | undefined”. La valeur « null » voudra dire que l’utilisateur n’est pas logé, et la valeur « undefined » signifiera qu’on n’a pas encore vérifié si l’utilisateur est connecté ou non.

login.service.ts
import { inject, Injectable, signal } from '@angular/core';
import { User } from '../../models/user.model';
import { HttpClient } from '@angular/common/http';

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

    private http = inject(HttpClient);

    user = signal<User | undefined | null>(undefined);

}

Pour faire appel à notre API de login, on va devoir passer un « username » et « password » en paramètre. Créons donc une interface avec ces deux propriétés:

login.service.ts
...
export interface LoginCredentials {
    username: string,
    password: string
}

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

Implémentons maintenant la méthode login qui prend en paramètre nos identifiants de type « LoginCredentials ». Notre API de login consiste en un post à faire sur l’URL « sessions/login ». Pour faire un tel appel API, on peut utiliser la méthode « post » de « HttpClient en lui passant en premier paramètre l’URL sur laquelle on veut faire le « post », et en deuxième paramètre les données à envoyer.

Le « post » retourne un « observable », et avant de retourner le résultat de cet « observable », on va vouloir extraire le token qu’on devra utiliser dans les autres appels API et on va aussi lire et stocker l’utilisateur dans notre signal. Pour cela, on va utiliser des méthodes « RxJS », en l’occurrence la méthode « pipe », la méthode « tap » et la méthode « map ».

Si vous ne connaissez pas « RxJS », ne vous en faites pas, on en parlera en détail dans un autre chapitre. Pour aujourd’hui, rappelez-vous que « pipe » permet d’enchaîner des opérations suite à un résultat d’un « observable », « tap » est l’une de ces opérations qui permet de prendre le résultat d’un observable et d’executer du code, sans modifier le résultat de l’ »observable » et « map » permet de prendre le résultat de l' »observable » et de la modifier avant de retourner le nouveau résultat. Donc ici, grâce à « tap », on va pouvoir récupérer le « user » et le « token » et grâce à « map », on va pouvoir renvoyer l’utilisateur en tant que résultat de notre « observable »:

login.service.ts
import { inject, Injectable, signal } from '@angular/core';
import { User } from '../../models/user.model';
import { HttpClient } from '@angular/common/http';
import { Observable, tap, map } from 'rxjs';

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

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

    private http = inject(HttpClient);
    private BASE_URL = 'http://localhost:8000';
    user = signal<User | undefined | null>(undefined);

    login(credentials: LoginCredentials): Observable<User | null | undefined> {
        return this.http.post(this.BASE_URL + '/sessions/login/', credentials).pipe(
            tap((result: any) => {
                localStorage.setItem('token', result['token']);
                const user = Object.assign(new User(), result['user']);
                this.user.set(user);
            }),
            map((result: any) => { return this.user(); })
        )
    }

}

Petite remarque: notre API tourne en localhost sur le port 8000, et donc notre API est joignable à l’url http://localhost:8000/sessions/login. En ce qui concerne le stockage de notre token, on va simplement le sauvegarder dans notre « localStorage ». Ainsi, si l’utilisateur quitte notre site, on aura encore le token lors de se prochaine visite et il n’aura pas à se logger une nouvelle fois.

Créons encore les méthodes « getUser() » et « logout() » qui vont être très proches de ce que nous avons fait pour la méthode « login ». A la différence près qu’on va utiliser la méthode « get » de « httpClient ». Pour notre méthode « getUser() », on va récupérer l’utilisateur actuel et le stocker dans le signal « user », et pour la méthode « logout() » on va supprimer l’item « token » du « localStorage » et on va assigner la valeur « null » au signal « user » afin d’indiquer que l’utilisateur n’est pas connecté.

login.service.ts
import { inject, Injectable, signal } from '@angular/core';
import { User } from '../../models/user.model';
import { HttpClient } from '@angular/common/http';
import { Observable, tap, map } from 'rxjs';

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

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

    private http = inject(HttpClient);
    private BASE_URL = 'http://localhost:8000';
    user = signal<User | undefined | null>(undefined);

    login(credentials: LoginCredentials): Observable<User | null | undefined> {
        return this.http.post(this.BASE_URL + '/sessions/login/', credentials).pipe(
            tap((result: any) => {
                localStorage.setItem('token', result['token']);
                const user = Object.assign(new User(), result['user']);
                this.user.set(user);
            }),
            map(() => this.user())
        );
    }

    getUser(): Observable<User | null | undefined> {
        return this.http.get(this.BASE_URL + '/sessions/me/').pipe(
            tap((result: any) => {
                const user = Object.assign(new User(), result);
                this.user.set(user);
            }),
            map(() => this.user())
        );
    }

    logout(): Observable<any> {
        return this.http.get(this.BASE_URL + '/sessions/logout/').pipe(
            tap(() => {
                localStorage.removeItem('token');
                this.user.set(null);
            })
        );
    }

}

Notre « LoginService » est prêt. Nous pouvons maintenant l’utiliser dans notre page de « login ». Ouvrons de nouveau notre fichier « login.component.ts » et implémentons la méthode « login » que nous avions laissé vide à l’instant!

Injectons d’abord notre « LoginService », ainsi que le « Router » dans notre composant. Ensuite, dans notre méthode « login », on va faire un « this.loginService.login() » auquel on va passer les données de notre formulaire en paramètre. Dans le « subscribe », si on obtient un résulat, le login s’est bien passé et on peut rediriger l’utilisateur vers notre page principale. Si en revanche, nous avons une erreur, on affichera le message d’erreur en passant la propriété « invalidCredentials » à « true ». Bien sûr comme d’habitude on va stocker notre « Subscription » et faire un « unsubscribe » lors de la destruction de notre composant.

login.component.ts
import { Component, inject, OnDestroy } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input';
import { LoginCredentials, LoginService } from '../../services/login/login.service';
import { Router } from '@angular/router';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-login',
  standalone: true,
  imports: [ReactiveFormsModule, MatInputModule, MatButtonModule],
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnDestroy {

  private formBuilder = inject(FormBuilder);
  private loginService = inject(LoginService);
  private router = inject(Router);

  private loginSubscription: Subscription | null = null;

  loginFormGroup = this.formBuilder.group({
    username: ['', [Validators.required]],
    password: ['', [Validators.required]]
  });

  invalidCredentials = false;

  login() {
    this.loginSubscription = this.loginService.login(this.loginFormGroup.value as LoginCredentials)
      .subscribe({
        next: () => this.navigateHome(),
        error: () => this.invalidCredentials = true
      });
  }

  navigateHome() {
    this.router.navigate(['home']);
  }

  ngOnDestroy(): void {
    this.loginSubscription?.unsubscribe();
  }
}

Maintenant si nous jetons un coup d’oeil à notre formulaire de login et entrons un nom d’utilisateur et mot de passe valide, on est bien redirigé vers notre page principale.

Login Form

Ajoutons maintenant une barre de navigation à notre application qui sera affiché sur toutes nos pages tant que l’utilisateur est connecté. Cette barre aura un bouton « home ». Pour rediriger l’utilisateur sur la page principale de l’application, un message de bienvenue avec le nom et le prénom de l’utilisateur et un bouton de logout.

Afin que cette page soit affichée sur toutes nos pages, on va l’ajouter directement dans le fichier « app.component.html » et on ne l’affichera que si le signal user de notre « LoginService » n’est ni « null », ni « undefined ».

app.component.html
@if(loginService.user()) {
    <mat-toolbar>
        <button mat-icon-button (click)="navigateHome()"><mat-icon>home</mat-icon></button>
        <p>Welcome {{loginService.user()?.firstName}} {{loginService.user()?.lastName}}</p>
        <button mat-flat-button color="warn" (click)="logout()">Logout</button>
    </mat-toolbar>
}
<router-outlet></router-outlet>

Afin que ce code fonctionne, on doit bien entendu injecter notre « loginService » dans le fichier « app.component.ts » et implémenter la méthode « logout() ». Cette méthode va se contenter d’appeler la méthode « logout » du service « loginService » et rediriger l’utilisateur sur la page de login, que le logout aie bien fonctionné ou non, car le logout ne retourne une erreur que si l’utilisateur tente de se déconnecter alors qu’il l’est déjà.

app.component.ts
import { Component, inject, OnInit } from '@angular/core';
import { Router, RouterOutlet } from '@angular/router';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { LoginService } from './services/login/login.service';

@Component({
    selector: 'app-root',
    standalone: true,
    imports: [RouterOutlet, MatToolbarModule, MatButtonModule, MatIconModule],
    templateUrl: './app.component.html',
    styleUrl: './app.component.css'
})
export class AppComponent {

    private router = inject(Router);
    loginService = inject(LoginService);

    logout() {
        this.loginService.logout().subscribe({
            next: _ => { this.navigateToLogin(); },
            error: _ => { this.navigateToLogin(); }
        });
    }

    navigateToLogin() {
        this.router.navigate(['login']);
    }

    navigateHome() {
        this.router.navigate(['home']);
    }

}

Ajoutons aussi un peu de « css » à notre barre de navigation dans le fichier « app.component.css »:

app.component.css
mat-toolbar {
    display: flex;
    align-items: center;
    padding: 0 20px;
}

mat-toolbar p {
    flex-grow: 1;
    margin: 0;
}

Avant de tester ce code, nous devons encore trouver un moyen pour intercepter nos requêtes et y ajouter notre token API, si un tel token existe dans le « localStorage ». Pour créer un intercepteur de requêtes « http » avec le nom « auth-token » dans un dossier qu’on va appeler « interceptors », on doit taper la commande suivante dans le terminal:

Bash
ng generate interceptor interceptors/auth-token/auth-token

Cette commande génère alors un fichier « auth-token.interceptor.ts » avec le contenu suivant:

auth-token.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';

export const tokenAuthInterceptor: HttpInterceptorFn = (req, next) => {
    return next(req);
};

Dans cette fonction, on va tout d’abord vérifier si on a un token dans le « localStorage ». Si c’est le cas, on va recopier les « headers » de notre requête et y ajouter l’entête d’autorisation. L’entête que notre API doit recevoir a la forme suivante: « ‘Authorization’: ‘Token <token>’ ».

Pour ajouter cette entête à une copie des « headers » originaux de notre requête, on peut faire un « req.headers.set » auquel on passe en paramètres le nom de l’entête à ajouter (« Authorization ») suivi de la valeur de l’entête en question, donc « Token <token> ». Ensuite, on va cloner la requête en elle-même en remplacer les « headers » de celle-ci par les nouveaux « headers » qu’on vient de créer. Pour finir, on retourne la nouvelle requête.

auth-token.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';

export const tokenAuthInterceptor: HttpInterceptorFn = (req, next) => {
    const token = localStorage.getItem('token');
    let requestToSend = req;

    if (token) {
        const headers = req.headers.set('Authorization', 'Token ' + token);
        requestToSend = req.clone({ headers: headers });
    }

    return next(requestToSend);
};

On a notre intercepteur qui est prêt à être utilisé, et pour ce faire, on va devoir retourner dans notre fichier « app.config.ts » ou va ajouter un paramètre à provideHttpClient. Le paramètre en question va être withInterceptors qu’on importe de ‘@angular/common/http’, et qui à son tour prend en paramètre les intercepteurs qu’on souhaite appliquer à nos requêtes, donc ici notre intercepteur « tokenAuthInterceptor » :

app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { tokenAuthInterceptor } from './interceptors/auth-token/auth-token.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }), 
    provideRouter(routes), 
    provideAnimationsAsync(), 
    provideHttpClient(withInterceptors([tokenAuthInterceptor]))
  ]
};

Maintenant, nous pouvons ouvrir notre application dans le navigateur et vérifier que la page de login ainsi que le bouton de logout fonctionnent bien.

Logout header

En revanche, notre application a encore un problème: si l’utilisateur navigue vers l’URL « root » ou sur une URL précise de l’application, il arrivera à voir la page en question sans pour autant être connecté.

Idéalement, on devrait avoir un moyen de vérifier si l’utilisateur est bien connecté à chaque fois qu’on tente d’accéder à une URL qui nécessite une authentification. Cela tombe bien, car Angular à un concept de gardes de route, qui permet d’exécuter des vérifications avant de naviguer vers une page spécifique. Pour créer une telle garde dans un dossier « guards » avec le nom « is-logged-in », il suffit de taper la commande suivante:

Bash
ng g guard guards/is-logged-in/is-logged-in

Afin d’exécuter cette commande, on doit encore choisir quel type de garde on veut implémenter. Etant donné que nous souhaitons vérifier si l’utilisateur peut ou non accéder à la route demandée, on doit choisir le type « CanActivate ». Cette commande génère le fichier « is-logged-in.guard.ts » suivant:

is-logged-in.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { LoginService } from '../services/login/login.service';
import { catchError, map, of } from 'rxjs';

export const isLoggedInGuard: CanActivateFn = (route, state) => {

    return true;

};

Cette fonction retourne « true » si la route peut être accédée. Etant donné que nous souhaitons vérifier si l’utilisateur est connecté, nous devons retourner « true » si l’utilisateur et bien connecté et si l’utilisateur n’est pas connecté nous devrons le rediriger vers la page de login.

Tout d’abord on va créer deux constantes dans lesquelles on va injecter notre « LoginService » et le « Router ». Ensuite nous devrons vérifier trois conditions:

  1. Premièrement, si « loginService.user() » est « undefined », on doit vérifier si l’utilisateur est bien connecté ou non. Pour cela, on peut faire appel à notre API « session/me » via la méthode « loginService.getUser() ». Si un token est dans le local « storage », notre intercepteur l’ajoutera à la requête et si ce token est valide, l’appel à la méthode fonctionnera et on peut retourner « true ». Si le token n’existe pas ou n’est pas valide, on aura une erreur et on devra rediriger l’utilisateur vers la page de login.
  2. Deuxièmement, si « loginService.user() » est « null », alors l’utilisateur n’est pas connecté et nous devons le rediriger vers la page de login.
  3. Pour finir, si loginService.user() contient bien un utilisateur, alors l’utilisateur est connecté et on peut retourner « true »:
is-logged-in.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { catchError, map, of } from 'rxjs';
import { LoginService } from '../../services/login/login.service';

export const isLoggedInGuard: CanActivateFn = (route, state) => {

    const loginService = inject(LoginService);
    const router = inject(Router);

    if (loginService.user() == undefined) {
        return loginService.getUser().pipe(
            map(_ => true),
            catchError(_ => router.navigate(['login']))
        )
    }

    if (!loginService.user()) {
        router.navigate(['login']);
    }

    return true;

};

Comme avant, la méthode « pipe » qu’on utilise sur « getUser » permet d’enchaîner des traitements sur le résultat de l’ »observable » retourné par « getUser() ». Si l’API retourne un résultat, on va exécuter la méthode « map » afin de retourner « true » en tant que résultat et d’indiquer que l’URL peut bien être accédée par l’utilisateur. La méthode « catchError » permet d’indiquer ce qu’on effectuera en cas d’erreur; ici on va rediriger l’utilisateur vers la page de login.

Une fois notre garde implémentée, nous devons indiquer dans notre fichier « app.routes.ts » quelles urls devront être protégées par cette garde. Pour cela, nous pouvons ajouter la clé « canActivate » suivi d’une liste de gardes aux routes qu’on souhaite protéger. Dans notre cas, on aura uniquement la garde « isLoggedInGuard », et nous allons l’appliquer aux URLs « home », « monster » et « monster/:id »:

app.routes.ts
import { Routes } from '@angular/router';
import { MonsterListComponent } from './pages/monster-list/monster-list.component';
import { MonsterComponent } from './pages/monster/monster.component';
import { NotFoundComponent } from './pages/not-found/not-found.component';
import { LoginComponent } from './pages/login/login.component';
import { isLoggedInGuard } from './guards/is-logged-in/is-logged-in.guard';

export const routes: Routes = [{
    path: '',
    redirectTo: 'home',
    pathMatch: 'full'
}, {
    path: 'home',
    component: MonsterListComponent,
    canActivate: [isLoggedInGuard]
}, {
    path: 'login',
    component: LoginComponent
}, {
    path: 'monster',
    children: [{
        path: '',
        component: MonsterComponent,
        canActivate: [isLoggedInGuard]
    }, {
        path: ':id',
        component: MonsterComponent,
        canActivate: [isLoggedInGuard]
    }]
}, {
    path: '**',
    component: NotFoundComponent
}];

Maintenant, si vous n’êtes pas connectés et que vous tentez de vous rendre à une URL protégée, vous serez redirigés vers la page de login, et si vous êtes connectés, que vous quittez l’application sans vous déconnecter et que, par la suite, vous retentez d’accéder à une page protégée, vous verrez que le token sauvegardé dans le « localStorage » est bien utilisé et que vous pouvez toujours accéder aux pages protégées.

Login Form

Et voilà, c’est tout ce que je voulais vous montrer. Lors du prochain chapitre, on verra plus en détail comment effectuer des appels APIs afin d’interagir avec des monstres provenant d’un server et non plus en local dans notre « localStorage ».

Resources

Vous retrouvez ci-dessous un lien vers le code source utilisé dans ce chapitre. Ce lien est uniquement disponible pour les abonnés Standard et Premium. 

Quiz

Répondez au quiz ci-dessous afin de vérifier que vous avez bien compris le contenu de cette leçon. Les quiz sont uniquement disponibles pour les abonnés Standard et Premium.

Laisser un commentaire