diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4af4093 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +node_modules +Dockerfile +.dockerignore +*.js diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3567603 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/package.json b/package.json index d2e192b..e007433 100644 --- a/package.json +++ b/package.json @@ -3,22 +3,29 @@ "module": "src/server.ts", "type": "module", "scripts": { - "build": "tsc", - "start": "tsx src/server.ts" + "build": "tsc && tsc-alias", + "start": "tsx src/server.ts", + "fmt": "dprint fmt" }, "devDependencies": { + "@types/bcrypt": "^5.0.2", "@types/bun": "^1.0.4", + "dprint": "^0.46.2", + "tsc-alias": "^1.8.10", "tsx": "^4.7.0", "typescript": "^5.3.3" }, "dependencies": { "@codemirror/collab": "^6.1.1", "@codemirror/state": "^6.4.1", + "@fastify/cookie": "^9.3.1", "@fastify/cors": "^9.0.0", + "@fastify/session": "^10.9.0", "@fastify/type-provider-typebox": "^4.0.0", "@fastify/websocket": "^10.0.1", "@sinclair/typebox": "^0.32.9", "dprint": "^0.46.1", + "bcrypt": "^5.1.1", "fastify": "^4.27.0", "nanoid": "^5.0.4", "sqlite3": "^5.1.7", diff --git a/src/database.ts b/src/database.ts index 4a0f351..89249ed 100644 --- a/src/database.ts +++ b/src/database.ts @@ -4,6 +4,11 @@ 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) */ @@ -45,9 +50,9 @@ export function getDB( db: sqlite3.Database, query: string, params: any[], -): Promise { +): Promise { return new Promise((resolve, reject) => { - db.get(query, params, (err, row: any) => { + db.get(query, params, (err, row: T) => { if (err) { reject(err); } else { @@ -118,18 +123,48 @@ export function createRegisteredUserTable(db: sqlite3.Database): Promise { } /* Insérer un utilisateur dans la table registered_user */ -export function insertUser( +export async function insertUser( db: sqlite3.Database, login: string, email: string, password: string, permissions: number, -) { +): Promise { 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; + } + } +} - return runDB(db, insertUserQuery, [login, email, password, permissions]); +/* Vérifier si un utilisateur existe dans la table registered_user */ +export async function verifyUser( + db: sqlite3.Database, + login: string, +): Promise { + const verifyUserQuery = `SELECT login, password FROM registered_user WHERE login = ?`; + + const res = await getDB(db, verifyUserQuery, [login]); + + if (!res) { + return null; + } else { + return res; + } } +export type User = { + login: string; + password: string; +}; + /* Modifier le login d'un utilisateur dans la table registered_user */ export function updateUserLogin( db: sqlite3.Database, diff --git a/src/server.ts b/src/server.ts index d93d053..be97bb0 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,31 +1,30 @@ -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 { Type, TypeBoxTypeProvider } from "@fastify/type-provider-typebox"; import Fastify, { FastifyReply } from "fastify"; import { nanoid } from "nanoid"; -import { allocateBuffer, getRunner } from "runner"; +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); -//let updates: Update[] = []; -//let doc = Text.of(["foo"]); - type Room = { sockets: WebSocket[]; updates: Update[]; doc: Text; -} +}; const rooms: Record = {}; @@ -43,29 +42,47 @@ const fastify = Fastify({ 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", }); -fastify.register(websocket); +declare module "fastify" { + interface Session { + userKey: string | null; + } +} -fastify.register(async function (fastify: Fastify) { +fastify.register(websocket); +fastify.register(async function(fastify: Fastify) { fastify.get( "/live/:roomId", { schema: { - params: Type.Object({ - roomId: Type.String(), - }) + params: Type.Object({ + roomId: Type.String(), + }), }, - websocket: true + websocket: true, }, (socket, request) => { const { roomId } = request.params; let room = rooms[roomId]; - if(!room){ + if (!room) { room = { sockets: [], updates: [], - doc: Text.of(['']) + doc: Text.of([""]), }; rooms[roomId] = room; } @@ -102,16 +119,33 @@ fastify.register(async function (fastify: Fastify) { 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("/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: { @@ -168,12 +202,58 @@ fastify.post( }, }, async (request, reply) => { - const { login, email, password, permissions } = request.body; - db.insertUser(database, login, email, password, permissions); - reply.send({ success: true }); + 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", @@ -194,7 +274,7 @@ fastify.put( const { newLogin } = request.body; db.updateUserLogin(database, id, newLogin); reply.send({ success: true }); - }, + } ); /* Route pour mettre à jour le mot de passe d'un utilisateur */ @@ -215,9 +295,13 @@ fastify.put( async (request, reply) => { const { id } = request.params; const { newPassword } = request.body; - db.updateUserPassword(database, id, newPassword); + + 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 */ @@ -240,7 +324,7 @@ fastify.put( const { newPermissions } = request.body; await db.updateUserPermissions(database, id, newPermissions); reply.send({ success: true }); - }, + } ); /* Route pour mettre à jour l'email d'un utilisateur */ @@ -289,7 +373,7 @@ fastify.delete( const { id } = request.params; await db.deleteUserById(database, id); reply.send({ success: true }); - }, + } ); /* Route pour supprimer un utilisateur par son login */ @@ -306,7 +390,7 @@ fastify.delete( const { login } = request.params; await db.deleteUserByLogin(database, login); reply.send({ success: true }); - }, + } ); /* Route pour supprimer tous les utilisateurs */ @@ -335,9 +419,11 @@ fastify.get( }, async (request, reply) => { const { id } = request.params; - const user = await db.selectUserById(database, id); - reply.send(user); - }, + if (request.session.userKey) { + const user = await db.selectUserById(database, id); + reply.send(user); + } + } ); /* Route pour récupérer un utilisateur par son login */ @@ -354,7 +440,7 @@ fastify.get( const { login } = request.params; const user = await db.selectUserByLogin(database, login); reply.send(user); - }, + } ); /* Route pour créer un work */ @@ -375,7 +461,7 @@ fastify.post( 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 */ @@ -406,7 +492,7 @@ fastify.delete( const { id } = request.params; db.deleteWork(database, id); reply.send({ success: true }); - }, + } ); /* Route pour récupérer un work par son Link */ @@ -445,55 +531,6 @@ fastify.get( }, ); -/* 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) { @@ -531,4 +568,4 @@ async function forwardOutput() { } /* Lancer le serveur et la fonction forwardOutput sur le même thread en parallèle */ -await Promise.all([fastify.listen({ port: 3000 }), forwardOutput()]); +await Promise.all([fastify.listen({ port: 3000, host: '0.0.0.0' }), forwardOutput()]); diff --git a/tsconfig.json b/tsconfig.json index 2558e1d..4accc64 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,15 @@ { "compilerOptions": { "baseUrl": "./src", + "outDir": "./dist", "types": ["bun-types"], "lib": ["esnext"], "module": "esnext", "target": "esnext", - "moduleResolution": "bundler", - "noEmit": true, - "allowImportingTsExtensions": true, + "moduleResolution": "Node", + "noEmit": false, "moduleDetection": "force", "strict": true, @@ -18,5 +18,8 @@ "composite": true, "downlevelIteration": true, "allowSyntheticDefaultImports": true + }, + "tsc-alias": { + "resolveFullPaths": true } }