Compare commits

...

4 Commits

Author SHA1 Message Date
1cf5b5a089 Update user display 2023-12-07 23:55:26 +01:00
dd6d7fc552 Update handler to ease implementation of REST API 2023-12-07 23:50:39 +01:00
dcb50849aa Set default body font 2023-12-07 12:36:47 +01:00
be62b85c52 Add user list and update user management 2023-12-07 12:33:59 +01:00
21 changed files with 323 additions and 137 deletions

View File

@ -9,7 +9,7 @@ loadDatabase();
const submitEvent = new EventEmitter(); const submitEvent = new EventEmitter();
await launchWeb(submitEvent); await launchWeb(submitEvent);
submitEvent.on("call", async (call) => { submitEvent.on("contacts/call", async (call) => {
let vote; let vote;
if (call.vote) { if (call.vote) {
console.log(`${call.phone} va voter`); console.log(`${call.phone} va voter`);
@ -23,7 +23,7 @@ submitEvent.on("call", async (call) => {
{ where: { phone: call.phone } }, { where: { phone: call.phone } },
); );
}); });
submitEvent.on("add", (request) => { submitEvent.on("contacts/add", (request) => {
console.log(request); console.log(request);
}); });

View File

@ -1,8 +0,0 @@
export default {
path: "/api/contacts/add",
requiresLogin: true,
type: "post",
async execute(request, response) {
global.events.submitEvent.emit("add", request.body);
},
}

View File

@ -1,12 +0,0 @@
import { permissionBits } from '../../../../utils/permissions.js';
export default {
path: "/api/contacts/call",
requiresLogin: true,
permissions: permissionBits.CALL,
type: "post",
async execute(request, response) {
global.events.submitEvent.emit("call", request.body);
response.redirect("/calls");
},
}

View File

@ -0,0 +1,22 @@
import { permissionBits } from "../../../../utils/permissions.js";
export default {
path: "/api/contacts(/*)?",
requiresLogin: true,
permissions: permissionBits.CALL,
type: "post",
async execute(request, response) {
const phone = request.originalUrl.split("/")[3];
const operation = request.originalUrl.split("/")[4];
switch(operation) {
case 'call':
global.events.submitEvent.emit("contacts/call", request.body);
response.redirect("/calls");
break
case 'add':
global.events.submitEvent.emit("contacts/add", request.body);
//response.redirect("/");
break
}
},
};

View File

@ -1,39 +0,0 @@
import { permissionBits } from "../../../../utils/permissions.js";
export default {
path: "/api/users(/*)?",
requiresLogin: true,
permissions: permissionBits.ADMIN,
type: "post",
async execute(request, response) {
const username = request.originalUrl.split("/")[3];
const operation = request.originalUrl.split("/")[4];
const args = request.body;
switch (operation) {
case "create":
if (
await global.database.users.findOne({
where: { username: username },
})
) return response.redirect("/dashboard/users/create?error=User already exists");
console.log(`Creating user ${username}`);
let permissions = Number(args.permissions);
if (isNaN(permissions)) {
permissions = 0;
for (const permissionBit of args.permissions) {
permissions += Number(permissionBit);
}
}
if(!(permissions & permissionBits.DEFAULT)) permissions ^= permissionBits.DEFAULT;
await global.database.users.create({
username: username,
password: args.password,
permissions: permissions,
});
break;
case "delete":
break;
}
},
};

View File

@ -0,0 +1,19 @@
import { permissionBits } from "../../../../utils/permissions.js";
export default {
path: "/api/users/:username",
requiresLogin: true,
permissions: permissionBits.ADMIN,
type: "get",
async execute(request, response) {
const { username } = request.params;
const user = await global.database.users.findOne({
where: { username: username },
});
if(!user || user == 0) return response.status(404).send({ message: "User does not exist" });
response.send({
username: user.username,
permissions: user.permissions,
});
},
};

View File

@ -0,0 +1,32 @@
import { permissionBits } from "../../../../utils/permissions.js";
export default {
path: "/api/users/:username",
requiresLogin: true,
permissions: permissionBits.ADMIN,
type: "post",
async execute(request, response) {
const { username } = request.params;
let { permissions, password } = request.body;
if (
await global.database.users.findOne({
where: { username: username },
})
) return response.status(412).send({ message: "User already exists" });
if (!(permissions & permissionBits.DEFAULT))
permissions ^= permissionBits.DEFAULT;
const userParams = {
username: username,
password: password,
permissions: permissions,
}
console.log(`Creating user ${username}`);
await global.database.users.create(userParams);
response.status(201).send({
username: username,
permissions: permissions,
});
},
};

View File

@ -1,4 +1,6 @@
import { permissionBits } from '../../../../utils/permissions.js'; import { permissionBits } from '../../../../utils/permissions.js';
import { readFile } from 'fs/promises';
import { navbar } from '../../../../utils/navbar.js';
export default { export default {
path: "/dashboard/users", path: "/dashboard/users",
@ -6,6 +8,9 @@ export default {
permissions: permissionBits.ADMIN, permissions: permissionBits.ADMIN,
type: "get", type: "get",
async execute(request, response) { async execute(request, response) {
response.redirect("/dashboard/users/list"); const html = await readFile(`${process.env.WWW}/dashboard/users/index.html`);
return await response.send(html.toString()
.replace('<NAVBAR>', navbar(request.session))
);
}, },
} }

View File

@ -0,0 +1,44 @@
import { navbar } from '../../../../utils/navbar.js';
import { readFile } from 'fs/promises';
import { permissionBits, checkPermissions } from '../../../../utils/permissions.js';
export default {
path: "/dashboard/users/list",
requiresLogin: true,
permissions: permissionBits.ADMIN,
type: "get",
async execute(request, response) {
const userList = await global.database.users.findAll();
const userTable = genUserTable(userList);
const html = await readFile(`${process.env.WWW}/dashboard/users/list.html`);
return await response.send(html.toString()
.replace('<NAVBAR>', navbar(request.session))
.replace('<USERTABLE>', userTable)
);
},
}
function genUserTable(users) {
let res = `
<table>
<tr>
<th>Identifiant</th>
<th>Permissions</th>
<th>Action</th>
</tr>`;
for(const user of users) {
const userPermDict = checkPermissions(user.permissions);
let userPerms = [];
for (const [key, value] of Object.entries(userPermDict)) {
if(value && key != "default") userPerms.push(key);
};
res += `
<tr>
<td><a href='./${user.username}'>${user.username}</a></td>
<td>${userPerms.join(", ")}</td>
<!--<td><button>Supprimer</button></td>-->
</tr>`;
}
res += `</table>`;
return res
}

View File

@ -0,0 +1,42 @@
import { permissionBits, checkPermissions } from '../../../../utils/permissions.js';
import { readFile } from 'fs/promises';
import { navbar } from '../../../../utils/navbar.js';
export default {
path: "/dashboard/users/:username",
requiresLogin: true,
permissions: permissionBits.ADMIN,
type: "get",
async execute(request, response) {
const { username } = request.params;
const user = await global.database.users.findOne({ where: { username: username } });
if(!user) return response.redirect('/dashboard/users');
const html = await readFile(`${process.env.WWW}/dashboard/users/info.html`);
response.send(html.toString()
.replace('<NAVBAR>', navbar(request.session))
.replace('<USERINFO>', getUserHTML(user))
);
},
}
function getUserHTML(user) {
const userPermDict = checkPermissions(user.permissions);
let userPerms = [];
for (const [key, value] of Object.entries(userPermDict)) {
if(value && key != "default") userPerms.push(key);
};
let res = `
<table>
<tr>
<th>Identifiant</th>
<th>Permissions</th>
<th>Action</th>
</tr>
<tr>
<td>${user.username}</td>
<td>${userPerms.join(", ")}</td>
<td><button>Supprimer</button></td>
</tr>
</table>`;
return res;
}

View File

@ -4,10 +4,10 @@ export default {
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);
global.database.users.findOne({ where: { username: username } }).then(function (user) { global.database.users.findOne({ where: { username: username } }).then(async function (user) {
if (!user) { if (!user) {
response.redirect('/login'); response.redirect('/login');
} else if (!user.validPassword(password)) { } else if (!await user.validPassword(password)) {
response.redirect('/login'); response.redirect('/login');
} else { } else {
request.session.user = user.dataValues; request.session.user = user.dataValues;

View File

@ -1,29 +1,89 @@
import fs from 'fs'; import fs from 'fs';
const pagesPath = "./src/html/pages/"; const pagesPath = "./src/html/pages/";
async function deployHandler() { async function genHandler() {
global.handler = { const handler = {
get: {}, get: [],
post: {}, post: [],
path: [],
delete: [],
}; };
let numberOfPages = 0; let numberOfPages = 0;
const PagesCategories = fs const endpointCategories = fs
.readdirSync(pagesPath) .readdirSync(pagesPath)
.filter((file) => !file.includes(".")); .filter((file) => !file.includes("."));
for (const category of PagesCategories) { for (const category of endpointCategories) {
const pageFiles = fs const endpointFiles = fs
.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 endpointFiles) {
const { default: page } = await import(`.${pagesPath}${category}/${file}`); const { default: endpoint } = await import(`.${pagesPath}${category}/${file}`);
global.handler[page.type][page.path] = page; handler[endpoint.type].push(endpoint);
console.log( console.log(
`\x1b[32mChargement de POST: ${page.path} !\x1b[0m`, `\x1b[32mChargement de POST: ${endpoint.path} !\x1b[0m`,
); );
numberOfPages++; numberOfPages++;
console.log(numberOfPages + " pages chargées ! "); console.log(numberOfPages + " endpoints chargés ! ");
} }
} }
return handler;
}
async function deployHandler(app) {
const handler = await genHandler();
for(const endpoint of handler.get) {
app.get(endpoint.path, async (request, response) => {
const [path, args] = parseURL(request.originalUrl);
console.log("GET: " + path);
if (endpoint.requiresLogin && !request.session.user) {
return response.redirect("/login");
}
if (endpoint.permissions) {
if((endpoint.permissions & request.session.user.permissions) == 0) {
return response.status(403).send("Vous n'avez pas la permission d'effectuer cette action !");
}
}
return await endpoint.execute(
request,
response,
);
});
}
for(const endpoint of handler.post) {
app.post(endpoint.path, async (request, response) => {
console.log("POST: " + request.originalUrl);
if (endpoint.requiresLogin && !request.session.user) {
return response.redirect("/login");
}
if (endpoint.permissions) {
if((endpoint.permissions & request.session.user.permissions) == 0) {
return response.status(403).send("Vous n'avez pas la permission d'effectuer cette action !");
}
}
return await endpoint.execute(
request,
response,
);
});
}
}
function parseURL(URL) {
const spURL = URL.split("?");
const path = spURL[0];
if(!spURL[1]) return [path, {}];
const args = {};
for (let i = 0; i < spURL[1].split("=").length; i+=2) {
const key = spURL[1].split("=")[i];
const val = spURL[1].split("=")[i+1];
args[key] = val;
}
return [path, args];
} }
export { deployHandler }; export { deployHandler };

View File

@ -26,6 +26,7 @@ function checkPermissions(userPermissionBits) {
if(userPermissionBits & permissionBits.ADMIN) { if(userPermissionBits & permissionBits.ADMIN) {
permissions.admin = true; permissions.admin = true;
} }
return permissions;
} }
/* /*
let userPermissionBits = permissionBits.DEFAULT; let userPermissionBits = permissionBits.DEFAULT;

57
web.js
View File

@ -19,48 +19,7 @@ async 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`));
await deployHandler(); await deployHandler(app);
app.post("*", async (request, response) => {
console.log("POST: " + request.originalUrl);
let handled = global.handler.post[request.originalUrl];
if(!handled) for(const path of Object.keys(global.handler.post)) {
if(new RegExp(path).test(request.originalUrl)) handled = global.handler.post[path];
}
if (!handled) return console.log(request.originalUrl);
if (handled.requiresLogin && !request.session.user) {
return response.redirect("/login");
}
if (handled.permissions) {
if((handled.permissions & request.session.user.permissions) == 0) return response.status(403).send("Vous n'avez pas la permission d'effectuer cette action !");
}
return await handled.execute(
request,
response,
);
});
app.get("*", async (request, response) => {
const [path, args] = parseURL(request.originalUrl);
//console.log(parseURL(request.originalUrl));
console.log(`GET: ${path}${args ? "?" + args : ""}`);
let handled = global.handler.get[path];
if(!handled) for(const path of Object.keys(global.handler.get)) {
if(new RegExp(path).test(request.originalUrl)) handled = global.handler.get[path];
}
if (!handled) return;
if (handled.requiresLogin && !request.session.user) {
return response.redirect("/login");
}
if (handled.permissions) {
if((handled.permissions & request.session.user.permissions) == 0) return response.status(403).send("Vous n'avez pas la permission d'accéder cette page !");
}
return await handled.execute(request, response, args);
});
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
app.listen(PORT, () => { app.listen(PORT, () => {
@ -68,18 +27,4 @@ async function launchWeb() {
}); });
} }
function parseURL(URL) {
const spURL = URL.split("?");
const path = spURL[0];
if(!spURL[1]) return [path, {}];
const args = {};
for (let i = 0; i < spURL[1].split("=").length; i+=2) {
const key = spURL[1].split("=")[i];
const val = spURL[1].split("=")[i+1];
args[key] = val;
}
return [path, args];
}
export { launchWeb }; export { launchWeb };

View File

@ -1,3 +1,5 @@
body {font-family: Arial, Helvetica, sans-serif;}
ul { ul {
list-style-type: none; list-style-type: none;
margin: 0; margin: 0;

View File

@ -9,7 +9,7 @@
<NAVBAR> <NAVBAR>
<h1>Dashboard</h1> <h1>Dashboard</h1>
<p><a href='/calls'>Phoning</a></p> <p><a href='/calls'>Phoning</a></p>
<p><a href='/dashboard/users/create'>Ajouter un utilisateur</a></p> <p><a href='/dashboard/users'>Gestion des utilisateurs</a></p>
<p><a href='/logout'>Déconnexion</a></p> <p><a href='/logout'>Déconnexion</a></p>
</body> </body>
</html> </html>

View File

@ -2,6 +2,7 @@
<html> <html>
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Utilisateurs</title>
<style> <style>
body { body {
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
@ -77,15 +78,36 @@ a {
} }
</style> </style>
<script> <script>
function setAction(form) { function createUserFromForm() {
form.action = `/api/users/${form.username.value}/create`; const form = document.getElementById("userCreateForm");
return true; let perms = 1;
for(const node of form.querySelectorAll('input[name="permissions"]')) {
perms += node.checked ? Number(node.value) : 0;
}
console.log(perms)
fetch(`/api/users/${form.username.value}`, {
method: "POST",
body: JSON.stringify({
username: form.username.value,
permissions: perms,
password: form.password.value,
}),
headers: {
"Content-type": "application/json; charset=UTF-8"
}
})
.then(async (response) => {
const res = await response.json();
console.log(res);
if(!response.ok) return alert(res.message);
window.location = `/dashboard/users/${res.username}`;
});
} }
</script> </script>
</head> </head>
<body> <body>
<NAVBAR> <NAVBAR>
<form action="/api/users/:username/create" method="post" onsubmit="return setAction(this)"> <form action="javascript:createUserFromForm()" id="userCreateForm">
<div class="container"> <div class="container">
<h1>Création de compte</h1> <h1>Création de compte</h1>
<p>Formulaire de création de compte.</p> <p>Formulaire de création de compte.</p>

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>Utilisateurs</title>
</head>
<body>
<NAVBAR>
<h1>Gestion des utilisateurs</h1>
<p><a href='/dashboard/users/list'>Liste des utilisateurs</a></p>
<p><a href='/dashboard/users/create'>Ajouter un utilisateur</a></p>
<p><a href='/dashboard'>Retour</a></p>
</body>
</html>

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Utilisateurs</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
table, th, td {
border:1px solid black;
}
</style>
</head>
<body>
<NAVBAR>
<USERINFO>
<a href='./'><p>Retour</p></a>
</body>
</html>

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Utilisateurs</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
table, th, td {
border:1px solid black;
}
</style>
</head>
<body>
<NAVBAR>
<USERTABLE>
<a href='./'><p>Retour</p></a>
</body>
</html>