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 = {}; const generateId = () => nanoid(32); //let updates: Update[] = []; //let doc = Text.of(["foo"]); type Room = { sockets: WebSocket[]; updates: Update[]; doc: Text; } const rooms: Record = {}; function send(socket: WebSocket, requestId: number, payload: unknown) { const response = { _request: requestId, payload, }; socket.send(JSON.stringify(response)); } const fastify = Fastify({ logger: true, }).withTypeProvider(); 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.post( "/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(), email: Type.String(), password: Type.String(), permissions: Type.Number(), }), }, }, async (request, reply) => { const { login, email, password, permissions } = request.body; db.insertUser(database, login, email, 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 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; 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({ id_user: Type.Number(), link: Type.String(), language: Type.String(), title: Type.String(), code: Type.String(), }), }, }, async (request, reply) => { const { id_user, link, language, title, code } = request.body; await db.insertWork(database, link, id_user, language, title, 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 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); }, ); /* Update the work title by its ID */ fastify.put( "/works/:id/title", { schema: { params: Type.Object({ id: Type.Number({ minimum: 0, }), }), body: Type.Object({ newTitle: Type.String(), }), }, }, async (request, reply) => { const { id } = request.params; const { newTitle } = request.body; await db.updateWorkTitle(database, id, newTitle); reply.send({ success: true }); } ); /* Update the work content by its ID */ fastify.put( "/works/:id/content", { schema: { params: Type.Object({ id: Type.Number({ minimum: 0, }), }), body: Type.Object({ newContent: Type.String(), language: Type.String(), }), }, }, async (request, reply) => { const { id } = request.params; const { newContent, language } = request.body; await db.updateWorkContent(database, id, newContent, language); reply.send({ success: true }); } ); /* 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()]);