diff --git a/backend/controllers/Chat.ts b/backend/controllers/Chat.ts index 1c76c5a..cec9ce6 100644 --- a/backend/controllers/Chat.ts +++ b/backend/controllers/Chat.ts @@ -1,3 +1,6 @@ +import type { Socket } from "socket.io"; +import ChatMessagePayload from "../../common/requests/ChatMessagePayload"; + class ChatController { /** * Broadcasts a message to all connected clients @@ -6,7 +9,7 @@ class ChatController { * @param data The message data * @param socket The client socket */ - public static async broadcastMessage(data: ChatMessagePayload, socket: SocketIO.Socket) { + public static async broadcastMessage(data: ChatMessagePayload, socket: Socket) { // TODO: Broadcast the message to all clients /** * VALIDATION diff --git a/backend/index.ts b/backend/index.ts index 9bfbe33..2164a12 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -22,6 +22,8 @@ WSS.io.on("connection", (socket: Socket) => { const userAgent = socket.handshake.headers["user-agent"]; console.log(`Socket connected from ${ip} using ${userAgent}`); + WSS.updateClassement(socket); + socket.on("place-pixel", (data) => CanvasController.placePixel(data, socket)); socket.on("message", (data) => ChatController.broadcastMessage(data, socket)); diff --git a/backend/package.json b/backend/package.json index 5e30315..da401e1 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,7 +8,8 @@ "build": "tsc", "start": "node index.js", "lint": "eslint . --ext .ts", - "format": "prettier --write ." + "format": "prettier --write .", + "seed": "ts-node prisma/seed.ts" }, "author": "", "license": "ISC", diff --git a/backend/prisma/migrations/20240218154047_add_unique_emails/migration.sql b/backend/prisma/migrations/20240218154047_add_unique_emails/migration.sql new file mode 100644 index 0000000..8b30391 --- /dev/null +++ b/backend/prisma/migrations/20240218154047_add_unique_emails/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[devinciEmail]` on the table `Account` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX `Account_devinciEmail_key` ON `Account`(`devinciEmail`); diff --git a/backend/prisma/migrations/20240218154912_add_defaults/migration.sql b/backend/prisma/migrations/20240218154912_add_defaults/migration.sql new file mode 100644 index 0000000..ed9a3f2 --- /dev/null +++ b/backend/prisma/migrations/20240218154912_add_defaults/migration.sql @@ -0,0 +1,15 @@ +/* + Warnings: + + - The `lastPixelTime` column on the `Account` table would be dropped and recreated. This will lead to data loss if there is data in the column. + +*/ +-- AlterTable +ALTER TABLE `Account` MODIFY `isMuted` BOOLEAN NOT NULL DEFAULT false, + MODIFY `isBanned` BOOLEAN NOT NULL DEFAULT false, + MODIFY `isAdmin` BOOLEAN NOT NULL DEFAULT false, + MODIFY `placedPixels` INTEGER NOT NULL DEFAULT 0, + MODIFY `timeAlive` INTEGER NULL, + DROP COLUMN `lastPixelTime`, + ADD COLUMN `lastPixelTime` DATETIME(3) NULL, + MODIFY `lastSentMessageTimes` JSON NULL; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 6249c60..094660b 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -12,14 +12,14 @@ datasource db { model Account { id Int @id @default(autoincrement()) - devinciEmail String - isMuted Boolean - isBanned Boolean - isAdmin Boolean - placedPixels Int - timeAlive Int - lastPixelTime Int - lastSentMessageTimes Json + devinciEmail String @unique() + isMuted Boolean @default(false) + isBanned Boolean @default(false) + isAdmin Boolean @default(false) + placedPixels Int @default(0) + timeAlive Int? + lastPixelTime DateTime? + lastSentMessageTimes Json? } model LogEntry { diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts new file mode 100644 index 0000000..9eec5eb --- /dev/null +++ b/backend/prisma/seed.ts @@ -0,0 +1,34 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +async function main() { + const userCount = Number(process.argv.find(arg => arg.startsWith("userCount="))?.replace(/^userCount=/, "")) ?? 10; + const usersPromise = [] as Promise[]; + for(let i = 1; i <= userCount; i++) { + const user = prisma.account.upsert({ + where: { + devinciEmail: `user_${i}@edu.devinci.fr` + }, + update: { + placedPixels: Math.floor(Math.random() * 3000) + }, + create: { + devinciEmail: `user_${i}@edu.devinci.fr`, + placedPixels: Math.floor(Math.random() * 3000) + } + }); + usersPromise.push(user); + } + await Promise.all(usersPromise); +} + +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); \ No newline at end of file diff --git a/backend/server/Websocket.ts b/backend/server/Websocket.ts index b77be76..25a74dd 100644 --- a/backend/server/Websocket.ts +++ b/backend/server/Websocket.ts @@ -1,11 +1,38 @@ import type http from "http"; import SocketIO from "socket.io"; +import type { Server, Socket } from "socket.io"; +import { PrismaClient } from "@prisma/client"; + +const client = new PrismaClient(); class WSS { - public static io: SocketIO.Server; + public static io: Server; public static init(server: http.Server) { - WSS.io = new SocketIO.Server(server); + WSS.io = new SocketIO.Server(server, { + cors: { + origin: process.env.NODE_ENV === "production" ? undefined : "http://localhost:5173" + } + }); + } + + /** + * Sends an 'updateClassement' event to one socket if provided. \ + * If no socket is provided, broadcast the event to all connected clients. + * @param socket + */ + static async updateClassement(socket?: Socket) { + const classement = await client.account.findMany({ + select: { + devinciEmail: true, + placedPixels: true + }, + orderBy: { + placedPixels: "desc" + } + }); + if(!socket) this.io.emit("classementUpdate", classement); + else socket.emit("classementUpdate", classement); } } diff --git a/common/interfaces/classementItem.interface.ts b/common/interfaces/classementItem.interface.ts new file mode 100644 index 0000000..ade0fc0 --- /dev/null +++ b/common/interfaces/classementItem.interface.ts @@ -0,0 +1,6 @@ +interface classementItem { + devinciEmail: string; + placedPixels: number; +} + +export default classementItem \ No newline at end of file diff --git a/common/requests/ChatMessagePayload.ts b/common/requests/ChatMessagePayload.ts new file mode 100644 index 0000000..61cb538 --- /dev/null +++ b/common/requests/ChatMessagePayload.ts @@ -0,0 +1,5 @@ +interface ChatMessagePayload { + [index: string] : Record +} + +export default ChatMessagePayload \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 84f81df..20f7b38 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,8 +1,39 @@ // import { useState } from 'react' +import { useEffect, useState } from 'react'; import './App.css' import LoginComponent from './pages/login' +import { socket } from './socket'; +import classementItem from '../../common/interfaces/classementItem.interface' function App() { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [classement, setClassement] = useState([]) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [isConnected, setIsConnected] = useState(socket.connected); + + useEffect(() => { + function onConnect() { + setIsConnected(true); + } + + function onDisconnect() { + setIsConnected(false); + } + + function onclassementUpdate(data: classementItem[]) { + setClassement(data) + } + + socket.on('connect', onConnect); + socket.on('disconnect', onDisconnect); + socket.on('classementUpdate', onclassementUpdate); + + return () => { + socket.off('connect', onConnect); + socket.off('disconnect', onDisconnect); + socket.off('classementUpdate', onclassementUpdate); + }; + }, []); // affichage (render) return ( diff --git a/frontend/src/socket.ts b/frontend/src/socket.ts new file mode 100644 index 0000000..6ae7197 --- /dev/null +++ b/frontend/src/socket.ts @@ -0,0 +1,6 @@ +import { io } from 'socket.io-client'; + +// "undefined" means the URL will be computed from the `window.location` object +const URL = process.env.NODE_ENV === 'production' ? undefined : 'http://localhost:3000'; + +export const socket = io(URL); \ No newline at end of file