Added login system and updated interface

This commit is contained in:
Ninjdai 2023-12-07 01:28:39 +01:00
parent dd32138229
commit 677d1d92d2
24 changed files with 381 additions and 103 deletions

View File

@ -1,43 +1,13 @@
const { launchWeb } = require("./web.js"); import { launchWeb } from './web.js';
const { EventEmitter } = require("events"); import { EventEmitter } from 'events';
const Sequelize = require("sequelize"); import { loadDatabase } from './utils/database.js';
require('dotenv').config() import dotenv from 'dotenv';
dotenv.config();
const sequelize = new Sequelize("database", "user", "password", { loadDatabase();
host: "localhost",
dialect: "sqlite",
logging: false,
storage: "database.sqlite",
});
const contactsDB = sequelize.define("contacts", {
phone: {
type: Sequelize.STRING,
primaryKey: true,
},
firstName: {
type: Sequelize.STRING,
},
lastName: {
type: Sequelize.STRING,
},
called: {
type: Sequelize.INTEGER, // 0: not called - 1: called - 2: ongoing call (- 3: no response ?)
defaultValue: false,
},
vote: {
type: Sequelize.BOOLEAN,
defaultValue: false,
},
});
contactsDB.sync();
global.database = {};
global.database.contacts = contactsDB;
const submitEvent = new EventEmitter(); const submitEvent = new EventEmitter();
launchWeb(submitEvent); await launchWeb(submitEvent);
submitEvent.on("call", async (call) => { submitEvent.on("call", async (call) => {
let vote; let vote;

View File

@ -6,9 +6,14 @@
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"type": "module",
"author": "Ninjdai", "author": "Ninjdai",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@adminjs/express": "^6.1.0",
"@adminjs/sequelize": "^4.1.1",
"adminjs": "^7.5.0",
"bcrypt": "^5.1.1",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.17.3", "express-session": "^1.17.3",

View File

@ -10,4 +10,4 @@ async function getRandomUser() {
}; };
} }
module.exports = { getRandomUser }; export { getRandomUser };

View File

@ -1,9 +1,9 @@
const { navbar } = require('../../utils/navbar.js'); import { navbar } from '../../utils/navbar.js';
function generateCallHTML(phoneNumber, name, callcount, session) { function generateCallHTML(phoneNumber, name, callcount, session) {
const head = `<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><link rel="stylesheet" type="text/css" href="/assets/css/form.css" /><title>AutoCallMenu</title></head>`; const head = `<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><link rel="stylesheet" type="text/css" href="/assets/css/form.css" /><title>AutoCallMenu</title></head>`;
const form = ` const form = `
<form action="/inputs/call" method="post"> <form action="/api/contacts/call" method="post">
<input type="hidden" name="phone" value="${phoneNumber}"> <input type="hidden" name="phone" value="${phoneNumber}">
<label class="switch"> <label class="switch">
<input type="checkbox" name="vote"> <input type="checkbox" name="vote">
@ -18,4 +18,4 @@ function generateCallHTML(phoneNumber, name, callcount, session) {
return `<!DOCTYPE html><html>${head}${body}</html>`; return `<!DOCTYPE html><html>${head}${body}</html>`;
} }
module.exports = { generateCallHTML }; export { generateCallHTML };

View File

@ -1,5 +1,5 @@
module.exports = { export default {
path: "/inputs/add", path: "/api/contacts/add",
requiresLogin: true, requiresLogin: true,
type: "post", type: "post",
async execute(request, response) { async execute(request, response) {

View File

@ -1,6 +1,9 @@
module.exports = { import { permissionBits } from '../../../../utils/permissions.js';
path: "/inputs/call",
export default {
path: "/api/contacts/call",
requiresLogin: true, requiresLogin: true,
permissions: [permissionBits.CALL],
type: "post", type: "post",
async execute(request, response) { async execute(request, response) {
global.events.submitEvent.emit("call", request.body); global.events.submitEvent.emit("call", request.body);

View File

@ -0,0 +1,15 @@
export default {
path: "/api/users/*",
requiresLogin: true,
type: "post",
async execute(request, response) {
const path = request.originalUrl.split("/")[3];
const args = request.body;
switch(path) {
case 'create':
break;
case 'delete':
break;
}
},
}

View File

@ -0,0 +1,14 @@
import { readFile } from 'fs/promises';
import { navbar } from '../../../../utils/navbar.js';
import { permissionBits } from '../../../../utils/permissions.js';
export default {
path: "/dashboard",
requiresLogin: true,
permissions: permissionBits.ADMIN,
type: "get",
async execute(request, response) {
const res = await readFile(`${process.env.WWW}/dashboard/index.html`, "utf8")
response.send(res.replaceAll("<NAVBAR>", navbar(request.session)));
},
}

View File

@ -0,0 +1,11 @@
import { permissionBits } from '../../../../utils/permissions.js';
export default {
path: "/dashboard/users",
requiresLogin: true,
permissions: permissionBits.ADMIN,
type: "get",
async execute(request, response) {
response.redirect("/dashboard/users/list");
},
}

View File

@ -0,0 +1,14 @@
import { readFile } from 'fs/promises';
import { navbar } from '../../../../utils/navbar.js';
import { permissionBits } from '../../../../utils/permissions.js';
export default {
path: "/dashboard/users/create",
requiresLogin: true,
permissions: permissionBits.ADMIN,
type: "get",
async execute(request, response) {
const res = await readFile(`${process.env.WWW}/dashboard/users/create.html`, "utf8")
response.send(res.replaceAll("<NAVBAR>", navbar(request.session)));
},
}

View File

@ -1,10 +1,12 @@
const { generateCallHTML } = require("../../../../src/html/callHTML.js"); import { generateCallHTML } from "../../../../src/html/callHTML.js";
const { getRandomUser } = require('../../../../src/database/getUser.js'); import { getRandomUser } from '../../../../src/database/getUser.js';
const { navbar } = require('../../../../utils/navbar.js'); import { navbar } from '../../../../utils/navbar.js';
import { permissionBits } from "../../../../utils/permissions.js";
module.exports = { export default {
path: "/calls", path: "/calls",
requiresLogin: true, requiresLogin: true,
permissions: [permissionBits.CALL],
type: "get", type: "get",
async execute(request, response) { async execute(request, response) {
const res = await generateCallResponse(request.session); const res = await generateCallResponse(request.session);

View File

@ -1,7 +1,7 @@
const { readFile } = require('fs').promises; import { readFile } from 'fs/promises';
const { navbar } = require('../../../../utils/navbar.js'); import { navbar } from '../../../../utils/navbar.js';
module.exports = { export default {
path: "/", path: "/",
type: "get", type: "get",
async execute(request, response) { async execute(request, response) {

View File

@ -1,7 +1,7 @@
const { readFile } = require('fs').promises; import { readFile } from 'fs/promises';
const { navbar } = require('../../../../utils/navbar.js'); import { navbar } from '../../../../utils/navbar.js';
module.exports = { export default {
path: "/login", path: "/login",
type: "get", type: "get",
async execute(request, response, args) { async execute(request, response, args) {

View File

@ -1,16 +1,18 @@
const { readFile } = require('fs').promises; export default {
module.exports = {
path: "/login", path: "/login",
type: "post", type: "post",
async execute(request, response) { async execute(request, response) {
const { username, password } = request.body; const { username, password } = request.body;
console.log(request.body); console.log(request.body);
if (username == "ninjdai" && password == "azerty") { global.database.users.findOne({ where: { username: username } }).then(function (user) {
request.session.user = { username }; if (!user) {
response.redirect("/"); response.redirect('/login');
} else { } else if (!user.validPassword(password)) {
response.redirect("/login?fail=true"); response.redirect('/login');
} } else {
request.session.user = user.dataValues;
response.redirect('/');
}
});
}, },
} }

View File

@ -1,4 +1,4 @@
module.exports = { export default {
path: "/logout", path: "/logout",
type: "get", type: "get",
async execute(request, response) { async execute(request, response) {

69
utils/database.js Normal file
View File

@ -0,0 +1,69 @@
import Sequelize from "sequelize";
import bcrypt from 'bcrypt';
import { permissionBits } from "./permissions.js";
function loadDatabase() {
const sequelize = new Sequelize("database", "user", "password", {
host: "localhost",
dialect: "sqlite",
logging: false,
storage: "database.sqlite",
});
const contactsDB = sequelize.define("contacts", {
phone: {
type: Sequelize.STRING,
primaryKey: true,
},
firstName: {
type: Sequelize.STRING,
},
lastName: {
type: Sequelize.STRING,
},
called: {
type: Sequelize.INTEGER, // 0: not called - 1: called - 2: ongoing call (- 3: no response ?)
defaultValue: false,
},
vote: {
type: Sequelize.BOOLEAN,
defaultValue: false,
},
});
const userDB = sequelize.define("users", {
username: {
type: Sequelize.STRING,
primaryKey: true,
},
password: {
type: Sequelize.STRING,
},
permissions: {
type: Sequelize.INTEGER,
defaultValue: permissionBits.DEFAULT,
},
}, {
hooks: {
beforeCreate: async function(user) {
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(user.password, salt);
},
},
});
userDB.prototype.validPassword = async function(password) {
return await bcrypt.compare(password, this.password);
}
contactsDB.sync();
userDB.sync();
global.database = {};
global.database.contacts = contactsDB;
global.database.users = userDB;
//global.database.users.create({username: "admin", password: "admin", permissions: permissionBits.DEFAULT | permissionBits.CALL | permissionBits.ADMIN})
//global.database.users.create({username: "test", password: "test", permissions: permissionBits.DEFAULT | permissionBits.CALL})
}
export { loadDatabase };

View File

@ -1,7 +1,7 @@
const fs = require('fs'); import fs from 'fs';
const pagesPath = "./src/html/pages/"; const pagesPath = "./src/html/pages/";
function deployHandler() { async function deployHandler() {
global.handler = { global.handler = {
get: {}, get: {},
post: {}, post: {},
@ -15,7 +15,7 @@ function deployHandler() {
.readdirSync(`${pagesPath}${category}`) .readdirSync(`${pagesPath}${category}`)
.filter((file) => file.endsWith(".js")); .filter((file) => file.endsWith(".js"));
for (const file of pageFiles) { for (const file of pageFiles) {
const page = require(`.${pagesPath}${category}/${file}`); const { default: page } = await import(`.${pagesPath}${category}/${file}`);
global.handler[page.type][page.path] = page; global.handler[page.type][page.path] = page;
console.log( console.log(
`\x1b[32mChargement de POST: ${page.path} !\x1b[0m`, `\x1b[32mChargement de POST: ${page.path} !\x1b[0m`,
@ -26,4 +26,4 @@ function deployHandler() {
} }
} }
module.exports = { deployHandler }; export { deployHandler };

View File

@ -1,18 +1,17 @@
module.exports = { function navbar(session) {
navbar(session) { let logField;
let logField; if(session.user) {
if(session.user) { logField = `<li style="float:right"><a class="active" href="/dashboard">${session.user.username}</a></li>`
logField = `<li style="float:right"><a class="active" href="/logout">${session.user.username} ✖ </a></li>` } else {
} else { logField = `<li style="float:right"><a class="active" href="/login">Login</a></li>`;
logField = `<li style="float:right"><a class="active" href="/login">Login</a></li>`;
}
return `
<link rel="stylesheet" type="text/css" href="/assets/css/navbar.css" />
<ul>
<li><a href="/">Accueil</a></li>
<li><a href="/calls">Phoning</a></li>
<li><a href="#nope">Nope</a></li>
${logField}
</ul>`;
} }
return `
<link rel="stylesheet" type="text/css" href="/assets/css/navbar.css" />
<ul>
<li><a href="/">Accueil</a></li>
<li><a href="/calls">Phoning</a></li>
<li><a href="#nope">Nope</a></li>
${logField}
</ul>`;
} }
export { navbar };

37
utils/permissions.js Normal file
View File

@ -0,0 +1,37 @@
const permissionBits = {
DEFAULT: 1, // 0001
CALL: 2, // 0010
UNUSED: 4, // 0100
ADMIN: 8, // 1000
}
function checkPermissions(userPermissionBits) {
// Default if you have no permissions
let permissions = {
default: false,
call: false,
unused: false,
admin: false,
}
if(userPermissionBits & permissionBits.DEFAULT) {
permissions.default = true;
}
if(userPermissionBits & permissionBits.CALL) {
permissions.call = true;
}
if(userPermissionBits & permissionBits.UNUSED) {
permissions.unused = true;
}
if(userPermissionBits & permissionBits.ADMIN) {
permissions.admin = true;
}
}
/*
let userPermissionBits = permissionBits.DEFAULT;
userPermissionBits |= permissionBits.CALL; // add permission
userPermissionBits ^= permissionBits.ADMIN; // toggle permission
userPermissionBits &= (~permissionBits.CALL); // remove permission
*/
export { permissionBits, checkPermissions };

38
web.js
View File

@ -1,10 +1,9 @@
const express = require("express"); import express from "express";
const session = require("express-session"); import session from "express-session";
const { readFile } = require("fs").promises; import { deployHandler } from "./utils/handler.js";
const { deployHandler } = require("./utils/handler.js"); import favicon from "serve-favicon";
const favicon = require("serve-favicon");
function launchWeb() { async function launchWeb() {
const app = express(); const app = express();
app.use(express.json()); // Used to parse JSON bodies app.use(express.json()); // Used to parse JSON bodies
@ -20,14 +19,26 @@ function launchWeb() {
app.use("/assets", express.static(`${process.env.WWW}/assets`)); app.use("/assets", express.static(`${process.env.WWW}/assets`));
app.use(favicon(`${process.env.WWW}/assets/images/favicon.ico`)); app.use(favicon(`${process.env.WWW}/assets/images/favicon.ico`));
deployHandler(); await deployHandler();
app.post("*", async (request, response) => { app.post("*", async (request, response) => {
console.log("POST: " + request.originalUrl); console.log("POST: " + request.originalUrl);
if (!global.handler.post[request.originalUrl]) return; if (!global.handler.post[request.originalUrl]) return;
if (global.handler.post[request.originalUrl].requiresLogin && !request.session.user) { if (
global.handler.post[request.originalUrl].requiresLogin &&
!request.session.user
) {
return response.redirect("/login"); return response.redirect("/login");
} }
if (
global.handler.post[request.originalUrl].permissions &&
global.handler.post[request.originalUrl].permissions.reduce(
(a, b) => a + b,
) &
(request.session.user.permissions == 0)
) {
return response.status(403);
}
return await global.handler.post[request.originalUrl].execute( return await global.handler.post[request.originalUrl].execute(
request, request,
response, response,
@ -35,20 +46,15 @@ function launchWeb() {
}); });
app.get("*", async (request, response) => { app.get("*", async (request, response) => {
const [path, args] = parseURL(request.originalUrl); const [path, args] = parseURL(request.originalUrl);
//console.log(parseURL(request.originalUrl)); //console.log(parseURL(request.originalUrl));
console.log(`GET: ${path}${args ? "?"+args : ""}`); console.log(`GET: ${path}${args ? "?" + args : ""}`);
if (!global.handler.get[path]) return; if (!global.handler.get[path]) return;
if (global.handler.get[path].requiresLogin && !request.session.user) { if (global.handler.get[path].requiresLogin && !request.session.user) {
return response.redirect("/login"); return response.redirect("/login");
} }
return await global.handler.get[path].execute( return await global.handler.get[path].execute(request, response, args);
request,
response,
args,
);
}); });
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
@ -64,4 +70,4 @@ function parseURL(URL) {
return [path, args]; return [path, args];
} }
module.exports = { launchWeb }; export { launchWeb };

15
www/dashboard/index.html Normal file
View File

@ -0,0 +1,15 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>AutoCallMenu</title>
</head>
<body>
<NAVBAR>
<h1>Dashboard</h1>
<p><a href='/calls'>Phoning</a></p>
<p><a href='/dashboard/users/create'>Ajouter un utilisateur</a></p>
<p><a href='/logout'>Déconnexion</a></p>
</body>
</html>

View File

@ -0,0 +1,116 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: Arial, Helvetica, sans-serif;
background-color: black;
}
* {
box-sizing: border-box;
}
/* Add padding to containers */
.container {
padding: 16px;
background-color: white;
display: block;
position: relative;
padding-left: 35px;
margin-bottom: 12px;
cursor: pointer;
font-size: 22px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* Full-width input fields */
input[type=text], input[type=password] {
width: 100%;
padding: 15px;
margin: 5px 0 22px 0;
display: inline-block;
border: none;
background: #f1f1f1;
}
input[type=text]:focus, input[type=password]:focus {
background-color: #ddd;
outline: none;
}
/* Overwrite default styles of hr */
hr {
border: 1px solid #f1f1f1;
margin-bottom: 25px;
}
/* Set a style for the submit button */
.registerbtn {
background-color: #04AA6D;
color: white;
padding: 16px 20px;
margin: 8px 0;
border: none;
cursor: pointer;
width: 100%;
opacity: 0.9;
}
.registerbtn:hover {
opacity: 1;
}
/* Add a blue text color to links */
a {
color: dodgerblue;
}
/* Set a grey background color and center the text of the "sign in" section */
.signin {
background-color: #f1f1f1;
text-align: center;
}
</style>
</head>
<body>
<NAVBAR>
<form action="/api/users/create" method="post">
<div class="container">
<h1>Création de compte</h1>
<p>Formulaire de création de compte.</p>
<hr>
<label for="email"><b>Nom d'utilisateur</b></label>
<input type="text" placeholder="Entrez le nom d'utilisateur" name="email" id="email" required>
<label for="psw"><b>Mot de passe</b></label>
<input type="password" placeholder="Mot de passe" name="password" id="psw" required>
<label for="psw-repeat"><b>Mot de passe (confirmation)</b></label>
<input type="password" placeholder="Répétez le mot de passe" name="password-repeat" id="psw-repeat" required>
<h4>Permissions</h4>
<label class="container">Appels
<input type="checkbox" name="permissions_calls">
<span class="checkmark"></span>
</label>
<!--<label class="container">Unused
<input type="checkbox">
<span class="checkmark"></span>
</label>-->
<label class="container">Admin
<input type="checkbox" name="permissions_admin">
<span class="checkmark"></span>
</label>
<hr>
<button type="submit" class="registerbtn">Register</button>
</div>
</form>
</body>
</html>

View File

@ -4,7 +4,7 @@
<title></title> <title></title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/assets/css/login.css" rel="stylesheet"> <link href="/assets/css/forms/login.css" rel="stylesheet">
</head> </head>
<body> <body>
<NAVBAR> <NAVBAR>