You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

551 lines
13 KiB

import { rebaseUpdates, Update } from "@codemirror/collab";
import { ChangeSet, Text } from "@codemirror/state";
import cors from "@fastify/cors";
import { Type, TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
import websocket, { WebSocket } from "@fastify/websocket";
import Fastify, { FastifyReply } from "fastify";
import { nanoid } from "nanoid";
import { allocateBuffer, getRunner } from "runner";
import { Pull, Push } from "zeromq";
import * as db from "./database";
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);
//let updates: Update[] = [];
//let doc = Text.of(["foo"]);
type Room = {
sockets: WebSocket[];
updates: Update[];
doc: Text;
}
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 || "*",
});
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() });
}
});
});
})
/* Route pour créer une room */
fastify.get(
"/live",
async (request, reply) => {
return generateId();
},
);
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(),
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 });
});
/* 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();
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);
}
}
}
/* Lancer le serveur et la fonction forwardOutput sur le même thread en parallèle */
await Promise.all([fastify.listen({ port: 3000 }), forwardOutput()]);