Compare commits
3 Commits
59c4b0de38
...
f51079eeb4
| Author | SHA1 | Date | |
|---|---|---|---|
| f51079eeb4 | |||
| fae622aafc | |||
| 5e6da61ccb |
80
README.md
Normal file
80
README.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Présentation
|
||||
Ce projet sert de socle technique facilitant le développement de "back-end" d'applications en NodeJS, communiquant avec une base MongoDB.
|
||||
|
||||
## Base de données
|
||||
Il s'agit d'une base MongoDB.
|
||||
|
||||
On peut simplement créer/démarrer la base en utilisant le fichier `docker-compose.yml` :
|
||||
|
||||
```bash
|
||||
$ cd <PROJECT_PATH>/docker
|
||||
$ docker-compose up -d
|
||||
```
|
||||
|
||||
Par défaut, la base utilise le port `27017`.
|
||||
|
||||
# Application Back-end
|
||||
## Notions théoriques
|
||||
### Route
|
||||
Une route correspond à l'URI associée à un service exposé.
|
||||
|
||||
Typiquement, une route est composée d'un chemin, par exemple `/users`, et d'un verbe http, par exemple `POST`.
|
||||
|
||||
### Controleurs
|
||||
Un controlleur est un composant qui expose des services via la déclaration de routes.
|
||||
|
||||
## Controlleurs
|
||||
### Création
|
||||
Pour créer un controlleur, il suffit de copier-coller le fichier `src/js/controller/templateCtrl.js` et de renommer la copie, de préférence, de cette façon `<entity>Ctrl.js`.
|
||||
|
||||
Par exemple, si on veut exposer un service sur des entités représentant des voitures, on appelerait ce controlleur `carCtrl.js`.
|
||||
|
||||
### Déclaration
|
||||
Une fois le controlleur créé, les routes qu'il contient ne sont pas effectives. En effet, le controlleur est créé mais n'est pas lié à l'application `Express`.
|
||||
|
||||
Pour que notre nouveau controlleur soit utilisée par notre application, il va falloir le déclarer en modifiant le fichier `src/js/app.js`.
|
||||
|
||||
Dans un premier temps, il va falloir importer notre controlleur, en utilisant l'instruction suivante, mais de préférence en haut du fichier `src/js/app.js` :
|
||||
|
||||
```javascript
|
||||
const carController = require('./controller/carCtrl');
|
||||
```
|
||||
|
||||
Puis, dans un second temps, il va falloir déclarer le controlleur dans l'application `Express`, en utilisant la méthode `use` comme ce qui suit :
|
||||
|
||||
```javascript
|
||||
app.use('/cars', carController);
|
||||
```
|
||||
|
||||
Voilà, votre controlleur est effectif, les services qu'il expose sont désormais accéssibles.
|
||||
|
||||
# Communication avec la base de données
|
||||
## Notions théoriques
|
||||
MongoDB est un gestionnaire de base de données `NoSQL`, et orienté `Document`s.
|
||||
|
||||
Un document est la représentation `JSON` d'entités.
|
||||
|
||||
Les documents sont stockés dans des `collections`, ce sont des sortes de "dépôts".
|
||||
Par convention, on stocke les mêmes types d'entités dans une seule et même collection.
|
||||
|
||||
Imaginons que notre application permette de gérer des voitures et des clients, alors on créera deux collections dans notre base de données : `cars` et `customers`.
|
||||
|
||||
Si notre application doit gérer plusieurs types de véhicules, des voitures essences, diesel, électriques, hybrides etc., alors on doit se demander si toutes ces entités doivent être stockées dans la même collection `cars`, ou si c'est plus pertinent d'en créer d'autres.
|
||||
|
||||
Si la différence entre les différentes catégories de véhicules n'est pas très grande et peu importante dans la logique métier de l'application, alors il vaut mieux stocker toutes les voitures dans la même collection.
|
||||
|
||||
## Manipulation d'une collection
|
||||
Il existe une classe permettant de créer/modifier/supprimer/récupérer des entités dans la base Mongo.
|
||||
|
||||
Il s'agit de la classe `Repository`, définie dans le fichier `src/js/repository/repository.js`.
|
||||
|
||||
Si on veut exposer des services pour manipuler des voitures, qui sont stockées dans la collection `cars`, il suffit alors de créer un objet `Repository`, en passant en paramètre du constructeur de cette classe, le nom de la collection :
|
||||
|
||||
```javascript
|
||||
// En haut du fichier :
|
||||
const Repository = require('../repository/repository');
|
||||
...
|
||||
const carRepository = new Repository('cars');
|
||||
```
|
||||
|
||||
Ensuite, il suffira d'utiliser les méthodes suivantes en fonction de l'implémentation souaitée des services qu'on veut exposer : `find`, `insert`, `update` et `delete`.
|
||||
@@ -11,8 +11,11 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.0.0",
|
||||
"express": "^4.17.1",
|
||||
"mongodb": "^3.6.1"
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mongodb": "^3.6.1",
|
||||
"yaml": "^1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.4.0",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const applicationController = require('./controller/applicationCtrl');
|
||||
const userController = require('./controller/userCtrl');
|
||||
|
||||
const port = 3000;
|
||||
|
||||
@@ -9,5 +10,6 @@ app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use(bodyParser.json());
|
||||
|
||||
app.use('/apps', applicationController);
|
||||
app.use('/users', userController);
|
||||
|
||||
app.listen(port, () => console.log('Mock is listening at port ', port, '\n'));
|
||||
@@ -1,8 +1,16 @@
|
||||
module.exports = {
|
||||
database: {
|
||||
url: 'mongodb://localhost:27017',
|
||||
username: 'express-user',
|
||||
password: 'P@ssword1',
|
||||
database: 'express-test'
|
||||
}
|
||||
};
|
||||
const fs = require('fs');
|
||||
const yaml = require('yaml');
|
||||
|
||||
const configurationFile = fs.readFileSync('src/resources/application.yml', 'utf8');
|
||||
const configuration = yaml.parse(configurationFile);
|
||||
console.log(configuration);
|
||||
module.exports = configuration;
|
||||
|
||||
// module.exports = {
|
||||
// database: {
|
||||
// url: 'mongodb://localhost:27017',
|
||||
// username: 'express-user',
|
||||
// password: 'P@ssword1',
|
||||
// database: 'express-test'
|
||||
// }
|
||||
// };
|
||||
@@ -1,4 +1,3 @@
|
||||
const ObjectId = require('mongodb').ObjectId;
|
||||
const Repository = require('../repository/repository');
|
||||
const router = require('express').Router();
|
||||
|
||||
@@ -10,6 +9,12 @@ router.get('/', (request, response) => {
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:applicationId', (request, response) => {
|
||||
applicationRepository.find({_id: request.params.applicationId}, (result) => {
|
||||
response.json(result[0]);
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/', (request, response) => {
|
||||
applicationRepository.insert(request.body, (result) => {
|
||||
response.json(result);
|
||||
@@ -17,7 +22,7 @@ router.post('/', (request, response) => {
|
||||
});
|
||||
|
||||
router.put('/:applicationId', (request, response) => {
|
||||
const applicationId = ObjectId(request.params.applicationId);
|
||||
const applicationId = request.params.applicationId;
|
||||
applicationRepository.find({_id: applicationId}, entity => {
|
||||
if (entity.length === 0) {
|
||||
response.status(404).send();
|
||||
@@ -32,7 +37,7 @@ router.put('/:applicationId', (request, response) => {
|
||||
});
|
||||
|
||||
router.delete('/:applicationId', (request, response) => {
|
||||
const applicationId = ObjectId(request.params.applicationId);
|
||||
const applicationId = request.params.applicationId;
|
||||
applicationRepository.find({_id: applicationId}, entity => {
|
||||
if (entity.length === 0) {
|
||||
response.status(404).send();
|
||||
|
||||
6
src/js/controller/templateCtrl.js
Normal file
6
src/js/controller/templateCtrl.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const Repository = require('../repository/repository');
|
||||
const router = require('express').Router();
|
||||
|
||||
// Develop routes here
|
||||
|
||||
module.exports = router;
|
||||
17
src/js/controller/userCtrl.js
Normal file
17
src/js/controller/userCtrl.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const router = require('express').Router();
|
||||
const tokenService = require('../service/tokenService');
|
||||
const userService = require('../service/userService');
|
||||
|
||||
// Develop routes here
|
||||
router.post('/login', (request, response) => {
|
||||
const loginRequest = request.body;
|
||||
|
||||
if (!!loginRequest && userService.areCredentialsValid(loginRequest.login, loginRequest.password)) {
|
||||
const tokenPayload = { login: loginRequest.login };
|
||||
response.json(tokenService.build(tokenPayload));
|
||||
} else {
|
||||
response.status(403).send();
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,13 +1,14 @@
|
||||
const mongodb = require('mongodb');
|
||||
const assert = require('assert');
|
||||
|
||||
const configuration = require('../configuration');
|
||||
const mongoConfig = configuration.database;
|
||||
const mongoConfig = configuration.mongodb;
|
||||
|
||||
class MongoClient {
|
||||
constructor() {
|
||||
mongodb.MongoClient.connect(mongoConfig.url, (error, client) => {
|
||||
assert.equal(null, error, `Unable to connect to mongodb: ${error}.`);
|
||||
if (error !== null) {
|
||||
throw new Error(`Unable de connect to Mongo database: ${error}`);
|
||||
}
|
||||
|
||||
console.log('Connected successfuly to mongodb');
|
||||
this.db = client.db(mongoConfig.database);
|
||||
});
|
||||
@@ -19,12 +20,17 @@ class MongoClient {
|
||||
console.log(`Entities ${collectionName} founded.`);
|
||||
callback(results);
|
||||
})
|
||||
.catch(error => console.error(error));
|
||||
.catch(error => {
|
||||
throw new Error(`Unable to find entities in collection ${collectionName}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
insert(collectionName, entity, callback) {
|
||||
this.db.collection(collectionName).insert(entity, (error, result) => {
|
||||
assert.equal(null, error, `Unable to insert ${collectionName} entity: ${error}.`);
|
||||
this.db.collection(collectionName).insertOne(entity, (error, result) => {
|
||||
if (error !== null) {
|
||||
throw new Error(`Unable to insert ${collectionName} entity: ${error}`);
|
||||
}
|
||||
|
||||
console.log(`Entity ${collectionName} inserted.`);
|
||||
// Return only the inserted document.
|
||||
callback(result.ops[0]);
|
||||
@@ -32,8 +38,11 @@ class MongoClient {
|
||||
}
|
||||
|
||||
update(collectionName, entity, callback) {
|
||||
this.db.collection(collectionName).save(entity, (error) => {
|
||||
assert.equal(null, error, `Unable to update ${collectionName} entity: ${error}.`);
|
||||
this.db.collection(collectionName).updateOne({_id: mongodb.ObjectId(entity._id)}, {$set: entity}, {upsert: true}, (error) => {
|
||||
if (error !== null) {
|
||||
throw new Error(`Unable to update ${collectionName} entity: ${error}`);
|
||||
}
|
||||
|
||||
console.log(`Entity ${collectionName} updated.`);
|
||||
callback();
|
||||
});
|
||||
@@ -41,12 +50,16 @@ class MongoClient {
|
||||
|
||||
delete(collectionName, entityId, callback) {
|
||||
this.db.collection(collectionName).deleteOne({_id: mongodb.ObjectId(entityId)}, (error) => {
|
||||
assert.equal(null, error, `Unable to delete ${collectionName} entity with id ${entityId}: ${error}.`);
|
||||
if (error !== null) {
|
||||
throw new Error(`Unable to delete ${collectionName} entity with id ${entityId}: ${error}`);
|
||||
}
|
||||
|
||||
console.log(`Entity ${collectionName} with id ${entityId} deleted.`);
|
||||
callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Define a singleton of class "MongoClient".
|
||||
const mongoClient = new MongoClient();
|
||||
module.exports = mongoClient;
|
||||
@@ -1,26 +1,63 @@
|
||||
const mongoClient = require('./mongoClient');
|
||||
const ObjectId = require('mongodb').ObjectId;
|
||||
|
||||
class Repository {
|
||||
/**
|
||||
* If entity in parameters has an attribute named "_id", this function converts it into a MongoDB {@code ObjectId}.
|
||||
*/
|
||||
function convertIdToMongodbFormat(entity) {
|
||||
if (!!entity._id && !(entity._id instanceof ObjectId)) {
|
||||
entity._id = ObjectId(entity._id);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = class Repository {
|
||||
/**
|
||||
* Creates a new repository which read and write into the {@code collectionName} collection in database.
|
||||
* @param {*} collectionName
|
||||
*/
|
||||
constructor(collectionName) {
|
||||
this.collectionName = collectionName;
|
||||
this.mongoClient = mongoClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entities that matches criteria in {@code query}.
|
||||
* @param {*} query The query which contains criteria to find some entities.
|
||||
* @param {*} callback The function to execute after getting entities.
|
||||
*/
|
||||
find(query, callback) {
|
||||
convertIdToMongodbFormat(query);
|
||||
this.mongoClient.find(this.collectionName, query, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert entity in database.
|
||||
* @param {*} entity The entity to insert into database.
|
||||
* @param {*} callback The function to execute after inserting entity.
|
||||
*/
|
||||
insert(entity, callback) {
|
||||
this.mongoClient.insert(this.collectionName, entity, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the whole entity in database.
|
||||
* @param {*} entity The entity to update into database.
|
||||
* @param {*} callback The function to execute after updating entity.
|
||||
*/
|
||||
update(entity, callback) {
|
||||
convertIdToMongodbFormat(entity);
|
||||
this.mongoClient.update(this.collectionName, entity, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete entity in database.
|
||||
* @param {*} entityId Entity to delete id.
|
||||
* @param {*} callback The function to execute after deleting entity.
|
||||
*/
|
||||
delete(entityId, callback) {
|
||||
if (!(entityId instanceof ObjectId)) {
|
||||
entityId = ObjectId(entityId);
|
||||
}
|
||||
this.mongoClient.delete(this.collectionName, entityId, callback);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Repository;
|
||||
};
|
||||
16
src/js/service/passwordService.js
Normal file
16
src/js/service/passwordService.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const bcrypt = require('bcrypt');
|
||||
const saltRounds = 10;
|
||||
|
||||
class PasswordService {
|
||||
hashPassword(password) {
|
||||
const salt = bcrypt.genSaltSync(saltRounds);
|
||||
return bcrypt.hashSync(password, salt);
|
||||
}
|
||||
|
||||
areSamePasswords(plainTextPassword, hashedPassword) {
|
||||
return bcrypt.compareSync(plainTextPassword, hashedPassword);
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new PasswordService();
|
||||
module.exports = singleton;
|
||||
21
src/js/service/tokenService.js
Normal file
21
src/js/service/tokenService.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const configuration = require('../configuration');
|
||||
const securityConfig = configuration.security;
|
||||
|
||||
class TokenService {
|
||||
build(tokenPayload) {
|
||||
return jwt.sign(tokenPayload, securityConfig.jwt.secret, {expiresIn: securityConfig.jwt.validity});
|
||||
}
|
||||
|
||||
isValid(token) {
|
||||
try {
|
||||
jwt.verify(token, securityConfig.jwt.secret);
|
||||
} catch (exception) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const jwtInstance = new TokenService();
|
||||
module.exports = jwtInstance;
|
||||
26
src/js/service/userService.js
Normal file
26
src/js/service/userService.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const Repository = require('../repository/repository');
|
||||
const passwordService = require('./passwordService');
|
||||
|
||||
const userRepository = new Repository('users');
|
||||
|
||||
class UserService {
|
||||
getUser(login) {
|
||||
return login === 'toto'
|
||||
? {login: 'toto', password: passwordService.hashPassword('pwd')}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
isAuthenticated() {
|
||||
return false;
|
||||
}
|
||||
|
||||
areCredentialsValid(login, password) {
|
||||
const user = this.getUser(login);
|
||||
// If login is incorrect, the "getUser" function will return "undefined".
|
||||
// So if "user" is not "undefined", this proofs that login is correct.
|
||||
return !!user && passwordService.areSamePasswords(password, user.password);
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new UserService();
|
||||
module.exports = singleton;
|
||||
9
src/resources/application.yml
Normal file
9
src/resources/application.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
mongodb:
|
||||
url: 'mongodb://localhost:27017'
|
||||
username: 'express-user'
|
||||
password: 'P@ssword1'
|
||||
database: 'express-test'
|
||||
security:
|
||||
jwt:
|
||||
secret: 5ubtcCCo7hWBqjNGtzzVKnLT1KxN9uS4D6kRZowCunZAYPmxtKy6mvgoxANe4WqLVfiVI7AZSVqZCtvlSWFwIsnXGH6lxeKG0U8Wu7Kw0jwfFOGLvlO8bXaB
|
||||
validity: 1h
|
||||
Reference in New Issue
Block a user