From 132dae480985c63aec4736da749c68ad1dbea7ee Mon Sep 17 00:00:00 2001 From: Raphix Date: Thu, 2 Nov 2023 11:09:47 +0100 Subject: [PATCH] =?UTF-8?q?Version=200.1.2=20-=20Ajout=20du=20syst=C3=AAme?= =?UTF-8?q?=20d'authentification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- bin/auth.js | 91 ++++++++++ bin/config.js | 37 ++++ bin/global-variables.js | 5 +- bin/keygen.js | 29 +++ bin/users.js | 291 ++++++++++++++++++++++++++++++ bin/www | 7 +- main.js | 10 +- package-lock.json | 35 +++- package.json | 6 +- public/javascripts/basics.js | 5 +- public/javascripts/loginscript.js | 52 +++++- routes/index.js | 10 +- routes/login.js | 52 +++++- views/index.ejs | 2 +- views/login.ejs | 5 +- 16 files changed, 620 insertions(+), 21 deletions(-) create mode 100644 bin/auth.js create mode 100644 bin/config.js create mode 100644 bin/keygen.js create mode 100644 bin/users.js diff --git a/.gitignore b/.gitignore index a238609..65654f3 100644 --- a/.gitignore +++ b/.gitignore @@ -132,4 +132,6 @@ dist #data data -data/* \ No newline at end of file +data/* + +private \ No newline at end of file diff --git a/bin/auth.js b/bin/auth.js new file mode 100644 index 0000000..01121a6 --- /dev/null +++ b/bin/auth.js @@ -0,0 +1,91 @@ +const { LogType } = require("loguix") +const fs = require("fs") +const path = require("path") +const { __glob } = require("./global-variables") +const alog = new LogType("Authentification") +const keygen = require("./keygen") +const users = require("./users") + +/** + * Vérifie si le token est présent et appartient à un utilisateur + * @param {string} token + */ + +module.exports.check = function(token) { + var isApproved = false; + var username = null + users.getUsers().forEach((fetchUser) => { + if(fetchUser.tokens.includes(token)) { + isApproved = true + username = fetchUser.username + } + + }) + + if(isApproved) { + alog.log("Connexion par Token de l'utilisateur : " + username) + return true + } else { + if(token) { + + alog.warn("Erreur d'authentification - Token n'existe pas : " + token) + } + return false + } +} + +/** + * Permet de se connecter à Neutral + * @param {object} data + * @returns Token or AUTH_FAILED + */ +module.exports.login = function(data) { + var username = data.username + var password = data.password + + if(users.getUsers().has(username)) { + const user = users.getUsers().get(username) + if(password == user.getPassword()) { + const token = user.generateToken() + alog.log("Connexion approuvé de l'utilisateur : " + username) + return token + } else { + alog.warn("Echec de connexion de l'utilisateur : " + username + " - Mot de passe incorrect") + return "AUTH_FAILED" + } + + } else { + alog.warn("Echec de connexion de l'utilisateur : " + username + " - Utilisateur non-inscrit dans la base de donnée") + return "AUTH_FAILED" + } +} + +/** + * Remove the token + * @param {string} token + * @returns + */ +module.exports.signout = function(token) { + var isDone = false; + var username = null + users.getUsers().forEach((fetchUser) => { + if(fetchUser.tokens.includes(token)) { + isDone = true + username = fetchUser.username + fetchUser.removeToken(token) + } + + }) + + if(isDone) { + alog.log("Suppression du Token '" + token + "' de l'utilisateur : " + username) + return true + } else { + if(token) { + + alog.warn("Erreur d'opération lors de la déconnexion - Token n'existe pas : " + token) + } + return false + } + +} \ No newline at end of file diff --git a/bin/config.js b/bin/config.js new file mode 100644 index 0000000..bcc307d --- /dev/null +++ b/bin/config.js @@ -0,0 +1,37 @@ +const { LogType } = require("loguix") +const fs = require("fs") +const path = require("path") +const { __glob } = require("./global-variables") +const clog = new LogType("Configuration") + +setup() + +function setup() { + if(!fs.existsSync(__glob.CONFIG)) { + clog.log("Création du fichier de configuration dans : " + __glob.CONFIG) + fs.writeFileSync(__glob.CONFIG, JSON.stringify({ + ENCRYPTION_KEY: "1", + }, null, 2)) + } +} + +/** + * + * @returns Config File + */ +module.exports.getFile = function () { + const file = JSON.parse(fs.readFileSync(__glob.CONFIG)) + return file +} + +/** + * Update le fichier configuration avec un object + * @param {Array} file + */ +module.exports.updateFile = function (file) { + if(fs.existsSync(__glob.CONFIG)) { + clog.log("Mise à jour du fichier configuration dans : " + __glob.CONFIG) + fs.writeFileSync(__glob.CONFIG, JSON.stringify(file, null, 2)) + } +} + diff --git a/bin/global-variables.js b/bin/global-variables.js index 96a2310..802b003 100644 --- a/bin/global-variables.js +++ b/bin/global-variables.js @@ -4,7 +4,10 @@ const root = path.resolve(__dirname, '../') const __glob = { ROUTES: root + path.sep + "routes" + path.sep, ROOT: root, - LOGS: root + path.sep + "logs" + LOGS: root + path.sep + "logs", + DATA: root + path.sep + "data", + USERS: root + path.sep + "data" + path.sep + "users.json", + CONFIG: root + path.sep + "data" + path.sep + "config.json" }; diff --git a/bin/keygen.js b/bin/keygen.js new file mode 100644 index 0000000..9f7a0a0 --- /dev/null +++ b/bin/keygen.js @@ -0,0 +1,29 @@ +const { LogType } = require("loguix") +const fs = require("fs") +const path = require("path") +var CryptoJS = require("crypto-js") +const { __glob } = require("./global-variables") +const clog = new LogType("KeyGen") +const config = require("./config") + +const keypass = config.getFile().ENCRYPTION_KEY + +setup() + +function setup() { + if(keypass) { + clog.log("Clé de chiffrement trouvé et importé") + } else { + clog.error("Clé de chiffrement inconnu : Passage en mode par défaut") + } +} + +module.exports.encrypt = function (text) { + let encryptedText = CryptoJS.AES.encrypt(text, keypass).toString(); + return encryptedText; + } + +module.exports.decrypt = function(text) { + let decryptedText = CryptoJS.AES.decrypt(text, keypass).toString(CryptoJS.enc.Utf8); + return decryptedText; + } \ No newline at end of file diff --git a/bin/users.js b/bin/users.js new file mode 100644 index 0000000..feaa711 --- /dev/null +++ b/bin/users.js @@ -0,0 +1,291 @@ +const { LogType } = require("loguix") +const fs = require("fs") +const path = require("path") +const { __glob } = require("./global-variables") +const ulog = new LogType("Users") +const keygen = require("./keygen") +const uuid = require("uuid") + +var usersList = new Map() + +setup() + +function setup() { + + if(!fs.existsSync(__glob.USERS)) { + ulog.log("Création du fichier utilisateur dans : " + __glob.USERS) + fs.writeFileSync(__glob.USERS, JSON.stringify([], null, 2)) + } +} + +/** + * + * @returns Liste des utilisateurs + */ +module.exports.getUsers = function () { + + return usersList +} + + + +/** + * Get all users from Users Data Base + */ +module.exports.fetchUsers = function () { + + ulog.step.init("fetch_user", "Récupération de tous les utilisateurs inscrit dans la base de donnée") + const userFile = getFile() + usersList = new Map() + for(var userFetched of userFile) { + const user = new this.User({ + username: userFetched.username, + password: userFetched.password, + display_name: userFetched.display_name, + permission: userFetched.permission, + tokens: userFetched.tokens, + lastLogin: userFetched.lastLogin, + + }) + + usersList.set(user.username, user) + } + ulog.step.end("fetch_user") +} + +/** + * User Class is used to access to default user's properties and methods + * @param {object} properties User properties with : username, password, display_name, permission... + */ +module.exports.User = class { + username = null + password = null; + display_name = null + permission = [] + tokens = [] + lastLogin = new Date() + + + constructor(properties) { + + if(properties) { + + this.username = properties.username + this.password = keygen.encrypt(properties.password) + this.display_name = properties.display_name + this.permission = properties.permission + this.tokens = properties.tokens + this.lastLogin = properties.lastLogin + + const userFile = getFile() + + for(var userFetched of userFile) { + if(properties.username == userFetched.username) { + ulog.log("Récupération dans la base de donnée, de l'utilisateur : " + userFetched.username) + this.username = userFetched.username + this.password = userFetched.password + this.display_name = userFetched.display_name + this.permission = userFetched.permission + this.tokens = userFetched.tokens + this.lastLogin = userFetched.lastLogin + } + } + + + + + + } + + + if(this.username == null) { + ulog.error("One of user is without username ! [IMPORANT_FIELD_IS_MISSING]") + this.username = Math.random() + } + if(this.password == null) { + ulog.error("'" + this.username + "' is without password ! Password reset to 'default' [IMPORANT_FIELD_IS_MISSING]") + this.password = keygen.encrypt("default") + } + if(this.display_name == null) { + ulog.warn("'" + this.username + "' is without display name !") + this.display_name = this.username + } + if(this.permission == null) { + ulog.warn("'" + this.username + "' has no longer permission !") + + } + if(this.tokens == null) { + this.tokens = [] + } + if(this.permission == null) { + this.permission = [] + } + if(this.lastLogin == null) { + this.lastLogin = new Date() + } + + + + } + + register() { + + var alreadyExist = false + const userFile = getFile() + + for(var userFetched of userFile) { + if(userFetched.username == this.username) { + userFile.splice(userFile.indexOf(userFetched), 1) + ulog.log("Mise à jour dans la base de donnée, de l'utilisateur : " + this.username) + alreadyExist = true + } + } + if(!alreadyExist) { + ulog.log("Création dans la base de donnée de l'utilisateur : " + this.username) + } + userFile.push(this) + updateFile(userFile) + usersList.set(this.username, this) + } + + unregister() { + + var alreadyExist = false + const userFile = getFile() + + for(var userFetched of userFile) { + if(userFetched.username == this.username) { + userFile.splice(userFile.indexOf(userFetched), 1) + ulog.log("Mise à jour dans la base de donnée, de l'utilisateur : " + this.username) + alreadyExist = true + } + } + if(!alreadyExist) { + ulog.log("L'utilisateur n'est pas enregistré dans la base de donnée : " + this.username) + } + + updateFile(userFile) + usersList.delete(this.username) + } + + checkPermission(name) { + this.#sync() + if(this.permission.includes(name)) { + return true + + } else { + + return false + } + + } + + addPermission(name) { + this.#sync() + for(var perms of this.permission) { + if(name == perms) { + ulog.warn("'" + this.username + "' a déjà la permission : " + name) + return false + } + } + this.permission.push(name) + this.register() + } + + removePermission(name) { + this.#sync() + var havePermission = false + for(var perms of this.permission) { + if(name == perms) { + havePermission = true + } + } + if(havePermission) { + this.permission.splice(this.permission.indexOf(name), 1) + this.register() + } else { + + ulog.warn("'" + this.username + "' n'a pas la permission : " + name) + return false + } + + } + + setPassword(newPassword) { + this.#sync() + this.password = keygen.encrypt(newPassword) + this.register() + ulog.log("Le mot de passe de l'utilisateur a été modifié : " + this.username) + + } + getPassword() { + this.#sync() + return keygen.decrypt(this.password) + } + + generateToken() { + this.#sync() + const gToken = uuid.v4().toString() + this.tokens.push(gToken) + this.register() + return gToken + + } + + removeToken(token) { + this.#sync() + var haveToken = false + for(var aToken of this.tokens) { + if(token == aToken) { + haveToken = true + } + } + if(haveToken) { + this.tokens.splice(this.tokens.indexOf(token), 1) + this.register() + } else { + + ulog.warn("'" + this.username + "' n'a pas le token : " + token) + return false + } + + } + + #sync() { + + for(var userGet of usersList.keys()) { + const userFetched = usersList.get(userGet) + if(this.username == userFetched.username) { + this.username = userFetched.username + this.password = userFetched.password + this.display_name = userFetched.display_name + this.permission = userFetched.permission + this.tokens = userFetched.tokens + this.lastLogin = userFetched.lastLogin + } + } + + } +} + + +/** + * + * @returns User File + */ +function getFile() { + const file = JSON.parse(fs.readFileSync(__glob.USERS)) + return file +} + +/** + * Update le fichier utilisateur avec un object + * @param {Array} file + */ +function updateFile(file) { + if(fs.existsSync(__glob.USERS)) { + ulog.log("Mise à jour du fichier utilisateur dans : " + __glob.USERS) + fs.writeFileSync(__glob.USERS, JSON.stringify(file, null, 2)) + } +} + diff --git a/bin/www b/bin/www index 692c2a4..1fed09d 100644 --- a/bin/www +++ b/bin/www @@ -7,13 +7,15 @@ var log = require("loguix") var {LogType} = require("loguix") var { __glob } = require("./global-variables") log.setup(__glob.LOGS) + const wlog = new LogType("Web") + wlog.step.init("start_server", "Démarrage du serveur Express JS") var app = require('../main'); var debug = require('debug')('neutral:server'); var http = require('http'); - +var config = require("./config") /** @@ -91,9 +93,10 @@ function onError(error) { */ function onListening() { + wlog.log("Serveur entrain d'écouter sur le port : " + server.address().port) var addr = server.address(); var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; wlog.step.end("start_server") -} +} diff --git a/main.js b/main.js index 6c45712..1ff6e70 100644 --- a/main.js +++ b/main.js @@ -4,9 +4,14 @@ const path = require('path'); const cookieParser = require('cookie-parser'); const app = express(); const fs = require("fs"); -const { __glob } = require('./bin/global-variables'); const log = require("loguix") +const { __glob } = require('./bin/global-variables'); +const users = require("./bin/users") +const {User} = require("./bin/users") + + + var wlog = log.getInstance("Web") @@ -16,7 +21,7 @@ setup() function getRouters() { - wlog.log("Récupération de " + fs.readdirSync(__glob.ROUTES).length + " routeurs") + wlog.log("Récupération de " + fs.readdirSync(__glob.ROUTES).length + " routeurs depuis : " + __glob.ROUTES) for(var route of fs.readdirSync(__glob.ROUTES)) { if(route == "index.js") { @@ -44,6 +49,7 @@ function setup() { app.use(express.static(path.join(__dirname, 'public'))); getRouters() + users.fetchUsers() // catch 404 and forward to error handler app.use(function(req, res, next) { diff --git a/package-lock.json b/package-lock.json index 010a3d0..8795ed3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,23 @@ { "name": "neutral", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "neutral", - "version": "0.1.0", + "version": "0.1.1", "license": "ISC", "dependencies": { "cookie-parser": "~1.4.4", + "crypto-js": "^4.2.0", "debug": "~2.6.9", "ejs": "~2.6.1", "express": "~4.16.1", "http-errors": "~1.6.3", "loguix": "1.4.1", - "nodemon": "^3.0.1" + "nodemon": "^3.0.1", + "uuid": "^9.0.1" } }, "node_modules/abbrev": { @@ -185,6 +187,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -839,6 +846,18 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -980,6 +999,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1468,6 +1492,11 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 78d03ef..3e4be88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neutral", - "version": "0.1.1", + "version": "0.1.2", "description": "Panel d'administration de Raphix", "main": "index.js", "scripts": { @@ -20,12 +20,14 @@ "keywords": [], "dependencies": { "cookie-parser": "~1.4.4", + "crypto-js": "^4.2.0", "debug": "~2.6.9", "ejs": "~2.6.1", "express": "~4.16.1", "http-errors": "~1.6.3", "loguix": "1.4.1", - "nodemon": "^3.0.1" + "nodemon": "^3.0.1", + "uuid": "^9.0.1" }, "author": "Raphix", "license": "ISC" diff --git a/public/javascripts/basics.js b/public/javascripts/basics.js index 003ed88..b3e8fe0 100644 --- a/public/javascripts/basics.js +++ b/public/javascripts/basics.js @@ -9,13 +9,14 @@ class InfoPop { constructor(name) { this.name = name this.element = getID(this.name) + this.element.innerHTML = " " this.element.style.fontSize = "14px" - + this.element.style.position = "sticky" } clear() { - this.element.innerHTML = "" + this.element.innerHTML = " " } err(text) { diff --git a/public/javascripts/loginscript.js b/public/javascripts/loginscript.js index 007f630..8a5c204 100644 --- a/public/javascripts/loginscript.js +++ b/public/javascripts/loginscript.js @@ -10,13 +10,57 @@ submit.addEventListener("click", () => { if(!username.value) { - loginInfo.err("Le nom d'utilisateur est nécéssaire pour se connecter !") + loginInfo.err("Un nom d'utilisateur est nécéssaire pour se connecter !") } else if(!password.value) { - loginInfo.err("Le mot de passe est nécéssaire pour se connecter !") + loginInfo.err("Un mot de passe est nécéssaire pour se connecter !") } else { - loginInfo.clear() + + login() } -}) \ No newline at end of file +}) + +password.addEventListener("keyup", (event) => { + if (event.key === "Enter") { + login() + } +}); + +function login() { + + loginInfo.clear() + + fetch('/login', { + method: 'POST', + redirect: 'follow', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + username: username.value, + password: password.value + }) + }) + .then(response => redirect(response)) + + async function redirect(response) { + + response = await response.text() + + if(response == "AUTH_FAILED") { + + loginInfo.err("Le nom d'utilisateur et le mot de passe sont incorrects.") + + } else if(response == "AUTH_SUCCESS") { + + window.location.href = "/" + + } + + + + } +} diff --git a/routes/index.js b/routes/index.js index b1c3f15..740a184 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,9 +1,17 @@ var express = require('express'); var router = express.Router(); +var auth = require("../bin/auth") /* GET home page. */ router.get('/', function(req, res, next) { - res.render('index'); + + if(!auth.check(req.cookies.token)) { + res.clearCookie('token') + res.redirect(302, "/login") +} else { + + res.render('index'); + } }); module.exports = router; diff --git a/routes/login.js b/routes/login.js index b44b1c5..a73480d 100644 --- a/routes/login.js +++ b/routes/login.js @@ -1,9 +1,59 @@ var express = require('express'); var router = express.Router(); +var auth = require("../bin/auth") /* GET home page. */ router.get('/', function(req, res, next) { - res.render('login'); + + if(auth.check(req.cookies.token)) { + + res.redirect(302, "/") + } else { + res.clearCookie('token') + res.render('login', {version: process.env.npm_package_version}); + } + + }); module.exports = router; + +router.post("/", (req, res) => { + const body = req.body + + const token = auth.login({ + username: body.username, + password: body.password + }) + + if(token == "AUTH_FAILED") { + + + res.status(403).send("AUTH_FAILED") + + + } else { + + res.cookie('token' , token, { maxAge: 900000000, httpOnly: true }) + res.status(200).send("AUTH_SUCCESS") + } + +}) + +router.get('/signout', function(req, res, next) { + + if(!auth.check(req.cookies.token)) { + + res.clearCookie('token') + res.redirect(302, "/") + + + } else { + + auth.signout(req.cookies.token) + res.clearCookie('token') + res.redirect(302, "/") + + } + +}); \ No newline at end of file diff --git a/views/index.ejs b/views/index.ejs index 22f80da..d9b4350 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -7,7 +7,7 @@

Neutral - Home

Welcome to Neutral

- + diff --git a/views/login.ejs b/views/login.ejs index a953ade..e022378 100644 --- a/views/login.ejs +++ b/views/login.ejs @@ -18,7 +18,10 @@

- +

Version : <%- version %>

+

Panel d'administration

+ Revenir sur raphix.fr +