From 3a11d7cebcfb9daac2e63bbc53163a9528fbeec0 Mon Sep 17 00:00:00 2001 From: Pierre Kraemer <kraemer@unistra.fr> Date: Thu, 10 Dec 2020 08:03:47 +0100 Subject: [PATCH] ajout sujet 4 --- server/tp4/controllers/item.js | 80 ++++++++++++++++++++++ server/tp4/controllers/list.js | 119 +++++++++++++++++++++++++++++++++ server/tp4/controllers/user.js | 111 ++++++++++++++++++++++++++++++ server/tp4/models/index.js | 32 +++++++++ server/tp4/models/item.js | 16 +++++ server/tp4/models/list.js | 17 +++++ server/tp4/models/user.js | 31 +++++++++ server/tp4/package.json | 22 ++++++ server/tp4/routes/index.js | 16 +++++ server/tp4/routes/item.js | 34 ++++++++++ server/tp4/routes/list.js | 45 +++++++++++++ server/tp4/routes/user.js | 51 ++++++++++++++ server/tp4/server.js | 31 +++++++++ sujets/sujet4.md | 47 +++++++++++++ 14 files changed, 652 insertions(+) create mode 100644 server/tp4/controllers/item.js create mode 100644 server/tp4/controllers/list.js create mode 100644 server/tp4/controllers/user.js create mode 100644 server/tp4/models/index.js create mode 100644 server/tp4/models/item.js create mode 100644 server/tp4/models/list.js create mode 100644 server/tp4/models/user.js create mode 100644 server/tp4/package.json create mode 100644 server/tp4/routes/index.js create mode 100644 server/tp4/routes/item.js create mode 100644 server/tp4/routes/list.js create mode 100644 server/tp4/routes/user.js create mode 100644 server/tp4/server.js create mode 100644 sujets/sujet4.md diff --git a/server/tp4/controllers/item.js b/server/tp4/controllers/item.js new file mode 100644 index 000000000..53fe260ae --- /dev/null +++ b/server/tp4/controllers/item.js @@ -0,0 +1,80 @@ +'use_strict'; + +const db = require('../models'); + +module.exports = { + + get_all: (req, res, next) => { + return db.Item.findAll() + .then((items) => res.json(items)) + .catch((err) => next(err)); + }, + + get_by_id: (req, res, next) => { + return db.Item.findByPk(req.params.item_id) + .then((item) => { + if (!item) { + throw { status: 404, message: 'Requested Item not found' }; + } + return res.json(item); + }) + .catch((err) => next(err)); + }, + + create: (req, res, next) => { + const data = { + text: req.body.text || '...' + }; + return db.Item.create(data) + .then((item) => res.json(item)) + .catch((err) => next(err)); + }, + + update_by_id: (req, res, next) => { + return db.Item.findByPk(req.params.item_id) + .then((item) => { + if (!item) { + throw { status: 404, message: 'Requested Item not found' }; + } + Object.assign(item, req.body); + return item.save(); + }) + .then((item) => res.json(item)) + .catch((err) => next(err)); + }, + + delete_by_id: (req, res, next) => { + return db.Item.findByPk(req.params.item_id) + .then((item) => { + if (!item) { + throw { status: 404, message: 'Requested Item not found' }; + } + return item.destroy(); + }) + .then(() => res.status(200).end()) + .catch((err) => next(err)); + }, + + // USER // + + is_of_user: (req, res, next) => { + return db.Item.findByPk(req.params.item_id) + .then((item) => { + if (!item) { + throw { status: 404, message: 'Requested Item not found' }; + } + return item.getList(); + }) + .then((list) => { + return list.getUser(); + }) + .then((user) => { + if (user.id !== req.user.id) { + throw { status: 401, message: 'This is not your List..' }; + } + return next(); + }) + .catch((err) => next(err)); + } + +}; diff --git a/server/tp4/controllers/list.js b/server/tp4/controllers/list.js new file mode 100644 index 000000000..7245c69be --- /dev/null +++ b/server/tp4/controllers/list.js @@ -0,0 +1,119 @@ +'use_strict'; + +const db = require('../models'); + +module.exports = { + + get_all: (req, res, next) => { + return db.List.findAll() + .then((lists) => res.json(lists)) + .catch((err) => next(err)); + }, + + get_by_id: (req, res, next) => { + return db.List.findByPk(req.params.list_id) + .then((list) => { + if (!list) { + throw { status: 404, message: 'Requested List not found' }; + } + return res.json(list); + }) + .catch((err) => next(err)); + }, + + create: (req, res, next) => { + const data = { + title: req.body.title || '...' + }; + return db.List.create(data) + .then((list) => res.json(list)) + .catch((err) => next(err)); + }, + + update_by_id: (req, res, next) => { + return db.List.findByPk(req.params.list_id) + .then((list) => { + if (!list) { + throw { status: 404, message: 'Requested List not found' }; + } + Object.assign(list, req.body); + return list.save(); + }) + .then((list) => res.json(list)) + .catch((err) => next(err)); + }, + + delete_by_id: (req, res, next) => { + return db.List.findByPk(req.params.list_id) + .then((list) => { + if (!list) { + throw { status: 404, message: 'Requested List not found' }; + } + return list.destroy(); + }) + .then(() => res.status(200).end()) + .catch((err) => next(err)); + }, + + get_items_of_id: (req, res, next) => { + return db.List.findByPk(req.params.list_id) + .then((list) => { + if (!list) { + throw { status: 404, message: 'Requested List not found' }; + } + return list.getItems(); + }) + .then((items) => res.json(items)) + .catch((err) => next(err)); + }, + + add_item_to_id: (req, res, next) => { + return db.List.findByPk(req.params.list_id) + .then((list) => { + if (!list) { + throw { status: 404, message: 'Requested List not found' }; + } + const data = { + text: req.body.text || '...' + }; + return list.createItem(data); + }) + .then((item) => res.json(item)) + .catch((err) => next(err)); + }, + + // USER // + + is_of_user: (req, res, next) => { + return db.List.findByPk(req.params.list_id) + .then((list) => { + if (!list) { + throw { status: 404, message: 'Requested List not found' }; + } + return list.getUser(); + }) + .then((user) => { + if (user.id !== req.user.id) { + throw { status: 401, message: 'This is not your List..' }; + } + return next(); + }) + .catch((err) => next(err)); + }, + + get_all_of_user: (req, res, next) => { + return req.user.getLists() + .then((lists) => res.json(lists)) + .catch((err) => next(err)); + }, + + create_of_user: (req, res, next) => { + const data = { + title: req.body.title || '...' + }; + return req.user.createList(data) + .then((list) => res.json(list)) + .catch((err) => next(err)); + } + +}; diff --git a/server/tp4/controllers/user.js b/server/tp4/controllers/user.js new file mode 100644 index 000000000..24bb62dac --- /dev/null +++ b/server/tp4/controllers/user.js @@ -0,0 +1,111 @@ +'use_strict'; + +const + jsonwebtoken = require('jsonwebtoken'), + expressjwt = require('express-jwt'), + db = require('../models'); + +const secret = 'buiVUTY,676:88b&hj%cgF*Chi'; + +module.exports = { + + get_all: (req, res, next) => { + return db.User.findAll({ + order: ['username'] + }) + .then((users) => res.json(users)) + .catch((err) => next(err)); + }, + + get_by_id: (req, res, next) => { + return db.User.findByPk(req.params.user_id) + .then((user) => { + if (!user) { + throw { status: 404, message: 'Requested User not found' }; + } + return res.json(user); + }) + .catch((err) => next(err)); + }, + + signup: (req, res, next) => { + const data = { + username: req.body.username || '', + password: db.User.generate_hash(req.body.password) || '' + }; + return db.User.create(data) + .then((user) => res.json(user)) + .catch((err) => next(err)); + }, + + update_by_id: (req, res, next) => { + return db.User.findByPk(req.params.user_id) + .then((user) => { + if (!user) { + throw { status: 404, message: 'Requested User not found' }; + } + delete req.body.password; + Object.assign(user, req.body); + return user.save(); + }) + .then((user) => res.json(user)) + .catch((err) => next(err)); + }, + + delete_by_id: (req, res, next) => { + return db.User.findByPk(req.params.user_id) + .then((user) => { + if (!user) { + throw { status: 404, message: 'Requested User not found' }; + } + return user.destroy(); + }) + .then(() => res.status(200).end()) + .catch((err) => next(err)); + }, + + signin: (req, res, next) => { + const username = req.body.username || ''; + const password = req.body.password || ''; + + db.User.findOne({ + where: { username } + }) + .then((user) => { + if (!user) { + throw { status: 404, message: 'Requested User not found' }; + } + if (!user.check_password(password)) { + throw { status: 401, message: 'Wrong password' }; + } + + const token = jsonwebtoken.sign( + { id: user.id }, + secret, + { algorithm: 'HS256', expiresIn: 60 * 60 * 12 } + ); + + return res.json({ user, token }); + }) + .catch((err) => next(err)); + }, + + whoami: (req, res, next) => { + return res.json(req.user); + }, + + identify_client: [ + expressjwt({ secret, algorithms: ['HS256'] }), + (req, res, next) => { + db.User.findByPk(req.user.id) + .then((user) => { + if (!user) { + throw { status: 404, message: 'Requested User not found' }; + } + req.user = user; + return next(); + }); + } + ] + +}; diff --git a/server/tp4/models/index.js b/server/tp4/models/index.js new file mode 100644 index 000000000..b160847e3 --- /dev/null +++ b/server/tp4/models/index.js @@ -0,0 +1,32 @@ +'use strict'; + +const + fs = require('fs'), + Sequelize = require('sequelize'); + +// create Sequelize instance +const sequelize = new Sequelize('[DBname]', '[username]', '[password]', { + host: 'localhost', + port: 3306, + dialect: 'mysql', + dialectOptions: { decimalNumbers: true } + // operatorsAliases: false + // logging: false +}); + +const db = {}; + +fs.readdirSync(__dirname) + .filter((filename) => filename !== 'index.js') + .forEach((filename) => { + const model = require('./' + filename)(sequelize, Sequelize.DataTypes); + db[model.name] = model; + }); + +Object.keys(db).forEach((modelName) => { + db[modelName].associate(db); +}); + +sequelize.sync(); + +module.exports = db; diff --git a/server/tp4/models/item.js b/server/tp4/models/item.js new file mode 100644 index 000000000..976324c9f --- /dev/null +++ b/server/tp4/models/item.js @@ -0,0 +1,16 @@ +'use strict'; + +module.exports = (sequelize, DataTypes) => { + const Item = sequelize.define( + 'Item', + { + text: DataTypes.STRING + } + ); + + Item.associate = (db) => { + Item.belongsTo(db.List, { onDelete: 'cascade' }); + }; + + return Item; +}; diff --git a/server/tp4/models/list.js b/server/tp4/models/list.js new file mode 100644 index 000000000..a7ac13f2d --- /dev/null +++ b/server/tp4/models/list.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = (sequelize, DataTypes) => { + const List = sequelize.define( + 'List', + { + title: DataTypes.STRING + } + ); + + List.associate = (db) => { + List.hasMany(db.Item, { onDelete: 'cascade' }); + List.belongsTo(db.User, { onDelete: 'cascade' }); + }; + + return List; +}; diff --git a/server/tp4/models/user.js b/server/tp4/models/user.js new file mode 100644 index 000000000..447c8379b --- /dev/null +++ b/server/tp4/models/user.js @@ -0,0 +1,31 @@ +'use strict'; + +const bcrypt = require('bcrypt'); + +module.exports = (sequelize, DataTypes) => { + const User = sequelize.define( + 'User', + { + username: DataTypes.STRING, + password: DataTypes.STRING + } + ); + + User.associate = (db) => { + User.hasMany(db.List, { onDelete: 'cascade' }); + }; + + User.generate_hash = (password) => bcrypt.hashSync(password, 10); + + User.prototype.toJSON = function () { + const data = Object.assign({}, this.get()); + delete data.password; + return data; + }; + + User.prototype.check_password = function (password) { + return bcrypt.compareSync(password, this.password); + }; + + return User; +}; diff --git a/server/tp4/package.json b/server/tp4/package.json new file mode 100644 index 000000000..edf71666e --- /dev/null +++ b/server/tp4/package.json @@ -0,0 +1,22 @@ +{ + "name": "list", + "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": { + "bcrypt": "^5.0.0", + "body-parser": "^1.19.0", + "cors": "^2.8.5", + "express": "^4.17.1", + "express-jwt": "^6.0.0", + "jsonwebtoken": "^8.5.1", + "mysql2": "^2.2.5", + "sequelize": "^6.3.5" + } +} diff --git a/server/tp4/routes/index.js b/server/tp4/routes/index.js new file mode 100644 index 000000000..da9b75668 --- /dev/null +++ b/server/tp4/routes/index.js @@ -0,0 +1,16 @@ +'use strict'; + +const + fs = require('fs'); + +module.exports = function (app) { + + fs.readdirSync(__dirname) + .filter((filename) => filename !== 'index.js') + .forEach((filename) => { + require('./' + filename).forEach((r) => { + app[r.method](r.url, r.func); + }); + }); + +}; diff --git a/server/tp4/routes/item.js b/server/tp4/routes/item.js new file mode 100644 index 000000000..736f16938 --- /dev/null +++ b/server/tp4/routes/item.js @@ -0,0 +1,34 @@ +'use strict'; + +const + item_ctrl = require('../controllers/item'); + +module.exports = [ + + { + url: '/item', + method: 'get', + func: [item_ctrl.get_all] + }, + { + url: '/item', + method: 'post', + func: [item_ctrl.create] + }, + { + url: '/item/:item_id', + method: 'get', + func: [item_ctrl.get_by_id] + }, + { + url: '/item/:item_id', + method: 'put', + func: [item_ctrl.update_by_id] + }, + { + url: '/item/:item_id', + method: 'delete', + func: [item_ctrl.delete_by_id] + } + +]; diff --git a/server/tp4/routes/list.js b/server/tp4/routes/list.js new file mode 100644 index 000000000..2ce1eb7d9 --- /dev/null +++ b/server/tp4/routes/list.js @@ -0,0 +1,45 @@ +'use strict'; + +const + list_ctrl = require('../controllers/list'), + user_ctrl = require('../controllers/user'); + +module.exports = [ + + { + url: '/list', + method: 'get', + func: [user_ctrl.identify_client, list_ctrl.get_all_of_user] + }, + { + url: '/list', + method: 'post', + func: [user_ctrl.identify_client, list_ctrl.create_of_user] + }, + { + url: '/list/:list_id', + method: 'get', + func: [user_ctrl.identify_client, list_ctrl.is_of_user, list_ctrl.get_by_id] + }, + { + url: '/list/:list_id', + method: 'put', + func: [user_ctrl.identify_client, list_ctrl.is_of_user, list_ctrl.update_by_id] + }, + { + url: '/list/:list_id', + method: 'delete', + func: [user_ctrl.identify_client, list_ctrl.is_of_user, list_ctrl.delete_by_id] + }, + { + url: '/list/:list_id/items', + method: 'get', + func: [user_ctrl.identify_client, list_ctrl.is_of_user, list_ctrl.get_items_of_id] + }, + { + url: '/list/:list_id/items', + method: 'post', + func: [user_ctrl.identify_client, list_ctrl.is_of_user, list_ctrl.add_item_to_id] + }, + +]; diff --git a/server/tp4/routes/user.js b/server/tp4/routes/user.js new file mode 100644 index 000000000..545ec1706 --- /dev/null +++ b/server/tp4/routes/user.js @@ -0,0 +1,51 @@ +'use strict'; + +const user_ctrl = require('../controllers/user'); + +function client_is_user_id(req, res, next) { + if (req.user.id == req.params.user_id) { + return next(); + } else { + throw { status: 403, message: 'Action is not authorized' }; + } +} + +module.exports = [ + + { + url: '/user', + method: 'get', + func: user_ctrl.get_all + }, + { + url: '/user/signup', + method: 'post', + func: user_ctrl.signup + }, + { + url: '/user/signin', + method: 'post', + func: user_ctrl.signin + }, + { + url: '/user/whoami', + method: 'get', + func: [user_ctrl.identify_client, user_ctrl.whoami] + }, + { + url: '/user/:user_id', + method: 'get', + func: user_ctrl.get_by_id + }, + { + url: '/user/:user_id', + method: 'put', + func: [user_ctrl.identify_client, client_is_user_id, user_ctrl.update_by_id] + }, + { + url: '/user/:user_id', + method: 'delete', + func: [user_ctrl.identify_client, client_is_user_id, user_ctrl.delete_by_id] + } + +]; diff --git a/server/tp4/server.js b/server/tp4/server.js new file mode 100644 index 000000000..b156aa1db --- /dev/null +++ b/server/tp4/server.js @@ -0,0 +1,31 @@ +'use strict'; + +const + express = require('express'), + bodyParser = require('body-parser'), + cors = require('cors'); + +const app = express(); + +app.use(cors()); +// app.use(express.static('../build')); +app.use(bodyParser.json()); + +// register routes +require('./routes')(app); + +// register error handling middleware +app.use(function (err, req, res, next) { + if (err.status === undefined) { + return res.status(500).send(err.message); + } else { + return res.status(err.status).send(err.message); + } +}); + +// launch server +const server = app.listen(4200, function () { + const host = server.address().address; + const port = server.address().port; + console.log('App listening at http://%s:%s', host, port); +}); diff --git a/sujets/sujet4.md b/sujets/sujet4.md new file mode 100644 index 000000000..6abb7743f --- /dev/null +++ b/sujets/sujet4.md @@ -0,0 +1,47 @@ +Application Listes +=== + +Dans cette application, on va interagir avec un serveur qui dispose du modèle de données suivant : + - _User_ : id (pk), username, password + - _List_ : id (pk), title, UserId (fk) + - _Item_ : id (pk), title, ListId (fk) + +En plus des routes de gestion des User, l'API fournit les routes suivantes : + - `GET /list` : renvoie un tableau d'objets _List_ + - `POST /list` : attend un objet contenant un champ `title`, crée et renvoie une nouvelle _List_ + - `DELETE /list/:id` : supprime la _List_ d'id `id` + - `GET /list/:id/item` : renvoie un tableau d'objets _Item_ associés à la _List_ d'id `id` + - `POST /list/:id/item` : attend un objet contenant un champ `title`, crée et renvoie un nouvel _Item_ associé à _List_ d'id `id` + - `DELETE /item/:id` : supprime l'_Item_ d'id `id` + +Toutes ces routes ne sont accessibles qu'à un utilisateur authentifié et attendent un token dans l'en-tête `Authorization`. + +> ___Indication___ : le code serveur fourni a besoin d'un serveur MySql avec une base existante (les tables sont créées si besoin au lancement). Il vous faut modifier le fichier `list_api/models/index.js` pour y saisir les nom de user, mot de passe et nom de base. + +___ + +Ajouter une route `/lists` dans l'application (ainsi que l'entrée de menu correspondante) dans laquelle on rend un composant `Lists`. + +Au montage du composant `Lists`, l'ensemble des _List_ de l'utilisateur sont récupérées. +Les titres de ces _List_ sont affichés avec pour chacun un bouton permettant d'en demander la suppression et un bouton permettant d'en afficher le détail. +Un composant `AddListForm` permet l'ajout d'une nouvelle _List_. + +Une demande de détail pour la _List_ d'id `id` nous fait changer de route (client) pour aller à la route `/lists/:id`. +C'est dans cette sous-route que l'on rend un composant `ListDetail`. + +> ___Indications___ : on peut déclarer un composant `Route` à n'importe quel niveau de l'application, pas uniquement au top level. Pour déclarer une sous-route en fonction de la route courante (en concaténant la sous-route après la route courante par exemple), on peut récupérer la route courante grâce à la fonction `useRouteMatch` de React Router. On récupère les valeurs des paramètres des routes paramétrées (ici `:id`) grâce à la fonction `useParams`. + +Ce dernier récupère et affiche l'ensemble des _Item_ de la _List_ avec également pour chacun un bouton permettant d'en demander la suppression. +Un composant `AddItemForm` permet l'ajout d'un nouvel _Item_ dans la _List_. + +> ___Contraintes___ : utiliser la bibliothèque `react-query` pour accéder aux données (`useQuery`) ainsi que pour les ajouts et suppressions (`useMutation`). Toutes les fonctions exécutant les requêtes (`fetch`) et ayant donc connaissance de l'API seront déclarées dans un module séparé. + +**Test ultime** + +Le rafraîchissement de l'application à la route `/lists/2` doit restaurer l'interface dans un état où l'utilisateur est considéré authentifié, les titres des `List` de cet utilisateurs sont affichés ainsi que les _Item_ de la _List_ d'id 2. + +Code splitting +--- + +Faire en sorte que le code du composant `Lists` soit chargé dynamiquement uniquement lors du premier accès à cette section de l'application : +https://reactjs.org/docs/code-splitting.html#route-based-code-splitting -- GitLab