: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
AuthenticationServicepermettant :- 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" :
currentUserpour les données de l'utilisateur proprement dites,tokenpour le JWT renvoyé par le serveur sur une requête de login réussie
-
Modifié la méthode
createPostduPostService, 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
AboutetContact(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
canActivaterenvoie 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
canActivatedans 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éthodesregisteretloginPostService, 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 variableerrorMessageau tout début, contenant une chaîne vide. - Dans la 1ère "branche" du
if, changer la valeur d'errorMessageavec 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 unmappour récupérer uniquement la clémsgde chaque erreur, puis faire unjoinpour créer une string à partir de ce tableau de messages. - Enfin, on va passer cette valeur
errorMessageauthrowErrorà 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.thenet.catchmais 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
errorHandlerimplémentés pour l'AuthenticationServiceet 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
Authorizationpour 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éthodelikePostdePostService, en lui passant l'iddu 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'idreç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.likesau lieu delikesdans le templatecard.component.html. - Cet attribut
likesdoit ê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
loginpermettant de modifier le login/email de l'utilisateur actuellement connecté (il faut initialiser sa valeur en utilisant celle stockée dans lecurrentUserSubjectdu service d'authentification) - Un champ input
file. Vous pouvez suivre ce tuto en supprimant le champnamedu formulaire qui y est décrit. - Lorsque vous "uploaderez" les données, ce sera via une requête
PUTvers le endpoint/api/v2/users/me. Les données devront contenirlogin(l'email) et un champavatarqui contiendra les données du champimgSrcdu formulaire. - Donc, indice par rapport au tuto, où on envoie en "brut"
this.uploadForm.value: il vous faudra envoyer le champloginet le champimgSrcen renommant ce dernieravatar. - Le serveur vous répondra avec l'utilisateur mis à jour, dont le champ
avatarpointant 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.