Compare commits
16 Commits
Author | SHA1 | Date |
---|---|---|
|
1741f5d0cf | 10 months ago |
|
96bb32ac32 | 10 months ago |
|
3c79473164 | 10 months ago |
|
13788c5538 | 10 months ago |
|
c0ac68a516 | 11 months ago |
|
85e5d9257e | 11 months ago |
|
ef726bc789 | 11 months ago |
|
b58317ae88 | 11 months ago |
|
aaf24387f1 | 11 months ago |
|
0801e3005e | 1 year ago |
|
1750a2a33a | 1 year ago |
|
1590aa91ff | 1 year ago |
|
293f28ec9e | 1 year ago |
|
7a92fb3e70 | 1 year ago |
|
bfbc86cde3 | 1 year ago |
|
5fb98c0395 | 1 year ago |
@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
*.js
|
@ -0,0 +1,14 @@
|
||||
FROM node:22-bookworm AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV="production"
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install --omit=dev
|
||||
|
||||
FROM runner AS builder
|
||||
COPY . .
|
||||
RUN npm install && npm run build && npm prune --omit=dev
|
||||
|
||||
FROM runner
|
||||
COPY --from=builder /app/dist /app
|
||||
EXPOSE 3000
|
||||
ENTRYPOINT ["node", "src/server.js"]
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"typescript": {
|
||||
"quoteStyle": "preferSingle"
|
||||
"quoteStyle": "preferDouble"
|
||||
},
|
||||
"includes": ["src/**/*.{ts,tsx,js,jsx,cjs,mjs,json}"],
|
||||
"excludes": [
|
||||
"**/node_modules"
|
||||
],
|
||||
"plugins": [
|
||||
"https://plugins.dprint.dev/typescript-0.88.1.wasm"
|
||||
"https://plugins.dprint.dev/typescript-0.88.9.wasm"
|
||||
]
|
||||
}
|
||||
|
@ -0,0 +1,357 @@
|
||||
import fs from "fs";
|
||||
import sqlite3 from "sqlite3";
|
||||
|
||||
const dbDirectory = "./src/db";
|
||||
const dbFilePath = `${dbDirectory}/database.db`;
|
||||
|
||||
export type error = {
|
||||
errno: number;
|
||||
code: string;
|
||||
};
|
||||
|
||||
/* 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<void> {
|
||||
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<T>(
|
||||
db: sqlite3.Database,
|
||||
query: string,
|
||||
params?: any[]
|
||||
): Promise<unknown[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all(query, params, (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<T>(
|
||||
db: sqlite3.Database,
|
||||
query: string,
|
||||
params: any[],
|
||||
): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get(query, params, (err, row: T) => {
|
||||
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);
|
||||
createWorkTable(db);
|
||||
}
|
||||
|
||||
/////////////////////////// Gestion des utilisateurs ///////////////////////////
|
||||
// CREATE TABLE registered_user (
|
||||
// id_user SERIAL PRIMARY KEY,
|
||||
// login VARCHAR(64) NOT NULL,
|
||||
// email 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<void> {
|
||||
const tableRegisteredUser = `CREATE TABLE IF NOT EXISTS registered_user (
|
||||
id_user INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
login TEXT NOT NULL,
|
||||
email 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 async function insertUser(
|
||||
db: sqlite3.Database,
|
||||
login: string,
|
||||
email: string,
|
||||
password: string,
|
||||
permissions: number,
|
||||
): Promise<boolean> {
|
||||
const insertUserQuery = `INSERT INTO registered_user (login, email, password, permissions) VALUES (?, ?, ?, ?)`;
|
||||
try {
|
||||
await runDB(db, insertUserQuery, [login, email, password, permissions]);
|
||||
return true;
|
||||
} catch (e) {
|
||||
const error = e as error;
|
||||
if (error.code === "SQLITE_CONSTRAINT") {
|
||||
return false;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Vérifier si un utilisateur existe dans la table registered_user */
|
||||
export async function verifyUser(
|
||||
db: sqlite3.Database,
|
||||
login: string,
|
||||
): Promise<User | null> {
|
||||
const verifyUserQuery = `SELECT id_user, login, password FROM registered_user WHERE login = ?`;
|
||||
|
||||
const res = await getDB<User>(db, verifyUserQuery, [login]);
|
||||
|
||||
if (!res) {
|
||||
return null;
|
||||
} else {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export type User = {
|
||||
id_user: number;
|
||||
login: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
/* 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]);
|
||||
}
|
||||
|
||||
export function updateUserEmail(
|
||||
db: sqlite3.Database,
|
||||
id: number,
|
||||
newEmail: string
|
||||
) {
|
||||
const updateUserEmailQuery = `UPDATE registered_user SET email = ? WHERE id_user = ?`;
|
||||
return runDB(db, updateUserEmailQuery, [newEmail, 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<unknown[]> {
|
||||
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 works ///////////////////////////
|
||||
// CREATE TABLE work (
|
||||
// id_work SERIAL PRIMARY KEY,
|
||||
// link CHAR(36) NOT NULL,
|
||||
// user_id INT REFERENCES registered_user(id_user),
|
||||
// language TEXT NOT NULL,
|
||||
// title VARCHAR(30) NOT NULL,
|
||||
// content TEXT NOT NULL,
|
||||
// date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
// );
|
||||
|
||||
/* Créer la table work dans la base de données */
|
||||
export function createWorkTable(db: sqlite3.Database): Promise<void> {
|
||||
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 TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
date TIMESTAMP DEFAULT CURRENT_TIMESTAMP)`;
|
||||
return runDB(db, tableWork, []);
|
||||
}
|
||||
|
||||
/* Insérer un work dans la table work */
|
||||
export function insertWork(
|
||||
db: sqlite3.Database,
|
||||
link: string,
|
||||
user_id: number,
|
||||
language: string,
|
||||
title: string,
|
||||
content: string,
|
||||
) {
|
||||
const insertWorkQuery = `INSERT INTO work (link, user_id, language, title, content, date) VALUES (?, ?, ?, ?, ?, ?)`;
|
||||
|
||||
return runDB(db, insertWorkQuery, [link, user_id, language, title, content, new Date().toISOString()]);
|
||||
}
|
||||
|
||||
/* Sélectionner tous les works de la table work */
|
||||
export function selectAllWorks(db: sqlite3.Database): Promise<unknown[]> {
|
||||
const selectAllWorksQuery = `SELECT * FROM work`;
|
||||
|
||||
return allDB(db, selectAllWorksQuery);
|
||||
}
|
||||
|
||||
/* Sélectionner tous les works de la table work */
|
||||
export function selectWorksByUserId(db: sqlite3.Database, user_id: number): Promise<unknown[]> {
|
||||
const selectWorksByUserIdQuery = `SELECT * FROM work WHERE user_id = ?`;
|
||||
|
||||
return allDB(db, selectWorksByUserIdQuery, [user_id]);
|
||||
}
|
||||
|
||||
/* Sélectionner un work par son ID */
|
||||
export function selectWorkByLink(db: sqlite3.Database, link: string) {
|
||||
const selectWorkByLinkQuery = `SELECT * FROM work WHERE link = ?`;
|
||||
return getDB(db, selectWorkByLinkQuery, [link]);
|
||||
}
|
||||
|
||||
/* Sélectionner le dernier work par l'id de l'utilisateur */
|
||||
export function selectLastWorkByUserId(db: sqlite3.Database, user_id: number) {
|
||||
const selectLastWorkByUserIdQuery = `SELECT * FROM work
|
||||
WHERE user_id = ?
|
||||
ORDER BY date DESC
|
||||
LIMIT 1`;
|
||||
|
||||
return getDB(db, selectLastWorkByUserIdQuery, [user_id]);
|
||||
}
|
||||
|
||||
/* 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]);
|
||||
}
|
||||
|
||||
/* Update the work title by its ID */
|
||||
export function updateWorkTitle(
|
||||
db: sqlite3.Database,
|
||||
id: number,
|
||||
newTitle: string
|
||||
) {
|
||||
const updateWorkTitleQuery = `UPDATE work SET title = ? WHERE id_work = ?`;
|
||||
|
||||
return runDB(db, updateWorkTitleQuery, [newTitle, id]);
|
||||
}
|
||||
|
||||
/* Update the work content by its ID */
|
||||
export function updateWorkContent(
|
||||
db: sqlite3.Database,
|
||||
id: number,
|
||||
newContent: string,
|
||||
language: string,
|
||||
) {
|
||||
const updateWorkContentQuery = `UPDATE work SET content = ?, language = ? WHERE id_work = ?`;
|
||||
|
||||
return runDB(db, updateWorkContentQuery, [newContent, language, id]);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,43 @@
|
||||
export const RUNNERS = ["bash", "moshell", "bun", "typescript"] as const;
|
||||
const ALLOWED_LANGUAGES = new Set<string>(RUNNERS);
|
||||
const aliases: Record<string, (typeof RUNNERS)[number]> = {
|
||||
JavaScript: "bun",
|
||||
TypeScript: "typescript",
|
||||
};
|
||||
export const IMAGES = {
|
||||
logo: "logo.png",
|
||||
background: "background.png",
|
||||
};
|
||||
|
||||
/**
|
||||
* Prepares a buffer to be sent to the runner.
|
||||
*
|
||||
* @param jobId The job unique identifier.
|
||||
* @param code The code to be executed.
|
||||
* @param image The image to be used.
|
||||
*/
|
||||
export function allocateBuffer(
|
||||
jobId: string,
|
||||
code: string,
|
||||
image: string,
|
||||
): Buffer {
|
||||
let cur = 0;
|
||||
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);
|
||||
cur = buffer.writeUInt32BE(code.length, cur);
|
||||
cur += buffer.write(image, cur);
|
||||
buffer.write(code, cur);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
export function getRunner(language: string): (typeof RUNNERS)[number] | null {
|
||||
language = aliases[language] || language.toLowerCase();
|
||||
if (ALLOWED_LANGUAGES.has(language)) {
|
||||
return language as (typeof RUNNERS)[number];
|
||||
}
|
||||
return null;
|
||||
}
|
@ -1,76 +1,567 @@
|
||||
import http from 'http';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Pull, Push } from 'zeromq';
|
||||
|
||||
const host = 'localhost';
|
||||
const port = 3000;
|
||||
import cors from "@fastify/cors";
|
||||
import websocket, { WebSocket } from "@fastify/websocket";
|
||||
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 { ChangeSet, Text } from "@codemirror/state";
|
||||
import { Update, rebaseUpdates } from "@codemirror/collab";
|
||||
import * as db from "./database";
|
||||
import bcrypt from "bcrypt";
|
||||
import { fastifySession } from "@fastify/session";
|
||||
import { fastifyCookie } from "@fastify/cookie";
|
||||
|
||||
const sender = new Push();
|
||||
await sender.bind(`tcp://127.0.0.1:5557`);
|
||||
const receiver = new Pull();
|
||||
await receiver.bind(`tcp://127.0.0.1:5558`);
|
||||
|
||||
const clients: Record<string, FastifyReply> = {};
|
||||
const generateId = () => nanoid(32);
|
||||
|
||||
const clients: Record<string, http.ServerResponse> = {};
|
||||
|
||||
const CORS = {
|
||||
'Access-Control-Allow-Methods': '*',
|
||||
'Access-Control-Allow-Headers': '*',
|
||||
'Access-Control-Allow-Origin': process.env.ALLOW_ORIGIN || '*',
|
||||
type Room = {
|
||||
sockets: WebSocket[];
|
||||
updates: Update[];
|
||||
doc: Text;
|
||||
};
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(200, CORS);
|
||||
res.end();
|
||||
return;
|
||||
const rooms: Record<string, Room> = {};
|
||||
|
||||
function send(socket: WebSocket, requestId: number, payload: unknown) {
|
||||
const response = {
|
||||
_request: requestId,
|
||||
payload,
|
||||
};
|
||||
socket.send(JSON.stringify(response));
|
||||
}
|
||||
|
||||
const fastify = Fastify({
|
||||
logger: true,
|
||||
}).withTypeProvider<TypeBoxTypeProvider>();
|
||||
type Fastify = typeof fastify;
|
||||
await fastify.register(cors, {
|
||||
origin: process.env.ALLOW_ORIGIN || "*",
|
||||
credentials: true,
|
||||
methods: ["GET", "POST", "PUT", "DELETE"],
|
||||
});
|
||||
fastify.register(fastifyCookie);
|
||||
fastify.register(fastifySession, {
|
||||
secret: "8jYuS75JZuxb6C72nDtH2cY6hnV4B7i35r5c39gQ3h9G9DApAweBsQ47dU9DGpk5",
|
||||
cookie: {
|
||||
secure: false,
|
||||
sameSite: "none",
|
||||
partitioned: true,
|
||||
},
|
||||
saveUninitialized: false,
|
||||
cookieName: "session-id",
|
||||
});
|
||||
|
||||
declare module "fastify" {
|
||||
interface Session {
|
||||
userKey: number;
|
||||
}
|
||||
switch (req.url) {
|
||||
case '/run':
|
||||
const jobId = generateId();
|
||||
const code = 'echo a';
|
||||
const image = 'ghcr.io/moshell-lang/moshell:master';
|
||||
const buffer = Buffer.allocUnsafe(jobId.length + image.length + code.length + 8);
|
||||
buffer.write(jobId, 0);
|
||||
buffer.writeUInt32BE(image.length, jobId.length);
|
||||
buffer.writeUInt32BE(code.length, jobId.length + 4);
|
||||
buffer.write(image, jobId.length + 8);
|
||||
buffer.write(code, jobId.length + 8 + image.length);
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
Connection: 'keep-alive',
|
||||
'Cache-Control': 'no-cache',
|
||||
...CORS,
|
||||
});
|
||||
sender.send(buffer).then(() => {
|
||||
res.write('event: connected\n');
|
||||
res.write(`data: ${jobId}\n`);
|
||||
res.write('id: 0\n\n');
|
||||
});
|
||||
req.on('close', () => {
|
||||
res.end('OK');
|
||||
delete clients[jobId];
|
||||
}
|
||||
|
||||
fastify.register(websocket);
|
||||
fastify.register(async function(fastify: Fastify) {
|
||||
fastify.get(
|
||||
"/live/:roomId",
|
||||
{
|
||||
schema: {
|
||||
params: Type.Object({
|
||||
roomId: Type.String(),
|
||||
}),
|
||||
},
|
||||
websocket: true,
|
||||
},
|
||||
(socket, request) => {
|
||||
const { roomId } = request.params;
|
||||
let room = rooms[roomId];
|
||||
if (!room) {
|
||||
room = {
|
||||
sockets: [],
|
||||
updates: [],
|
||||
doc: Text.of([""]),
|
||||
};
|
||||
rooms[roomId] = room;
|
||||
}
|
||||
room.sockets.push(socket);
|
||||
socket.on("message", message => {
|
||||
const data = JSON.parse(message.toString());
|
||||
const requestId = data._request;
|
||||
|
||||
if (data.type === "pullUpdates") {
|
||||
send(socket, requestId, room.updates.slice(data.version));
|
||||
} else if (data.type === "pushUpdates") {
|
||||
let received = data.updates.map((json: any) => ({
|
||||
clientID: json.clientID,
|
||||
changes: ChangeSet.fromJSON(json.changes),
|
||||
}));
|
||||
|
||||
if (data.version != room.updates.length) {
|
||||
received = rebaseUpdates(received, room.updates.slice(data.version));
|
||||
}
|
||||
|
||||
for (let update of received) {
|
||||
room.updates.push(update);
|
||||
room.doc = update.changes.apply(room.doc);
|
||||
}
|
||||
send(
|
||||
socket,
|
||||
requestId,
|
||||
received.map((update: any) => ({
|
||||
clientID: update.clientID,
|
||||
changes: update.changes.toJSON(),
|
||||
})),
|
||||
);
|
||||
} else if (data.type === "getDocument") {
|
||||
send(socket, requestId, { version: room.updates.length, doc: room.doc.toString() });
|
||||
}
|
||||
});
|
||||
clients[jobId] = res;
|
||||
break;
|
||||
default:
|
||||
res.writeHead(404, CORS);
|
||||
res.end('404!');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
/* Route pour créer une room */
|
||||
fastify.post("/live", {
|
||||
schema: {
|
||||
body: Type.Object({
|
||||
code: Type.String(),
|
||||
}),
|
||||
},
|
||||
}, (request, reply) => {
|
||||
const { code } = request.body;
|
||||
let room, roomId;
|
||||
do {
|
||||
roomId = generateId();
|
||||
room = rooms[roomId];
|
||||
} while (room);
|
||||
|
||||
room = {
|
||||
sockets: [],
|
||||
updates: [],
|
||||
doc: Text.of([code]),
|
||||
};
|
||||
rooms[roomId] = room;
|
||||
return roomId;
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
/* 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(),
|
||||
email: Type.String(),
|
||||
password: Type.String(),
|
||||
permissions: Type.Number(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const { login, email, password, permissions } = request.body;
|
||||
|
||||
// Hashage du mot de passe
|
||||
const saltRounds = 10;
|
||||
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
||||
|
||||
const success = await db.insertUser(
|
||||
database,
|
||||
login,
|
||||
email,
|
||||
hashedPassword,
|
||||
permissions
|
||||
);
|
||||
reply.send({ success });
|
||||
}
|
||||
);
|
||||
|
||||
/* Route pour vérifier si un utilisateur existe */
|
||||
fastify.post(
|
||||
"/users/login",
|
||||
{
|
||||
schema: {
|
||||
body: Type.Object({
|
||||
login: Type.String(),
|
||||
password: Type.String(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const { login, password } = request.body;
|
||||
const user = await db.verifyUser(database, login);
|
||||
|
||||
if (user === null || !(await bcrypt.compare(password, user.password))) {
|
||||
reply.send({ success: false });
|
||||
} else {
|
||||
request.session.userKey = user.id_user;
|
||||
reply.send({ success: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/* Route pour se déconnecter */
|
||||
fastify.get("/users/logout", async (request, reply) => {
|
||||
console.log(request.session.userKey);
|
||||
await request.session.destroy();
|
||||
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;
|
||||
|
||||
const saltRounds = 10;
|
||||
const hashedPassword = await bcrypt.hash(newPassword, saltRounds);
|
||||
|
||||
await db.updateUserPassword(database, id, hashedPassword);
|
||||
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 mettre à jour l'email d'un utilisateur */
|
||||
fastify.put(
|
||||
"/users/:id/email",
|
||||
{
|
||||
schema: {
|
||||
params: Type.Object({
|
||||
id: Type.Number({
|
||||
minimum: 0,
|
||||
}),
|
||||
}),
|
||||
body: Type.Object({
|
||||
newEmail: Type.String(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const { id } = request.params;
|
||||
const { newEmail } = request.body;
|
||||
|
||||
// Check if the ID relates to an existing ID.
|
||||
const user = await db.selectUserById(database, id);
|
||||
if (!user) {
|
||||
reply.status(404).send({ error: "User not found" });
|
||||
return;
|
||||
}
|
||||
await db.updateUserEmail(database, id, newEmail);
|
||||
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;
|
||||
if (request.session.userKey) {
|
||||
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 work */
|
||||
fastify.post(
|
||||
"/works",
|
||||
{
|
||||
schema: {
|
||||
body: Type.Object({
|
||||
language: Type.String(),
|
||||
title: Type.String(),
|
||||
code: Type.String(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const link = generateId();
|
||||
const user = request.session.userKey;
|
||||
const { language, title, code } = request.body;
|
||||
await db.insertWork(database, link, user, language, title, code);
|
||||
reply.send({ success: true, link });
|
||||
}
|
||||
);
|
||||
|
||||
/* Route pour récupérer tous les works */
|
||||
fastify.get("/works", async (request, reply) => {
|
||||
const works = await db.selectAllWorks(database);
|
||||
reply.send(works);
|
||||
});
|
||||
server.listen(port, () => {
|
||||
console.log(`Server is running on http://${host}:${port}`);
|
||||
|
||||
/* Route pour supprimer tous les works */
|
||||
fastify.delete("/works", async (request, reply) => {
|
||||
await db.deleteAllWorks(database);
|
||||
reply.send({ success: true });
|
||||
});
|
||||
|
||||
for await (const [buff] of receiver) {
|
||||
const jobId = buff.subarray(0, 32).toString();
|
||||
console.log(`Received ${jobId}`);
|
||||
const res = clients[jobId];
|
||||
if (!res) {
|
||||
continue;
|
||||
/* 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 Link */
|
||||
fastify.get(
|
||||
"/works/:link",
|
||||
{
|
||||
schema: {
|
||||
params: Type.Object({
|
||||
link: Type.String(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const {link} = request.params;
|
||||
const work = await db.selectWorkByLink(database, link);
|
||||
reply.send(work);
|
||||
},
|
||||
);
|
||||
|
||||
/* Route pour récupérer le dernier work par l'id de l'utilisateur */
|
||||
fastify.get(
|
||||
"/works/last-work/:user_id",
|
||||
{
|
||||
schema: {
|
||||
params: Type.Object({
|
||||
user_id: Type.Number({
|
||||
minimum: 0,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const { user_id } = request.params;
|
||||
const work = await db.selectLastWorkByUserId(database, user_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();
|
||||
const jobId = buff.subarray(1, 33).toString();
|
||||
const reply = clients[jobId];
|
||||
if (!reply) {
|
||||
continue;
|
||||
}
|
||||
console.debug(`Forwarding message type ${messageType} for job ${jobId}`);
|
||||
const raw = reply.raw;
|
||||
switch (messageType) {
|
||||
case 1:
|
||||
case 2:
|
||||
const text = encodeURIComponent(buff.subarray(33).toString());
|
||||
raw.write("event: message\n");
|
||||
if (messageType === 1) {
|
||||
raw.write("id: stdout\n");
|
||||
} else {
|
||||
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(`data: ${exitCode}\n\n`);
|
||||
raw.end();
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown message type", messageType);
|
||||
}
|
||||
}
|
||||
res.write('event: message\n');
|
||||
res.write(`data: ${encodeURIComponent(buff.subarray(32).toString())}\n`);
|
||||
res.write('id: 1\n\n');
|
||||
}
|
||||
|
||||
/* Lancer le serveur et la fonction forwardOutput sur le même thread en parallèle */
|
||||
await Promise.all([fastify.listen({ port: 3000, host: '0.0.0.0' }), forwardOutput()]);
|
||||
|
Loading…
Reference in new issue