Version 0.1.2 - Ajout du systême d'authentification

This commit is contained in:
CICD - Pipeline 2023-11-02 11:09:47 +01:00
parent ea03e49887
commit 132dae4809
16 changed files with 620 additions and 21 deletions

2
.gitignore vendored
View File

@ -133,3 +133,5 @@ dist
#data
data
data/*
private

91
bin/auth.js Normal file
View File

@ -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
}
}

37
bin/config.js Normal file
View File

@ -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))
}
}

View File

@ -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"
};

29
bin/keygen.js Normal file
View File

@ -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;
}

291
bin/users.js Normal file
View File

@ -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))
}
}

View File

@ -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,6 +93,7 @@ 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

10
main.js
View File

@ -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) {

35
package-lock.json generated
View File

@ -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",

View File

@ -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"

View File

@ -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 = "&nbsp"
}
err(text) {

View File

@ -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()
}
})
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 = "/"
}
}
}

View File

@ -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;

View File

@ -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, "/")
}
});

View File

@ -7,7 +7,7 @@
<body>
<h1>Neutral - Home</h1>
<p>Welcome to Neutral</p>
<a href="/login/signout"><button class="btn red"><span><i class="fa fa-sign-out"></i> Déconnexion</span></button></a>
<script defer="" src="https://use.fontawesome.com/releases/v6.4.2/js/all.js" crossorigin="anonymous"></script>
</body>
</html>

View File

@ -18,7 +18,10 @@
</div>
<p id="login-info"></p>
<button id="submit" class="btn green m-align LOG_btn"><span><i class="fa-solid fa-right-to-bracket"></i> Connexion</span></button>
</div>
<p style="font-size: 12px;">Version : <%- version %></p>
<p style="font-size: 12px;">Panel d'administration</p>
<a href="https://raphix.fr" style="font-size: 12px;">Revenir sur raphix.fr</a>
</div>
<script defer="" src="https://use.fontawesome.com/releases/v6.4.2/js/all.js" crossorigin="anonymous"></script>
<script src="/javascripts/basics.js"></script>