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 = {}; const generateId = () => nanoid(32); 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 || "*", 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: string | null; } } 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", { 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 = generateId(); reply.send({ success: true }); } bcrypt.compare(password, user!.password) .then(res => reply.send({ sucess: res })) .catch(err => reply.send({ sucess: false })); }, ); /* Route pour se déconnecter */ fastify.get("/users/logout", async (request, reply) => { console.log(request.session.userKey); 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({ 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); }, ); /* 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, host: '0.0.0.0' }), forwardOutput()]);