From 2239df70b30cc89514f0a7e3ecfd5d64905d3eca Mon Sep 17 00:00:00 2001 From: Divlo Date: Wed, 25 Mar 2020 16:23:43 +0100 Subject: [PATCH] backend: Login/Inscription --- api/.env.example | 7 +- api/app.js | 2 + api/assets/config/config.js | 11 ++ api/assets/config/emails.js | 53 +++++++++ api/assets/config/transporter.js | 6 ++ api/assets/images/functions/default.png | Bin 0 -> 16502 bytes api/assets/images/users/default.png | Bin 0 -> 9359 bytes api/controllers/admin.js | 8 ++ api/controllers/users.js | 75 +++++++++++++ api/middlewares/isAdmin.js | 23 ++++ api/middlewares/isAuth.js | 25 +++++ api/models/functions.js | 3 +- api/models/users.js | 50 +++++++++ api/package-lock.json | 136 +++++++++++++++++++++++- api/package.json | 7 +- api/routes/admin.js | 11 ++ api/routes/users.js | 52 +++++++++ 17 files changed, 461 insertions(+), 8 deletions(-) create mode 100644 api/assets/config/emails.js create mode 100644 api/assets/config/transporter.js create mode 100644 api/assets/images/functions/default.png create mode 100644 api/assets/images/users/default.png create mode 100644 api/controllers/admin.js create mode 100644 api/controllers/users.js create mode 100644 api/middlewares/isAdmin.js create mode 100644 api/middlewares/isAuth.js create mode 100644 api/models/users.js create mode 100644 api/routes/admin.js create mode 100644 api/routes/users.js diff --git a/api/.env.example b/api/.env.example index 9ab37e8..570e038 100644 --- a/api/.env.example +++ b/api/.env.example @@ -1,5 +1,10 @@ +HOST = "http://localhost:8080" OpenWeatherMap_API_KEY = "" DB_HOST = "" DB_NAME = "" DB_USER = "" -DB_PASS = "" \ No newline at end of file +DB_PASS = "" +JWT_SECRET = "" +EMAIL_HOST = "" +EMAIL_USER = "" +EMAIL_PASSWORD = "" \ No newline at end of file diff --git a/api/app.js b/api/app.js index 4fb5494..cae56c7 100644 --- a/api/app.js +++ b/api/app.js @@ -22,6 +22,8 @@ app.use(express.json()); app.use('/images', express.static(path.join(__dirname, "assets", "images"))); app.use('/functions', require('./routes/functions')); app.use('/categories', require('./routes/categories')); +app.use('/users', require('./routes/users')); +app.use('/admin', require('./routes/admin')); /* Errors Handling */ app.use((_req, _res, next) => errorHandling(next, { statusCode: 404, message: "La route n'existe pas!" })); // 404 diff --git a/api/assets/config/config.js b/api/assets/config/config.js index a01ae23..5942a2e 100644 --- a/api/assets/config/config.js +++ b/api/assets/config/config.js @@ -1,5 +1,6 @@ const config = { PORT: process.env.PORT || 8080, + HOST: process.env.HOST, WEATHER_API_KEY: process.env.OpenWeatherMap_API_KEY, DATABASE: { host: process.env.DB_HOST, @@ -7,6 +8,16 @@ const config = { user: process.env.DB_USER, password: process.env.DB_PASS }, + JWT_SECRET: process.env.JWT_SECRET, + EMAIL_INFO: { + host: process.env.EMAIL_HOST, + port: 465, + secure: true, // true for 465, false for other ports + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASSWORD + } + } }; module.exports = config; \ No newline at end of file diff --git a/api/assets/config/emails.js b/api/assets/config/emails.js new file mode 100644 index 0000000..c2d3de6 --- /dev/null +++ b/api/assets/config/emails.js @@ -0,0 +1,53 @@ +exports.signupEmail = (url) => ` +
+ + + + + + +
+ + + + + + + + + +
+ + + + + + +
+

FunctionProject

+
+
+ + + + + + +
+

+ Veuillez confirmer l'inscription +

+ Oui, je m'inscris. +
+
+

Si vous avez reçu ce message + par erreur, il suffit de le supprimer. Vous ne serez pas + inscrit si vous ne cliquez pas sur le lien de + confirmation ci-dessus.

+
+
+
+
+
+`; \ No newline at end of file diff --git a/api/assets/config/transporter.js b/api/assets/config/transporter.js new file mode 100644 index 0000000..6f32737 --- /dev/null +++ b/api/assets/config/transporter.js @@ -0,0 +1,6 @@ +const nodemailer = require('nodemailer'); +const { EMAIL_INFO } = require('./config'); + +const transporter = nodemailer.createTransport(EMAIL_INFO); + +module.exports = transporter; \ No newline at end of file diff --git a/api/assets/images/functions/default.png b/api/assets/images/functions/default.png new file mode 100644 index 0000000000000000000000000000000000000000..292dee93b9909a161d5130bad951e3fb5c68b48e GIT binary patch literal 16502 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

V@)^dGu$bF^$RQJ!H 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 { + // TODO: Pouvoir créé une fonction + res.status(200).json({ message: "test"}); +} \ No newline at end of file diff --git a/api/controllers/users.js b/api/controllers/users.js new file mode 100644 index 0000000..408fe65 --- /dev/null +++ b/api/controllers/users.js @@ -0,0 +1,75 @@ +const { validationResult } = require('express-validator'); +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); +const uuid = require('uuid'); +const errorHandling = require('../assets/utils/errorHandling'); +const { serverError, generalError } = require('../assets/config/errors'); +const { JWT_SECRET } = require('../assets/config/config'); +const transporter = require('../assets/config/transporter'); +const { EMAIL_INFO, HOST } = require('../assets/config/config'); +const { signupEmail } = require('../assets/config/emails'); +const Users = require('../models/users'); + +exports.signup = async (req, res, next) => { + const { name, email, password } = req.body; + const errors = validationResult(req); + if (!errors.isEmpty()) { + return errorHandling(next, { message: errors.array()[0].msg, statusCode: 400 }); + } + try { + const hashedPassword = await bcrypt.hash(password, 12); + const tempToken = uuid.v4(); + await Users.create({ email, name, password: hashedPassword, tempToken }); + await transporter.sendMail({ + from: `"FunctionProject" <${EMAIL_INFO.auth.user}>`, + to: email, + subject: "FunctionProject - Confirmer l'inscription", + html: signupEmail(`${HOST}/users/confirm-email/${tempToken}`) + }); + return res.status(201).json({ result: "Vous y êtes presque, veuillez vérifier votre boite d'emails pour confirmer l'inscription." }); + } catch (error) { + console.log(error); + errorHandling(next, serverError); + } +} + +exports.login = async (req, res, next) => { + const { email, password } = req.body; + try { + const user = await Users.findOne({ where: { email, confirmed: true } }); + if (!user) { + return errorHandling(next, { message: "Le mot de passe ou l'adresse email n'est pas valide.", statusCode: 400 }); + } + const isEqual = await bcrypt.compare(password, user.password); + if (!isEqual) { + return errorHandling(next, { message: "Le mot de passe ou l'adresse email n'est pas valide.", statusCode: 400 }); + } + const token = jwt.sign({ + email: user.email, userId: user.id + }, JWT_SECRET, { expiresIn: '1h' }); + return res.status(200).json({ token, id: user.id, name: user.name, email: user.email, logo: user.logo, isAdmin: user.isAdmin, createdAt: user.createdAt }); + } catch (error) { + console.log(error); + errorHandling(next, serverError); + } +} + +exports.confirmEmail = async (req, res, next) => { + const { tempToken } = req.params; + if (!tempToken) { + return errorHandling(next, generalError); + } + try { + const user = await Users.findOne({ where: { tempToken, isConfirmed: false } }); + if (!user) { + return errorHandling(next, { message: "Le token n'est pas valide.", statusCode: 400 }); + } + user.tempToken = null; + user.isConfirmed = true; + await user.save(); + return res.redirect('https://function.divlo.fr'); + } catch (error) { + console.log(error); + errorHandling(next, serverError); + } +} \ No newline at end of file diff --git a/api/middlewares/isAdmin.js b/api/middlewares/isAdmin.js new file mode 100644 index 0000000..d783a23 --- /dev/null +++ b/api/middlewares/isAdmin.js @@ -0,0 +1,23 @@ +const errorHandling = require('../assets/utils/errorHandling'); +const { serverError } = require('../assets/config/errors'); +const Users = require('../models/users'); + +module.exports = (req, _res, next) => { + if (!req.userId) { + return errorHandling(next, { message: "Vous n'êtes pas connecté.", statusCode: 401 }); + } + Users.findOne({ where: { id: req.userId } }) + .then((user) => { + if (!user) { + return errorHandling(next, { message: "Le mot de passe ou l'adresse email n'est pas valide.", statusCode: 400 }); + } + if (!user.isAdmin) { + return errorHandling(next, { message: "Vous n'êtes pas administrateur.", statusCode: 400 }); + } + next(); + }) + .catch((error) => { + console.log(error); + errorHandling(next, serverError); + }); +} \ No newline at end of file diff --git a/api/middlewares/isAuth.js b/api/middlewares/isAuth.js new file mode 100644 index 0000000..5eb3b56 --- /dev/null +++ b/api/middlewares/isAuth.js @@ -0,0 +1,25 @@ +const jwt = require('jsonwebtoken'); +const errorHandling = require('../assets/utils/errorHandling'); +const { serverError } = require('../assets/config/errors'); +const { JWT_SECRET } = require('../assets/config/config'); + +module.exports = (req, _res, next) => { + const token = req.get('Authorization'); + if (!token) { + return errorHandling(next, { message: "Vous n'êtes pas connecté.", statusCode: 401 }); + } + + let decodedToken; + try { + decodedToken = jwt.verify(token, JWT_SECRET); + } catch (error) { + return errorHandling(next, serverError); + } + + if (!decodedToken) { + return errorHandling(next, { message: "Vous n'êtes pas connecté.", statusCode: 401 }); + } + + req.userId = decodedToken.userId; + next(); +} \ No newline at end of file diff --git a/api/models/functions.js b/api/models/functions.js index dc3fb6e..4fa6bed 100644 --- a/api/models/functions.js +++ b/api/models/functions.js @@ -22,7 +22,8 @@ module.exports = sequelize.define('function', { }, image: { type: Sequelize.STRING, - allowNull: false + allowNull: false, + defaultValue: "/images/functions/default.png" }, type: { type: Sequelize.STRING, diff --git a/api/models/users.js b/api/models/users.js new file mode 100644 index 0000000..c1d794c --- /dev/null +++ b/api/models/users.js @@ -0,0 +1,50 @@ +const Sequelize = require('sequelize'); +const sequelize = require('../assets/utils/database'); + +module.exports = sequelize.define('user', { + id: { + type: Sequelize.INTEGER, + allowNull: false, + autoIncrement: true, + primaryKey: true + }, + name: { + type: Sequelize.STRING, + allowNull: false, + }, + email: { + type: Sequelize.STRING, + allowNull: false, + }, + password: { + type: Sequelize.STRING, + allowNull: false, + }, + biography: { + type: Sequelize.TEXT, + }, + logo: { + type: Sequelize.STRING, + defaultValue: "/images/users/default.png" + }, + isConfirmed: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + isPublicEmail: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + isAdmin: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + tempToken: { + type: Sequelize.TEXT, + allowNull: true + }, + tempExpirationToken: { + type: Sequelize.DATE, + allowNull: true + } +}); \ No newline at end of file diff --git a/api/package-lock.json b/api/package-lock.json index 2ed1cf1..c645886 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -96,6 +96,11 @@ "safe-buffer": "5.1.2" } }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" + }, "binary-extensions": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", @@ -163,6 +168,11 @@ "fill-range": "^7.0.1" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -410,6 +420,14 @@ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", "dev": true }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -498,6 +516,22 @@ "vary": "~1.1.2" } }, + "express-validator": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.4.0.tgz", + "integrity": "sha512-Fs+x0yDOSiUV+o5jIRloMyBxqpSzJiMM8KQW1IRVv2l49F6ATU0F9uPa+3K6vXNlLlhUjauv2FCGLFPMaNr24w==", + "requires": { + "lodash": "^4.17.15", + "validator": "^12.1.0" + }, + "dependencies": { + "validator": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-12.2.0.tgz", + "integrity": "sha512-jJfE/DW6tIK1Ek8nCfNFqt8Wb3nzMoAbocBF6/Icgg1ZFSBpObdnwVY2jQj6qUqzhx5jc71fpvBWyLGO7Xl+nQ==" + } + } + }, "feature-policy": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/feature-policy/-/feature-policy-0.3.0.tgz", @@ -873,6 +907,49 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "latest-version": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", @@ -887,6 +964,41 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", @@ -1052,6 +1164,11 @@ "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz", "integrity": "sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==" }, + "nodemailer": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.6.tgz", + "integrity": "sha512-/kJ+FYVEm2HuUlw87hjSqTss+GU35D4giOpdSfGp7DO+5h6RlJj7R94YaYHOkoxu1CSaM0d3WRBtCzwXrY6MKA==" + }, "nodemon": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.2.tgz", @@ -1303,8 +1420,7 @@ "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "semver-diff": { "version": "2.1.0", @@ -1386,6 +1502,11 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" } } }, @@ -1457,6 +1578,11 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" } } }, @@ -1624,9 +1750,9 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.2.tgz", + "integrity": "sha512-vy9V/+pKG+5ZTYKf+VcphF5Oc6EFiu3W8Nv3P3zIh0EqVI80ZxOzuPfe9EHjkFNvf8+xuTHVeei4Drydlx4zjw==" }, "validator": { "version": "10.11.0", diff --git a/api/package.json b/api/package.json index 426d009..f77ef92 100644 --- a/api/package.json +++ b/api/package.json @@ -11,13 +11,18 @@ "license": "MIT", "dependencies": { "axios": "^0.19.2", + "bcryptjs": "^2.4.3", "cors": "^2.8.5", "express": "^4.17.1", + "express-validator": "^6.4.0", "helmet": "^3.21.3", + "jsonwebtoken": "^8.5.1", "moment": "^2.24.0", "mysql2": "^2.1.0", + "nodemailer": "^6.4.6", "sequelize": "^5.21.5", - "smart-request-balancer": "^2.1.1" + "smart-request-balancer": "^2.1.1", + "uuid": "^7.0.2" }, "devDependencies": { "dotenv": "^8.2.0", diff --git a/api/routes/admin.js b/api/routes/admin.js new file mode 100644 index 0000000..6e9cb72 --- /dev/null +++ b/api/routes/admin.js @@ -0,0 +1,11 @@ +const { Router } = require('express'); +const adminController = require('../controllers/admin'); +const isAuth = require('../middlewares/isAuth'); +const isAdmin = require('../middlewares/isAdmin'); + +const AdminRouter = Router(); + +// Permet de créé une fonction +AdminRouter.post('/functions', isAuth, isAdmin, adminController.postFunction); + +module.exports = AdminRouter; \ No newline at end of file diff --git a/api/routes/users.js b/api/routes/users.js new file mode 100644 index 0000000..d0c4879 --- /dev/null +++ b/api/routes/users.js @@ -0,0 +1,52 @@ +const { Router } = require('express'); +const { body } = require('express-validator'); +const usersController = require('../controllers/users'); +const Users = require('../models/users'); + +const UsersRouter = Router(); + +// Permet de se connecter +UsersRouter.post('/login', usersController.login); + +// Permet de s'inscrire +UsersRouter.post('/signup', [ + body('email') + .isEmail() + .withMessage("Veuillez rentré une adresse mail valide.") + .custom((async (email) => { + try { + const user = await Users.findOne({ where: { email } }); + if (user) { + return Promise.reject("L'adresse email existe déjà..."); + } + } catch (error) { + return console.log(error); + } + })) + .normalizeEmail(), + body('password') + .isLength({ min: 4 }) + .withMessage("Votre mot de passe est trop court!"), + body('name') + .trim() + .not() + .isEmpty() + .withMessage("Votre nom ne peut pas être vide.") + .isAlphanumeric() + .withMessage("Votre nom ne peut contenir que des lettres ou/et des nombres.") + .custom((async (name) => { + try { + const user = await Users.findOne({ where: { name } }); + if (user) { + return Promise.reject("Le nom existe déjà..."); + } + } catch (error) { + return console.log(error); + } + })) +], usersController.signup); + +// Confirme l'inscription +UsersRouter.get('/confirm-email/:tempToken', usersController.confirmEmail); + +module.exports = UsersRouter; \ No newline at end of file