diff --git a/server/tp3/package.json b/server/tp3/package.json new file mode 100644 index 0000000000000000000000000000000000000000..cdc4b3f17ce1ecf4d38eb6437809bb38a1b44f0c --- /dev/null +++ b/server/tp3/package.json @@ -0,0 +1,17 @@ +{ + "name": "user-server", + "version": "1.0.0", + "description": "", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "express": "^4.17.1", + "express-jwt": "^6.0.0", + "jsonwebtoken": "^8.5.1" + } +} diff --git a/server/tp3/server.js b/server/tp3/server.js new file mode 100644 index 0000000000000000000000000000000000000000..d6c69509bcc34110302cfb8ebbc593bd391e4095 --- /dev/null +++ b/server/tp3/server.js @@ -0,0 +1,97 @@ +const express = require("express"); +const body_parser = require("body-parser"); +const jsonwebtoken = require("jsonwebtoken"); +const expressjwt = require("express-jwt"); +const path = require("path"); + +const secret = "fze5EVvs:,;hsegFZEQGhtrh,;$:^fz"; + +const app = express(); + +// app.use(express.static("../build")); +app.use(body_parser.json()); + +app.all("*", (req, res, next) => { + res.set("Access-Control-Allow-Origin", "*"); + res.set("Access-Control-Allow-Credentials", true); + res.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, DELETE, PUT"); + res.set( + "Access-Control-Allow-Headers", + "X-Requested-With, Content-Type, Authorization" + ); + if (req.method === "OPTIONS") { + return res.status(200).end(); + } else { + next(); + } +}); + +const users = [ + { + id: 1, + username: "pierre", + password: "kraemer" + }, + { + id: 2, + username: "toto", + password: "tutu" + } +]; +let nextid = 3; + +app.post("/signin", (req, res, next) => { + const user = users.find(u => u.username === req.body.username); + if (user?.password === req.body.password) { + const token = jsonwebtoken.sign({ id: user.id }, secret, { + algorithm: 'HS256', + expiresIn: 60 * 60 * 12 + }); + const { password, ...user_without_pw } = user; + return res.json({ + user: user_without_pw, + token + }); + } + next({ status: 401, message: "Bad username or password" }); +}); + +app.get("/whoami", expressjwt({ secret, algorithms: ['HS256'] }), (req, res, next) => { + const user = users.find(u => u.id === req.user.id); + if (user) { + const { password, ...user_without_pw } = user; + return res.json(user_without_pw); + } + next({ status: 404, message: "User not found" }); +}); + +app.post("/signup", (req, res, next) => { + const available = !users.some(u => u.username === req.body.username); + if (available) { + users.push({ + id: nextid++, + username: req.body.username, + password: req.body.password + }); + return res.status(200).end(); + } + next(new Error("Username not available")); +}); + +// app.all("/*", (req, res) => { +// res.sendFile(path.resolve("../build/index.html")); +// }); + +app.use((err, req, res, next) => { + if (err.status) { + return res.status(err.status).send(err.message); + } else { + return res.status(500).send(err.message); + } +}); + +const server = app.listen(4200, "localhost", () => { + const host = server.address().address; + const port = server.address().port; + console.log("App listening at http://%s:%s", host, port); +}); diff --git a/sujets/sujet2.md b/sujets/sujet2.md index 9621c6b0e7d219da3949640fab7d86093c593585..be684c87cb39ed5e73892a7b505be65d342a6767 100644 --- a/sujets/sujet2.md +++ b/sujets/sujet2.md @@ -37,7 +37,7 @@ Comme pour la liste de personnages, en attendant la réponse du serveur, le comp S'assurer que la liste de films est bien mise à jour lors de la sélection d'un nouveau personnage. -V3 +<!-- V3 --- Ecrire un custom hook `useDataFromUrl`. @@ -47,13 +47,13 @@ En interne, cette fonction déclare les éléments de `state` nécessaire, et d Utiliser ce custom hook dans l'ensemble des composants qui font des requêtes. -Comment faire pour que l'on puisse passer un tableau d'URL à la fonction `useDataFromUrl`, et que le champ `data` obtenu soit un tableau contenant les données obtenue depuis chaque URL ? +Comment faire pour que l'on puisse passer un tableau d'URL à la fonction `useDataFromUrl`, et que le champ `data` obtenu soit un tableau contenant les données obtenue depuis chaque URL ? --> -V4 +V3 --- Utiliser la bibliothèque [react-query](https://react-query.tanstack.com/) pour gérer les requêtes à l'API (à la place de notre custom hook). -Bien lire la documentation sur les requêtes (https://react-query.tanstack.com/docs/guides/queries) et s'assurer d'avoir compris le fonctionnement des `keys` et le passage de paramètres à la fonction asynchrone de récupération des données. +Bien lire la documentation sur les requêtes (https://react-query.tanstack.com/docs/guides/queries) et s'assurer d'avoir compris le fonctionnement des `keys`. > ___Indication___ : pour récupérer un tableau d'identifiants de films à partir d'un tableau d'URL du type `https://swapi.dev/api/films/3` -> `const filmsId = filmsUrl.map(u => u.split('/').filter(Boolean).pop());`. diff --git a/sujets/sujet3.md b/sujets/sujet3.md new file mode 100644 index 0000000000000000000000000000000000000000..c38b5afd11953e0cc4c8166149c5b25427758521 --- /dev/null +++ b/sujets/sujet3.md @@ -0,0 +1,53 @@ +Authentification, routage et contextes +=== + +Ecrire une application qui gère la connexion d'un utilisateur en lien avec une API HTTP distante. + +Lire la [doc de `react-router`](https://reacttraining.com/react-router/web/guides/quick-start). + +V1 +--- + +L'application affiche un menu (donné par un composant `Menu`) contenant les entrées suivantes : + - Home (qui mène à la route "/") + - si l'utilisateur n'est pas connecté : + - Signin (qui mène à la route "/signin") + - Signup (qui mène à la route "/signout") + - si l'utilisateur est connecté : + - le nom de l'utilisateur connecté ("Connected as ...") + - Signout (qui appelle la logique de déconnexion) + +A chaque route ("/", "/signin", "/signup") correspond le rendu d'un composant particulier (`Home`, `Signin`, `Signup`). + +Les informations relatives à l'utilisateur connecté sont maintenues dans le composant principal (seul à contenir la logique de connexion ainsi que la connaissance de l'API) : + - Un objet `user` dans le state (initialisé à `null`) contenant l'utilisateur actuellement connecté + - Une fonction `signin` qui permet de soumettre un couple (identifiant, mot de passe) à l'API distante qui, en cas de succès, renvoie un JSON Web Token (JWT) ainsi qu'un objet correspondant au `user` authentifié. Le token doit alors être stocké en `localStorage` et l'utilisateur peut être emmené vers la route "/". + - Une fonction `signup` qui permet de soumettre un couple (identifiant, mot de passe) pour inscription. En cas de succès, on emmène l'utilisateur vers la route "/signin". + +Les composants `Signin` et `Signup` proposent un formulaire adéquat et font appel, lors de la soumission du formulaire, à une fonction `onSubmit` reçue en prop qui fait à son tour appel respectivement aux fonctions `signin` et `signup` du composant principal. +Faire en sorte qu'en cas d'échec des requêtes, le message d'erreur reçu s'affiche sous le formulaire. + +Au lancement de l'application, faire en sorte de restaurer l'utilisateur connecté en exploitant le JSON Web Token présent en `localStorage` si il existe (voir la route `/whoami` de l'API). +En attendant la réponse du serveur, ne rendre qu'un message d'attente. + +API +--- + +L'API HTTP fournie comprend les routes suivantes : + - `POST /signin` : reçoit un objet de la forme `{ username: '...', password: '...' }`. En cas de succès, renvoie une réponse HTTP code 200 avec un objet de la forme `{ token: '...', user: { id: 42, username: '...' } }`. En cas d'échec, renvoie une réponse HTTP code 401 avec un message d'erreur. + - `POST /signup` : reçoit un objet de la forme `{ username: '...', password: '...' }`. En cas de succès, renvoie une réponse HTTP code 200. En cas d'échec, renvoie une réponse HTTP code 500 avec un message d'erreur. + - `GET /whoami` : lit l'en-tête `Authorization` de la requête HTTP au format `"Bearer [JSON Web Token]"`. En cas de succès du décodage du token, renvoie une réponse HTTP code 200 avec un objet de la forme `{ id: 42, username: '...' }`. En cas d'échec, renvoie une réponse HTTP code 404 avec un message d'erreur. + +V2 +--- + +Les données concernant l'utilisateur connecté sont potentiellement utiles à différents niveaux de l'application. +Pour éviter d'avoir à passer ces informations manuellement au travers de nombreuses couches de composants ("props drilling"), on peut mettre en place un contexte, qui donnera accès aux données souhaitées dans tout le sous-arbre de l'application. + +Dans un module séparé (`auth.js` par exemple), créer un contexte `AuthContext`, et exporter 2 éléments : + - un composant `AuthProvider` dans lequel on va déplacer les données et la logique liées à la gestion de l'utilisateur. Ce composant rend le `Provider` du contexte `AuthContext` en lui passant comme valeur un objet contenant les champs `user`, `signup`, `signin` et `signout`. Le contenu du `Provider` est les éléments fils reçus par le composant `AuthProvider` (`children`). + - un custom hook `useAuth` qui retourne simplement le résultat de `useContext(AuthContext)` + +Nettoyer le composant principal de l'application qui ne devrait plus contenir que la déclaration du `AuthProvider`, le menu et les routes. + +Dans les composants `Menu`, `Signin` et `Signup`, récupérer les données nécessaires du contexte d'authentification grâce à la fonction `useAuth`.