diff --git a/.gitignore b/.gitignore index 8195efd..da0b00d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ package-lock.json *.js *.tsbuildinfo *.d.ts +*.db diff --git a/package.json b/package.json index abda9a9..85171a3 100644 --- a/package.json +++ b/package.json @@ -7,16 +7,17 @@ "start": "tsx src/server.ts" }, "devDependencies": { + "@types/bun": "^1.0.4", "tsx": "^4.7.0", - "typescript": "^5.3.3", - "@types/bun": "^1.0.4" + "typescript": "^5.3.3" }, "dependencies": { "@fastify/cors": "^9.0.0", "@fastify/type-provider-typebox": "^4.0.0", "@sinclair/typebox": "^0.32.9", - "fastify": "^4.25.2", + "fastify": "^4.27.0", "nanoid": "^5.0.4", + "sqlite3": "^5.1.7", "zeromq": "6.0.0-beta.19" } } diff --git a/src/database.ts b/src/database.ts new file mode 100644 index 0000000..c8b3d48 --- /dev/null +++ b/src/database.ts @@ -0,0 +1,328 @@ +import fs from "fs"; +import sqlite3 from "sqlite3"; + +const dbDirectory = "./src/db"; +const dbFilePath = `${dbDirectory}/database.db`; + +/* Fonction pour exécuter une requête sur la base de données */ + +/* Fonction pour exécuter une requête de modification de la base de données (INSERT, UPDATE, DELETE) */ +export function runDB( + db: sqlite3.Database, + query: string, + params: any[] +): Promise { + return new Promise((resolve, reject) => { + db.run(query, params, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + +/* Fonction pour récupérer plusieurs lignes de la base de données */ +export function allDB( + db: sqlite3.Database, + query: string +): Promise { + return new Promise((resolve, reject) => { + db.all(query, (err, rows) => { + if (err) { + reject(err); + } else { + resolve(rows); + } + }); + }); +} + +/* Fonction pour récupérer une seule ligne de la base de données */ +export function getDB( + db: sqlite3.Database, + query: string, + params: any[] +): Promise { + return new Promise((resolve, reject) => { + db.get(query, params, (err, row: any) => { + if (err) { + reject(err); + } else { + resolve(row); + } + }); + }); +} + +/* Fonctions pour la gestion de la base de données */ + +/* Créer le répertoire db s'il n'existe pas */ +export function createDbDirectory() { + if (!fs.existsSync(dbDirectory)) { + fs.mkdirSync(dbDirectory); + } +} + +/* Ouvrir la base de données */ +export function openDatabase() { + console.log("Ouverture de la connexion à la base de données."); + return new sqlite3.Database( + dbFilePath, + sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, + (err: Error | null) => { + if (err) console.error(err.message); + } + ); +} + +/* Fermer la base de données */ +export function closeDatabase(db: sqlite3.Database) { + db.close((err) => { + if (err) { + console.error(err.message); + } + console.log("Fermeture de la connexion à la base de données."); + }); +} + +/* Create all the tables in the database */ +export function createTables(db: sqlite3.Database) { + createRegisteredUserTable(db); + createLanguageTable(db); + createWorkTable(db); +} + +/////////////////////////// Gestion des utilisateurs /////////////////////////// +// CREATE TABLE registered_user ( +// id_user SERIAL PRIMARY KEY, +// login VARCHAR(64) NOT NULL, +// password VARCHAR(72) NOT NULL, +// permissions INT NOT NULL, +// UNIQUE (login) +// ); + +/* Créer la table registered_user dans la base de données */ +export function createRegisteredUserTable(db: sqlite3.Database): Promise { + const tableRegisteredUser = `CREATE TABLE IF NOT EXISTS registered_user (id_user INTEGER PRIMARY KEY AUTOINCREMENT, login TEXT NOT NULL, password TEXT NOT NULL, permissions INTEGER NOT NULL, UNIQUE (login))`; + + return runDB(db, tableRegisteredUser, []); +} + +/* Insérer un utilisateur dans la table registered_user */ +export function insertUser( + db: sqlite3.Database, + login: string, + password: string, + permissions: number +) { + const insertUserQuery = `INSERT INTO registered_user (login, password, permissions) VALUES (?, ?, ?)`; + + return runDB(db, insertUserQuery, [login, password, permissions]); +} + +/* Modifier le login d'un utilisateur dans la table registered_user */ +export function updateUserLogin( + db: sqlite3.Database, + id: number, + newLogin: string +) { + const updateUserLoginQuery = `UPDATE registered_user SET login = ? WHERE id_user = ?`; + + return runDB(db, updateUserLoginQuery, [newLogin, id]); +} + +/* Modifier le mot de passe d'un utilisateur dans la table registered_user */ +export function updateUserPassword( + db: sqlite3.Database, + id: number, + newPassword: string +) { + const updateUserPasswordQuery = `UPDATE registered_user SET password = ? WHERE id_user = ?`; + + return runDB(db, updateUserPasswordQuery, [newPassword, id]); +} + +/* Modifier les permissions d'un utilisateur dans la table registered_user */ +export function updateUserPermissions( + db: sqlite3.Database, + id: number, + newPermissions: number +) { + const updateUserPermissionsQuery = `UPDATE registered_user SET permissions = ? WHERE id_user = ?`; + + return runDB(db, updateUserPermissionsQuery, [newPermissions, id]); +} + +/* Supprimer un utilisateur de la table registered_user par son ID */ +export function deleteUserById(db: sqlite3.Database, id: number) { + const deleteUserQuery = `DELETE FROM registered_user WHERE id_user = ?`; + + return runDB(db, deleteUserQuery, [id]); +} + +/* Supprimer un utilisateur de la table registered_user par son login */ +export function deleteUserByLogin(db: sqlite3.Database, login: string) { + const deleteUserQuery = `DELETE FROM registered_user WHERE login = ?`; + + return runDB(db, deleteUserQuery, [login]); +} + +/* Supprimer tous les utilisateurs de la table registered_user */ +export function deleteAllUsers(db: sqlite3.Database) { + const deleteAllUsersQuery = `DELETE FROM registered_user`; + + return runDB(db, deleteAllUsersQuery, []); +} + +/* Sélectionner tous les utilisateurs de la table registered_user */ +export function selectAllUsers(db: sqlite3.Database): Promise { + const selectAllUsersQuery = `SELECT * FROM registered_user`; + + return allDB(db, selectAllUsersQuery); +} + +/* Sélectionner un utilisateur par son login */ +export function selectUserByLogin(db: sqlite3.Database, login: string) { + const selectUserByLoginQuery = `SELECT * FROM registered_user WHERE login = ?`; + + return getDB(db, selectUserByLoginQuery, [login]); +} + +/* Sélectionner un utilisateur par son ID */ +export function selectUserById(db: sqlite3.Database, id: number) { + const selectUserByIdQuery = `SELECT * FROM registered_user WHERE id_user = ?`; + + return getDB(db, selectUserByIdQuery, [id]); +} + +/////////////////////////// Gestion des Languages /////////////////////////// +// CREATE TABLE language ( +// id_language SERIAL PRIMARY KEY, +// designation VARCHAR(30) NOT NULL, +// version INT NOT NULL, +// ); + +/* Créer la table language dans la base de données */ +export function createLanguageTable(db: sqlite3.Database): Promise { + const tableLanguage = `CREATE TABLE IF NOT EXISTS language (id_language INTEGER PRIMARY KEY AUTOINCREMENT, designation TEXT NOT NULL, version INTEGER NOT NULL)`; + return runDB(db, tableLanguage, []); +} + +/* Insérer un language dans la table language */ +export function insertLanguage( + db: sqlite3.Database, + designation: string, + version: number +) { + const insertLanguageQuery = `INSERT INTO language (designation, version) VALUES (?, ?)`; + + return runDB(db, insertLanguageQuery, [designation, version]); +} + +/* Modifier la designation d'un language dans la table language */ +export function updateLanguageDesignation( + db: sqlite3.Database, + id: number, + newDesignation: string +) { + const updateLanguageDesignationQuery = `UPDATE language SET designation = ? WHERE id_language = ?`; + + return runDB(db, updateLanguageDesignationQuery, [newDesignation, id]); +} + +/* Modifier la version d'un language dans la table language */ +export function updateLanguageVersion( + db: sqlite3.Database, + id: number, + newVersion: number +) { + const updateLanguageVersionQuery = `UPDATE language SET version = ? WHERE id_language = ?`; + + return runDB(db, updateLanguageVersionQuery, [newVersion, id]); +} + +/* Supprimer un language de la table language par son ID */ +export function deleteLanguage(db: sqlite3.Database, id: number) { + const deleteLanguageQuery = `DELETE FROM language WHERE id_language = ?`; + + return runDB(db, deleteLanguageQuery, [id]); +} + +/* Supprimer tous les languages de la table language */ +export function deleteAllLanguages(db: sqlite3.Database) { + const deleteAllLanguagesQuery = `DELETE FROM language`; + + return runDB(db, deleteAllLanguagesQuery, []); +} + +/* Sélectionner tous les languages de la table language */ +export function selectAllLanguages(db: sqlite3.Database): Promise { + const selectAllLanguagesQuery = `SELECT * FROM language`; + + return allDB(db, selectAllLanguagesQuery); +} + +/* Sélectionner un language par son ID */ +export function selectLanguageById(db: sqlite3.Database, id: number) { + const selectLanguageByIdQuery = `SELECT * FROM language WHERE id_language = ?`; + + return getDB(db, selectLanguageByIdQuery, [id]); +} + +/////////////////////////// Gestion des works /////////////////////////// +// CREATE TABLE work ( +// id_work SERIAL PRIMARY KEY, +// link CHAR(36) NOT NULL, +// user_id INT REFERENCES registered_user(id_user), +// language_id INT NOT NULL REFERENCES language(id_language) +// content TEXT NOT NULL, +// ); + +/* Créer la table work dans la base de données */ +export function createWorkTable(db: sqlite3.Database): Promise { + const tableWork = `CREATE TABLE IF NOT EXISTS work (id_work INTEGER PRIMARY KEY AUTOINCREMENT, link CHAR(36) NOT NULL, user_id INTEGER REFERENCES registered_user(id_user), language_id INTEGER NOT NULL REFERENCES language(id_language), content TEXT NOT NULL)`; + return runDB(db, tableWork, []); +} + +/* Insérer un work dans la table work */ +export function insertWork( + db: sqlite3.Database, + link: string, + user_id: number, + language_id: number, + content: string +) { + const insertWorkQuery = `INSERT INTO work (link, user_id, language_id, content) VALUES (?, ?, ?, ?)`; + + return runDB(db, insertWorkQuery, [link, user_id, language_id, content]); +} + +/* Sélectionner tous les works de la table work */ +export function selectAllWorks(db: sqlite3.Database): Promise { + const selectAllWorksQuery = `SELECT * FROM work`; + + return allDB(db, selectAllWorksQuery); +} + +/* Supprimer tous les works de la table work */ +export function deleteAllWorks(db: sqlite3.Database) { + const deleteAllWorksQuery = `DELETE FROM work`; + + return runDB(db, deleteAllWorksQuery, []); +} + +/* Supprimer un work de la table work */ +export function deleteWork(db: sqlite3.Database, id: number) { + const deleteWorkQuery = `DELETE FROM work WHERE id_work = ?`; + + return runDB(db, deleteWorkQuery, [id]); +} + +/* Sélectionner un work par son ID */ +export function selectWorkById(db: sqlite3.Database, id: number) { + const selectWorkByIdQuery = `SELECT * FROM work WHERE id_work = ?`; + + return getDB(db, selectWorkByIdQuery, [id]); +} diff --git a/src/runner.ts b/src/runner.ts index 2dc383b..290e8bb 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -1,8 +1,12 @@ -export const RUNNERS = ['bash', 'moshell', 'bun', 'typescript'] as const; +export const RUNNERS = ["bash", "moshell", "bun", "typescript"] as const; const ALLOWED_LANGUAGES = new Set(RUNNERS); -const aliases: Record = { - 'JavaScript': 'bun', - 'TypeScript': 'typescript', +const aliases: Record = { + JavaScript: "bun", + TypeScript: "typescript", +}; +export const IMAGES = { + logo: "logo.png", + background: "background.png", }; /** @@ -12,9 +16,15 @@ const aliases: Record = { * @param code The code to be executed. * @param image The image to be used. */ -export function allocateBuffer(jobId: string, code: string, image: string): Buffer { +export function allocateBuffer( + jobId: string, + code: string, + image: string +): Buffer { let cur = 0; - const buffer = Buffer.allocUnsafe(jobId.length + image.length + code.length + 9); + const buffer = Buffer.allocUnsafe( + jobId.length + image.length + code.length + 9 + ); cur = buffer.writeUInt8(0, cur); cur += buffer.write(jobId, cur); cur = buffer.writeUInt32BE(image.length, cur); @@ -24,10 +34,10 @@ export function allocateBuffer(jobId: string, code: string, image: string): Buff return buffer; } -export function getRunner(language: string): typeof RUNNERS[number] | null { +export function getRunner(language: string): (typeof RUNNERS)[number] | null { language = aliases[language] || language; if (ALLOWED_LANGUAGES.has(language)) { - return language as typeof RUNNERS[number]; + return language as (typeof RUNNERS)[number]; } return null; } diff --git a/src/server.ts b/src/server.ts index 7099321..f8606ac 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,9 +1,13 @@ -import cors from '@fastify/cors'; -import { Type, TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; -import Fastify, { FastifyReply } from 'fastify'; -import { nanoid } from 'nanoid'; -import { allocateBuffer, getRunner } from 'runner'; -import { Pull, Push } from 'zeromq'; +import cors from "@fastify/cors"; +import { Type, TypeBoxTypeProvider } from "@fastify/type-provider-typebox"; +import Fastify, { FastifyReply, FastifyRequest } from "fastify"; +import { nanoid } from "nanoid"; +import { allocateBuffer, IMAGES } from "runner"; +import { Pull, Push } from "zeromq"; + +import * as db from "./database"; + +console.log(IMAGES.logo); const sender = new Push(); await sender.bind(`tcp://127.0.0.1:5557`); @@ -17,41 +21,377 @@ const fastify = Fastify({ logger: true, }).withTypeProvider(); await fastify.register(cors, { - origin: process.env.ALLOW_ORIGIN || '*', + origin: process.env.ALLOW_ORIGIN || "*", +}); + +/* Database */ + +/* Création du répertoire de la base de données s'il n'existe pas */ +db.createDbDirectory(); + +/* Ouvrir la base de données */ +const database = db.openDatabase(); + +/* Créer les tables si elles n'existent pas */ +db.createTables(database); + +/* Route pour créer un utilisateur */ +fastify.post( + "/users", + { + schema: { + body: Type.Object({ + login: Type.String(), + password: Type.String(), + permissions: Type.Number(), + }), + }, + }, + async (request, reply) => { + const { login, password, permissions } = request.body; + db.insertUser(database, login, password, permissions); + reply.send({ success: true }); + } +); + +/* Route pour mettre à jour le login d'un utilisateur */ +fastify.put( + "/users/:id/login", + { + schema: { + params: Type.Object({ + id: Type.Number({ + minimum: 0, + }), + }), + body: Type.Object({ + newLogin: Type.String(), + }), + }, + }, + async (request, reply) => { + const { id } = request.params; + const { newLogin } = request.body; + db.updateUserLogin(database, id, newLogin); + reply.send({ success: true }); + } +); + +/* Route pour mettre à jour le mot de passe d'un utilisateur */ +fastify.put( + "/users/:id/password", + { + schema: { + params: Type.Object({ + id: Type.Number({ + minimum: 0, + }), + }), + body: Type.Object({ + newPassword: Type.String(), + }), + }, + }, + async (request, reply) => { + const { id } = request.params; + const { newPassword } = request.body; + db.updateUserPassword(database, id, newPassword); + reply.send({ success: true }); + } +); + +/* Route pour mettre à jour les permissions d'un utilisateur */ +fastify.put( + "/users/:id/permissions", + { + schema: { + params: Type.Object({ + id: Type.Number({ + minimum: 0, + }), + }), + body: Type.Object({ + newPermissions: Type.Number(), + }), + }, + }, + async (request, reply) => { + const { id } = request.params; + const { newPermissions } = request.body; + await db.updateUserPermissions(database, id, newPermissions); + reply.send({ success: true }); + } +); + +/* Route pour supprimer un utilisateur par son ID */ +fastify.delete( + "/users/:id", + { + schema: { + params: Type.Object({ + id: Type.Number({ + minimum: 0, + }), + }), + }, + }, + async (request, reply) => { + const { id } = request.params; + await db.deleteUserById(database, id); + reply.send({ success: true }); + } +); + +/* Route pour supprimer un utilisateur par son login */ +fastify.delete( + "/users/login/:login", + { + schema: { + params: Type.Object({ + login: Type.String(), + }), + }, + }, + async (request, reply) => { + const { login } = request.params; + await db.deleteUserByLogin(database, login); + reply.send({ success: true }); + } +); + +/* Route pour supprimer tous les utilisateurs */ +fastify.delete("/users", async (request, reply) => { + await db.deleteAllUsers(database); + reply.send({ success: true }); +}); + +/* Route pour récupérer tous les utilisateurs */ +fastify.get("/users", async (request, reply) => { + const users = await db.selectAllUsers(database); + reply.send(users); +}); + +/* Route pour récupérer un utilisateur par son ID */ +fastify.get( + "/users/:id", + { + schema: { + params: Type.Object({ + id: Type.Number({ + minimum: 0, + }), + }), + }, + }, + async (request, reply) => { + const { id } = request.params; + const user = await db.selectUserById(database, id); + reply.send(user); + } +); + +/* Route pour récupérer un utilisateur par son login */ +fastify.get( + "/users/login/:login", + { + schema: { + params: Type.Object({ + login: Type.String(), + }), + }, + }, + async (request, reply) => { + const { login } = request.params; + const user = await db.selectUserByLogin(database, login); + reply.send(user); + } +); + +/* Route pour créer un language */ +fastify.post( + "/languages", + { + schema: { + body: Type.Object({ + designation: Type.String(), + version: Type.Number(), + }), + }, + }, + async (request, reply) => { + const { designation, version } = request.body; + db.insertLanguage(database, designation, version); + reply.send({ success: true }); + } +); + +/* Route pour mettre à jour la désignation d'un language */ +fastify.put( + "/languages/:id/designation", + { + schema: { + params: Type.Object({ + id: Type.Number({ + minimum: 0, + }), + }), + body: Type.Object({ + newDesignation: Type.String(), + }), + }, + }, + async (request, reply) => { + const { id } = request.params; + const { newDesignation } = request.body; + db.updateLanguageDesignation(database, id, newDesignation); + reply.send({ success: true }); + } +); + +/* Route pour mettre à jour la version d'un language */ +fastify.put( + "/languages/:id/version", + { + schema: { + params: Type.Object({ + id: Type.Number({ + minimum: 0, + }), + }), + body: Type.Object({ + newVersion: Type.Number(), + }), + }, + }, + async (request, reply) => { + const { id } = request.params; + const { newVersion } = request.body; + db.updateLanguageVersion(database, id, newVersion); + reply.send({ success: true }); + } +); + +/* Route pour supprimer un language */ +fastify.delete( + "/languages/:id", + { + schema: { + params: Type.Object({ + id: Type.Number({ + minimum: 0, + }), + }), + }, + }, + async (request, reply) => { + const { id } = request.params; + db.deleteLanguage(database, id); + reply.send({ success: true }); + } +); + +/* Route pour supprimer tous les languages */ +fastify.delete("/languages", async (request, reply) => { + db.deleteAllLanguages(database); + reply.send({ success: true }); }); -fastify.post('/run', { - schema: { - body: Type.Object({ - code: Type.String(), - language: Type.String(), - }), - }, -}, (req, reply) => { - const { code, language } = req.body; - const runner = getRunner(language); - if (runner === null) { - return reply.status(422).send({ error: 'Invalid language' }); - } - const jobId = generateId(); - const buffer = allocateBuffer(jobId, code, runner); - reply.raw.writeHead(200, { - 'Content-Type': 'text/event-stream', - Connection: 'keep-alive', - 'Cache-Control': 'no-cache', - 'Access-Control-Allow-Origin': process.env.ALLOW_ORIGIN || '*', - }); - reply.raw.on('close', () => { - delete clients[jobId]; - }); - sender.send(buffer).then(() => { - reply.raw.write('event: connected\n'); - reply.raw.write(`data: ${jobId}\n`); - reply.raw.write('id: 0\n\n'); - }); - clients[jobId] = reply; +/* Route pour récupérer un language par son ID */ +fastify.get( + "/languages/:id", + { + schema: { + params: Type.Object({ + id: Type.Number({ + minimum: 0, + }), + }), + }, + }, + async (request, reply) => { + const { id } = request.params; + const language = await db.selectLanguageById(database, id); + reply.send(language); + } +); + +/* Route pour récupérer tous les languages */ +fastify.get("/languages", async (request, reply) => { + const languages = await db.selectAllLanguages(database); + reply.send(languages); +}); + +/* Route pour créer un work */ +fastify.post( + "/works", + { + schema: { + body: Type.Object({ + id_user: Type.Number(), + link: Type.String(), + id_language: Type.Number(), + code: Type.String(), + }), + }, + }, + async (request, reply) => { + const { id_user, link, id_language, code } = request.body; + db.insertWork(database, link, id_user, id_language, code); + reply.send({ success: true }); + } +); + +/* Route pour récupérer tous les works */ +fastify.get("/works", async (request, reply) => { + const works = await db.selectAllWorks(database); + reply.send(works); }); +/* Route pour supprimer tous les works */ +fastify.delete("/works", async (request, reply) => { + db.deleteAllWorks(database); + reply.send({ success: true }); +}); + +/* Route pour supprimer un work par son ID */ +fastify.delete( + "/works/:id", + { + schema: { + params: Type.Object({ + id: Type.Number({ + minimum: 0, + }), + }), + }, + }, + async (request, reply) => { + const { id } = request.params; + db.deleteWork(database, id); + reply.send({ success: true }); + } +); + +/* Route pour récupérer un work par son ID */ +fastify.get( + "/works/:id", + { + schema: { + params: Type.Object({ + id: Type.Number({ + minimum: 0, + }), + }), + }, + }, + async (request, reply) => { + const { id } = request.params; + const work = await db.selectWorkById(database, id); + reply.send(work); + } +); + +/* Forward output est une fonction asynchrone qui permet de récupérer les messages envoyés par le container et de les renvoyer au client */ async function forwardOutput() { for await (const [buff] of receiver) { const messageType = buff.readInt8(); @@ -66,25 +406,26 @@ async function forwardOutput() { case 1: case 2: const text = encodeURIComponent(buff.subarray(33).toString()); - raw.write('event: message\n'); + raw.write("event: message\n"); if (messageType === 1) { - raw.write('id: stdout\n'); + raw.write("id: stdout\n"); } else { - raw.write('id: stderr\n'); + raw.write("id: stderr\n"); } raw.write(`data: ${text}\n\n`); break; case 3: const exitCode = buff.readUint32BE(33); - raw.write('event: message\n'); - raw.write('id: exit\n'); + raw.write("event: message\n"); + raw.write("id: exit\n"); raw.write(`data: ${exitCode}\n\n`); raw.end(); break; default: - console.error('Unknown message type', messageType); + console.error("Unknown message type", messageType); } } } +/* Lancer le serveur et la fonction forwardOutput sur le même thread en parallèle */ await Promise.all([fastify.listen({ port: 3000 }), forwardOutput()]);