:info: Doc de l'API : https://album-api.benoithubert.me
La dernière fois, nous avions :
-
Créé des formulaires de register et de login
-
Abordé la notion d'observable et vu quelques exemples avec la bibliothèque RxJS, utilisée par Angular.
-
Créé un
AuthenticationService
permettant :- de stocker l'envoi des données des deux formulaires vers l'API,
- de gérer l'état de l'authentification (connecté ou non) via un observable : plus précisément, l'attribut
currentUserSubject
, un type d'observable particulier, leBehaviorSubject
, qui contrairement aux observables ordinaires, permet de stocker la dernière valeur. - de "persister" cette authentification dans deux clés du "local storage" :
currentUser
pour les données de l'utilisateur proprement dites,token
pour le JWT renvoyé par le serveur sur une requête de login réussie
-
Modifié la méthode
createPost
duPostService
, de façon à envoyer le JWT dans le headerAuthorization
, sous la forme :Bearer JWT
.
Il nous reste beaucoup de choses à voir !
Quand on appelle la méthode logout
de AuthenticationService
, on devrait réinitialiser l'attribut token
à une chaîne vide.
Une des problématiques évoquées la dernière fois, mais qu'on n'a pas eu le temps de traiter : la "protection" de certaines pages contre des accès non-autorisés.
La plupart des pages de notre application ont vocation à être librement accessibles, sans authentification :
- la page d'accueil,
- la page de détails d'un post (accessible en cliquant "En savoir plus"),
- les pages
About
etContact
(bien qu'assez inutiles pour le moment, il faut le reconnaître !)
Par contre, certaines pages ne devraient pas être accessibles ainsi :
- le formulaire d'ajout d'un post,
- éventuellement, si on ajoute ce genre de fonctionnalité, un formulaire permettant à l'utilisateur connecté de modifier ses informations (email, avatar, etc.)
Dans un premier temps :
- Créer un guard :
ng generate guard auth
- Dans ce guard, renvoyer true/false suivant qu'un utilisateur est connecté ou non
- Pour cela, s'inspirer de la doc sur CanActivate : pour simplifier, la méthode
canActivate
renvoie un booléen (ou un Observable émettant un booléen, ou une Promise résolvant en booléen) - La valeur booléenne à renvoyer va être déterminée par la valeur courante stockée par l'observable
currentUserSubject
. - Un getter existe déjà pour simplifier l'accès à cette valeur depuis du code extérieur à
AuthenticationService
:currentUserValue
. - Ajouter un champ
canActivate
dans la route ayant le pathadd-post
, pour utiliser le guard
Après ces modifs, l'utilisateur sera directement renvoyé vers la page de login s'il essaie d'accéder à la page /add-post
.
Dans un second temps, améliorer. Il serait intéressant, quand on redirige l'utilisateur vers la page d'accueil, de se souvenir de quelle page il souhaitait visiter. Ainsi, on redirigera vers la page de login, en indiquant dans l'URL un paramètre contenant le chemin de cette page. Une fois connecté, l'utilisateur sera redirigé automatiquement vers cette page.
- en s'inspirant (voire en reprenant presque tel quel) le code du AuthGuard du tuto
- en récupérant également les lignes 51-52 du code du LoginComponent
Nous avions, par souci de temps, concentré nos efforts sur l'ajout des fonctionnalités, sans évoquer l'aspect de la gestion des erreurs.
Il est grand temps de s'y pencher, car sans une gestion d'erreurs appropriée, les utilisateurs seront confrontés à des problèmes inacceptables dans une application web moderne.
Par exemple :
- Si le serveur de l'API est en panne, nos visiteurs ne verront que les parties navbar/header et footer de l'application. On aurait intérêt à les informer d'un dysfonctionnement, et à les inviter à revenir plus tard.
- Si on essaie de se register avec une adresse email déjà inscrite dans la base de données sur le serveur, on aurait tout intérêt à en informer l'utilisateur.
- Si on soumet des identifiants incorrects lors d'une requête de login, on devrait le signifier à l'utilisateur.
En l'état actuel des choses, aucun de ces cas d'erreur ne se matérialise dans l'interface.
Quelques sources à consulter:
- Section Handling request errors du guide Communicating with backend services using HTTP de la doc Angular.
- Un tutoriel plus fourni : Error Handling in Angular 12 Tutorial with Examples. Voir particulièrement la section "Handle Errors in Angular with HttpClient and RxJS".
- Un tutoriel encore plus complet (peut-être trop !) : RxJs Error Handling: Complete Practical Guide
Voici une petite liste des endroits où on pourrait (et devrait) implémenter une gestion d'erreurs.
AuthenticationService
, dans les méthodesregister
etlogin
PostService
, dans les méthodesgetAllPosts
,getPost
,createPost
,deletePost
(et éventuellementlikePost
, qu'on n'a pas implémentée… voire bonus)
- On peut commencer par prendre le code décrit dans la doc officielle d'Angular (1ère ressource)
- Des explications plus complètes sur le fonctionnement de ce code sont données dans la 3ème ressource, sous la section intitulée "The Catch and Rethrow Strategy".
- L'idée est, dans la méthode
handleError
, d'initialiser une variableerrorMessage
au tout début, contenant une chaîne vide. - Dans la 1ère "branche" du
if
, changer la valeur d'errorMessage
avec une chaîne indiquant qu'il s'agit d'une erreur réseau. - Dans la 2nde branche, autrement dit dans le
else
, changer la valeur en se servant des données renvoyées par le serveur. Celles-ci consistent en un tableau d'erreurs accessible viaerror.error.errors
(!!!). On peut par exemple faire unmap
pour récupérer uniquement la clémsg
de chaque erreur, puis faire unjoin
pour créer une string à partir de ce tableau de messages. - Enfin, on va passer cette valeur
errorMessage
authrowError
à la fin.
Solution...
private handleError(error: HttpErrorResponse) {
let errorMessage = '';
if (error.status === 0) {
errorMessage = 'Network error! Please check your connection or come back later!'
} else {
errorMessage = error.error.errors.map((err: { msg: string }) => err.msg).join('. ');
}
return throwError(errorMessage);
}
Ceci marche bien pour les register
et login
.
Pour adapter ceci au PostService
, il faudra adapter le code :
- notamment, supprimer les
.toPromise()
pour utiliser les observables au lieu des promesses. - répercuter cette modification dans
AddPostComponent
: plus de.then
et.catch
mais un unique.subscribe
, auquel on pourra passer un objet contenant des méthodesnext
(cas nominal) eterror
(cas d'erreur) - jusqu'ici, on avait passé directement une unique fonction à
subscribe
, appelée lorsque l'observable émet une nouvelle valeur. Passer un objet permet de gérer les cas d'erreur et de complétion d'un observable. - Un exemple est donné dans la section Subscribing de la page Using observables to pass values de la doc Angular.
Par ailleurs, le handleError
du PostService
devra gérer un problème dont l'AuthenticationService
n'avait pas à s'occuper : l'expiration du JWT. Celui-ci n'a (idéalement !) pas une durée infinie. Quand il expire, le serveur renverra une erreur 401.
Dans handleError
, on pourra vérifier si error.status
vaut 401 pour gérer ce cas. Il faudra alors :
- Déconnecter l'utilisateur
- Le rediriger vers la page de login
Les intercepteurs sont un mécanisme qui permet d'inspecter et/ou transformer des requêtes et réponses HTTP gérées via le HttpClient
.
Ils sont documentés dans la section Intercepting requests and responses du guide HTTP d'Angular, auquel on s'est déjà référé plus haut, quand on a évoqué la gestion des erreurs.
Ils ont plusieurs utilités (voir section Http interceptor use-cases de la même page). Par exemple :
- Gérer les erreurs de façon plus centralisée les erreurs (éviter la duplication de code des
errorHandler
implémentés pour l'AuthenticationService
et lePostService
) - Positionner des en-têtes par défaut pour plusieurs requêtes, ce qui s'avèrera utile afin d'éviter de répéter l'insertion du header
Authorization
pour chaque requête devant être authentifiée. - Mettre en "cache" le résultat de certaines requêtes, de façon à éviter de requêter le backend plus que nécessaire
Le tuto sur l'authentification dont nous nous étions inspiré présente deux intercepteurs : l'un pour gérer les erreurs, l'autre pour ajouter le JWT à chaque requête. On peut s'en inspirer, moyennant des petites adaptations (nous ne stockons pas le token exactement comme le montre ce tuto).
Consigne :
- metre en place un intercepteur pour envoyer automatiquement le JWT avec chaque requête
- centraliser la gestion des erreurs 401 et 403 dans un 2ème intercepteur
Le tuto sur l'authentification dont nous nous étions inspiré présente deux intercepteurs : l'un pour gérer les erreurs, l'autre pour ajouter le JWT à chaque requête. On peut s'en inspirer, moyennant des petites adaptations (nous ne stockons pas le token exactement comme le montre ce tuto).
Si vous avez tout fini, tout compris, etc. 😁
Voir la doc : Validating form input. Attention à regarder plus attentivement ce qui concerne les "reactive forms" (ce que nous avons utilisé) plutôt que ce qui concerne les "template-driven forms".
C'est à dire, incrémenter les likes stockés dans la base de données du serveur pour chaque post. Le code serveur est en place, vous n'avez pas à vous en préoccuper. Dans la méthode like
de CardComponent
:
- au lieu de stocker les likes dans l'attribut
likes
, appeler la méthodelikePost
dePostService
, en lui passant l'id
du post. - Cette méthode
likePost
à implémenter devra envoyer une requête sur l'URL/api/v2/posts/<id>/like
, ou<id>
sera remplacé par l'id
reçu en paramètre. - On devra également passer le JWT dans les headers, en s'inspirant de ce qu'on avait fait pour
createPost
. - Pour afficher ce compteur de likes, il suffit d'afficher
post.likes
au lieu delikes
dans le templatecard.component.html
. - Cet attribut
likes
doit être ajouté à la définition du typePost
.
Vous pouvez créer un composant my-profile
. Il sera accessible via un lien de la navbar, et "protégé" comme on l'a fait pour add-post
.
Le formulaire comportera :
- un champ
login
permettant de modifier le login/email de l'utilisateur actuellement connecté (il faut initialiser sa valeur en utilisant celle stockée dans lecurrentUserSubject
du service d'authentification) - Un champ input
file
. Vous pouvez suivre ce tuto en supprimant le champname
du formulaire qui y est décrit. - Lorsque vous "uploaderez" les données, ce sera via une requête
PUT
vers le endpoint/api/v2/users/me
. Les données devront contenirlogin
(l'email) et un champavatar
qui contiendra les données du champimgSrc
du formulaire. - Donc, indice par rapport au tuto, où on envoie en "brut"
this.uploadForm.value
: il vous faudra envoyer le champlogin
et le champimgSrc
en renommant ce dernieravatar
. - Le serveur vous répondra avec l'utilisateur mis à jour, dont le champ
avatar
pointant vers le chemin relatif de l'avatar par rapport au serveur. - Pour afficher cette image dans un tag
img
, il faudra concaténer ce chemin relatif à l'URL de base du serveur.
Outre les attributs title
, description
et picture
d'un nouveau post, l'API accepte, sur la route POST
vers /api/v2/posts
, un champ tags
qui doit comprendre des tags séparés par des virgules. Par exemple : iceland,mountains,lake
. Exemple complet de "payload" (corps de requête) :
{
"title": "Landmannalaugar - Iceland",
"description": "In Landmannalaugar there are rhyolite mountains.",
"picture": "https://i.imgur.com/WCzfMMt.jpeg",
"tags": "iceland,mountains"
}
Côté Angular, une solution simple peut être d'ajouter un simple champ input
(avec éventuellement un placeholder et un label suggérant le format de la valeur acceptée).
Une solution plus poussée peut être d'utiliser la bibliothèque ng-select
(GitHub) dont la doc présente un exemple qui serait assez facilement adaptable : Tags. Si vous choisissez cette solution, le mieux est de prendre l'exemple "Custom tags" et l'adapter. C'est-à-dire qu'au final, lorsque vous soumettrez le formulaire, il faudra convertir le tableau de tags saisi en une chaîne, en vue de l'envoyer au backend.
Les tags sont renvoyés avec les posts sur les requêtes GET
Notre application, en l'état, est moche spartiate. On avait un minimum vital de CSS en place sur le template d'origine, mais on ne s'en est pas préoccupé depuis.
Il y aurait plusieurs approches pour y remédier :
- Ajouter du CSS à la main. C'est faisable mais potentiellement fastidieux.
- Utiliser une bibliothèque CSS comme Bootstrap, Bulma, Tailwind, etc. Cela nous allégerait déjà considérablement la tâche.
- Enfin, utiliser une bibliothèque de composants. C'est-à-dire un ensemble de composants développés spécifiquement pour Angular. Outre l'aspect esthétique, ces composants gèrent l'aspect dynamique de l'affichage (afficher/cacher un menu, des notifications, etc.).
La dernière option est intéressante et largement adoptée, notamment pour des applications d'entreprise où l'on s'efforce d'obtenir une esthétique cohérente, sans y consacrer d'efforts démesurés.
La bibliothèque de composants la plus connue est Angular Material.