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