From f499de5087b56ffa89e17aa3a5a8f8d3b51b1809 Mon Sep 17 00:00:00 2001 From: etienne Date: Tue, 13 Feb 2024 19:42:55 +0100 Subject: [PATCH 1/4] Add routes for single user and update socket functionality --- backend/src/index.ts | 2 + backend/src/models/single_user.ts | 30 ++++++++ backend/src/routes/single_user.ts | 74 +++++++++++++++++++ backend/src/routes/socket.ts | 25 +++---- docker-compose.yml | 1 - frontend/src/app/chat/[roomId]/page.tsx | 5 ++ frontend/src/components/Room/AddRoomPanel.tsx | 1 - frontend/src/components/Room/RoomCard.tsx | 3 +- frontend/src/components/Room/RoomSideBar.tsx | 11 ++- frontend/src/contexts/SocketContext.tsx | 3 +- 10 files changed, 132 insertions(+), 23 deletions(-) create mode 100644 backend/src/models/single_user.ts create mode 100644 backend/src/routes/single_user.ts diff --git a/backend/src/index.ts b/backend/src/index.ts index cd19cb9..8614484 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -3,6 +3,7 @@ import mongoose from "mongoose"; import { router as roomRoutes } from "./routes/roomRoutes.js"; import { router as messageRoutes } from "./routes/messageRoutes.js"; import { router as userRoutes } from "./routes/userRoutes.js"; +import { router as SingleUser } from "./routes/single_user.js"; import { app, server } from "./routes/socket.js"; import { log } from "./utils/log.js"; import cors from "cors"; @@ -21,6 +22,7 @@ app.use(bodyParser.json()); app.use("/rooms", roomRoutes); app.use("/messages", messageRoutes); app.use("/user", userRoutes); +app.use("/single_user", SingleUser); server.listen(process.env.PORT || 4000, () => { log("SERVER RUNNING"); diff --git a/backend/src/models/single_user.ts b/backend/src/models/single_user.ts new file mode 100644 index 0000000..44d5548 --- /dev/null +++ b/backend/src/models/single_user.ts @@ -0,0 +1,30 @@ +import { Schema, model, Document, Model } from "mongoose"; + +interface ISingleUser extends Document { + username: string; + socketId: string; +} + +interface ISingleUserModel extends Model { + findAll(): Promise; +} + +const singleUserSchema = new Schema({ + username: { type: String, required: true, unique: true }, + socketId: { type: String, required: true }, +}); + +singleUserSchema.statics.findAll = function (): Promise { + return this.find().exec(); +}; + +singleUserSchema.statics.findOneByUsername = function ( + socketId: string +): Promise { + return this.findOne({ socketId }).exec(); +}; + +export const SingleUser = model( + "SingleUser", + singleUserSchema +); diff --git a/backend/src/routes/single_user.ts b/backend/src/routes/single_user.ts new file mode 100644 index 0000000..e66c0bb --- /dev/null +++ b/backend/src/routes/single_user.ts @@ -0,0 +1,74 @@ +import express, { Request, Response } from "express"; +export const router = express.Router(); +import { SingleUser } from "../models/single_user.js"; + +router.post("/newuser", async (req: Request, res: Response) => { + const { username, socketId } = req.body; + + console.log(req.body); // Log the request body + + try { + const existingUser = await SingleUser.findOne({ + $or: [{ username }, { socketId }], + }); + if (existingUser) { + return res + .status(400) + .json({ message: "Username or SocketId already exists" }); + } + + const newUser = await SingleUser.create({ + username, + socketId, + }); + + res.status(201).json(newUser); + } catch (error: any) { + console.error(error); // Log the entire error + res.status(500).json({ message: error.message }); + } +}); + +router.get("/", async (req: Request, res: Response) => { + try { + const users = await SingleUser.find(); + res.status(200).json(users); + } catch (error: any) { + res.status(500).json({ message: error.message }); + } +}); + +router.get("/:socketId", async (req: Request, res: Response) => { + const { socketId } = req.params; + + try { + const user = await SingleUser.findOne({ socketId }); + + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + res.status(200).json(user); + } catch (error: any) { + res.status(500).json({ message: error.message }); + } +}); + +router.put("/updateusername", async (req: Request, res: Response) => { + const { socketId, newUsername } = req.body; + + try { + const user = await SingleUser.findOne({ socketId }); + + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + user.username = newUsername; + const updatedUser = await user.save(); + + res.status(200).json(updatedUser); + } catch (error: any) { + res.status(500).json({ message: error.message }); + } +}); diff --git a/backend/src/routes/socket.ts b/backend/src/routes/socket.ts index badbe3d..1900a62 100644 --- a/backend/src/routes/socket.ts +++ b/backend/src/routes/socket.ts @@ -9,7 +9,6 @@ import express from "express"; import dotenv from "dotenv"; import { User } from "../models/user.js"; - export const app = express(); app.use(cors()); dotenv.config(); @@ -24,7 +23,6 @@ const io = new Server(server, { maxHttpBufferSize: 2e7, }); -let users: { [key: string]: string } = {}; mongoose .connect( `mongodb://${process.env.MONGODB_USER}:${process.env.MONGODB_USER_PASSWORD}@mongodb:27017/${process.env.MONGO_INITDB_DATABASE}` @@ -38,7 +36,6 @@ let userNames: { [key: string]: string } = {}; io.on("connection", (socket) => { // changer de nom socket.on("change_name", ({ newName, OldName, roomId }) => { - console.log("User changed name: ", newName); userNames[socket.id] = newName; socket.emit("name_changed", newName); @@ -114,7 +111,7 @@ io.on("connection", (socket) => { io.emit("users_response", roomUsers); log(`User with ID: ${socket.id} joined room: ${roomId}`); }); - + socket.on("send_message", async (data) => { io.emit("receive_message", data); @@ -142,7 +139,7 @@ io.on("connection", (socket) => { }); socket.on("user_joined", ({ username, roomId }) => { - socket.to(roomId).emit("receive_message", { + io.emit("receive_message", { text: username + " joined the room. 🗿", socketId: "Kurama-chat", roomId: roomId, @@ -150,7 +147,7 @@ io.on("connection", (socket) => { }); }); - socket.on("leave_room", ({ username, roomId }) => { + socket.on("leave_room", async ({ username, roomId }) => { socket.leave(roomId); if (roomUsers[roomId]) { roomUsers[roomId] = roomUsers[roomId].filter((id) => id !== socket.id); @@ -163,8 +160,8 @@ io.on("connection", (socket) => { systemMessage: true, }); - io.emit("users_response", roomUsers); - log(`User with ID: ${socket.id} left room: ${roomId}`); + socket.to(roomId).emit("users_response", roomUsers); + console.log(`User with ID: ${socket.id} left room: ${roomId}`); }); socket.on("quit_room", async ({ username, roomId }) => { @@ -180,7 +177,7 @@ io.on("connection", (socket) => { systemMessage: true, }); - io.emit("users_response", roomUsers); + socket.to(roomId).emit("users_response", roomUsers); if (roomUsers[roomId] && roomUsers[roomId].length === 0) { try { @@ -194,21 +191,21 @@ io.on("connection", (socket) => { } }); - socket.on("logout", ({ username, roomId }) => { - socket.leave(roomId); + socket.on("logout", async ({ username, roomId }) => { if (roomUsers[roomId]) { roomUsers[roomId] = roomUsers[roomId].filter((id) => id !== socket.id); } io.emit("receive_message", { - text: username + " left the room. ➡️🚪", + text: username + " disconnect. ❌", socketId: "Kurama-chat", roomId: roomId, systemMessage: true, }); - io.emit("users_response", roomUsers); + socket.to(roomId).emit("users_response", roomUsers); log(`User with ID: ${socket.id} left room: ${roomId}`); + socket.leave(roomId); delete userNames[socket.id]; }); @@ -223,8 +220,8 @@ io.on("connection", (socket) => { roomId: roomId, systemMessage: true, }); + socket.to(roomId).emit("users_response", roomUsers); } } - io.emit("users_response", roomUsers); }); }); diff --git a/docker-compose.yml b/docker-compose.yml index f311137..a51ae12 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,7 +49,6 @@ services: - MONGODB_USER_PASSWORD=${MONGODB_USER_PASSWORD} - PORT=${PORT} - DEBUG=${DEBUG} - - ORIGIN_URL=${ORIGIN_URL} ports: - 4000:4000 diff --git a/frontend/src/app/chat/[roomId]/page.tsx b/frontend/src/app/chat/[roomId]/page.tsx index ee63d59..e92f092 100644 --- a/frontend/src/app/chat/[roomId]/page.tsx +++ b/frontend/src/app/chat/[roomId]/page.tsx @@ -19,6 +19,11 @@ function Page() { roomId: roomId, }); socket?.emit("join_room", roomId); + socket?.emit("change_name", { + newName: username, + OldName: username, + roomId: "", + }); }, []); return ( diff --git a/frontend/src/components/Room/AddRoomPanel.tsx b/frontend/src/components/Room/AddRoomPanel.tsx index 85174ee..7f3b1c4 100644 --- a/frontend/src/components/Room/AddRoomPanel.tsx +++ b/frontend/src/components/Room/AddRoomPanel.tsx @@ -25,7 +25,6 @@ function AddRoomPanel({ hideAddRoomPanel }: any) { return; } - // Fetch room data from server const response = await fetch( process.env.NEXT_PUBLIC_BASE_URL + `rooms/${joinId}` ); diff --git a/frontend/src/components/Room/RoomCard.tsx b/frontend/src/components/Room/RoomCard.tsx index 854baa8..8d0e789 100644 --- a/frontend/src/components/Room/RoomCard.tsx +++ b/frontend/src/components/Room/RoomCard.tsx @@ -8,12 +8,13 @@ import Avatar from "react-avatar"; import { ImExit } from "react-icons/im"; import { useSocket } from "@/contexts/SocketContext"; import { useRouter } from "next/navigation"; +import { useUser } from "@/contexts/UserContext"; function RoomCard({ room, users }: { room: IRoom; users: string[] }) { const { roomId } = useParams(); const { myRooms, setMyRooms } = useRoom(); const { socket } = useSocket(); - const username = localStorage.getItem("name"); + const { username } = useUser(); const router = useRouter(); const handleQuitRoom = () => { diff --git a/frontend/src/components/Room/RoomSideBar.tsx b/frontend/src/components/Room/RoomSideBar.tsx index 795f538..9660af3 100644 --- a/frontend/src/components/Room/RoomSideBar.tsx +++ b/frontend/src/components/Room/RoomSideBar.tsx @@ -8,25 +8,28 @@ import { BiMessageAdd } from "react-icons/bi"; import AddRoomPanel from "./AddRoomPanel"; import ThemeSwitcher from "../shared/themeswitcher"; import { useRouter } from "next/navigation"; +import { useUser } from "@/contexts/UserContext"; function RoomSideBar() { const [showAddRoomPanel, setShowAddRoomPanel] = useState(false); const { rooms, myRooms, currentRoomId, setCurrentRoomId } = useRoom(); const { socket, roomUsers } = useSocket(); const router = useRouter(); + const { username } = useUser(); const hideAddRoomPanel = () => setShowAddRoomPanel(false); const handleRoomClick = (newRoomId: string) => { - if (currentRoomId) { - socket?.emit("leave_room", currentRoomId); + if (currentRoomId !== newRoomId) { + socket?.emit("leave_room", { username: username, roomId: currentRoomId }); + } else { + console.log("No current room to leave"); } setCurrentRoomId(newRoomId); }; const logout = () => { - let username = localStorage.getItem("name"); - socket?.emit("logout", { username, currentRoomId }); + socket?.emit("logout", { username: username, roomId: currentRoomId }); router.push("/"); }; diff --git a/frontend/src/contexts/SocketContext.tsx b/frontend/src/contexts/SocketContext.tsx index a8d7d5e..bb9d530 100644 --- a/frontend/src/contexts/SocketContext.tsx +++ b/frontend/src/contexts/SocketContext.tsx @@ -39,7 +39,7 @@ export default function SocketProvider({ useEffect(() => { if (!username) { - router.replace("/"); + router.push("/"); return; } if (!socket) { @@ -55,7 +55,6 @@ export default function SocketProvider({ }); }); socket.on("users_response", (data) => setRoomUsers(data)); - socket?.emit("change_name", username); setSocket(socket); } }, []); From 1607a87c4048ede5554792498a114ff14c6c644a Mon Sep 17 00:00:00 2001 From: Akkiristo Date: Tue, 13 Feb 2024 22:00:55 +0100 Subject: [PATCH 2/4] Refactor socket event handlers and add join room functionality --- backend/src/routes/socket.ts | 26 ++++++++++++++-- frontend/src/components/Chat/ChatFooter.tsx | 33 ++++++++++++++++++--- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/backend/src/routes/socket.ts b/backend/src/routes/socket.ts index 1900a62..fe3a1b9 100644 --- a/backend/src/routes/socket.ts +++ b/backend/src/routes/socket.ts @@ -77,10 +77,7 @@ io.on("connection", (socket) => { socket.on("delete_room", async (roomId) => { try { - // Delete the room from the database await Room.findByIdAndDelete(roomId); - - // Notify all clients that the room has been deleted io.emit("room_deleted", roomId); console.log(`Room ${roomId} deleted.`); @@ -112,6 +109,29 @@ io.on("connection", (socket) => { log(`User with ID: ${socket.id} joined room: ${roomId}`); }); + socket.on("join", async (roomId, username) => { + // Vérifie si la salle existe + let room = await Room.findById(roomId); + if (!room) { + // Si la salle n'existe pas, vous pouvez choisir de la créer ou d'envoyer une erreur + socket.emit("join_error", `Room ${roomId} does not exist.`); + return; + } + + // Rejoint la salle + socket.join(roomId); + roomUsers[roomId] = [...(roomUsers[roomId] ?? []), socket.id]; + + // Informe les autres utilisateurs de la salle + socket.to(roomId).emit("receive_message", { + text: `${username} has joined the room.`, + systemMessage: true, + }); + + // Confirme la jonction à l'utilisateur + socket.emit("room_joined", {roomName: room.name, roomId: roomId}); + }); + socket.on("send_message", async (data) => { io.emit("receive_message", data); diff --git a/frontend/src/components/Chat/ChatFooter.tsx b/frontend/src/components/Chat/ChatFooter.tsx index 4a18262..aedca94 100644 --- a/frontend/src/components/Chat/ChatFooter.tsx +++ b/frontend/src/components/Chat/ChatFooter.tsx @@ -63,10 +63,30 @@ function ChatFooter({ roomId }: { roomId: string }) { } }, [socket, roomId]); + useEffect(() => { + if (socket) { + socket.on("room_joined", (id, username) => { + toast.success(`Joined room: ${id.roomName}`); + setMyRooms((prevRooms) => [...prevRooms, { id: id.roomId, title: id.roomName }]); + }); + + socket.on("join_error", (errorMessage) => { + toast.error(errorMessage); + }); + + return () => { + socket.off("room_joined"); + socket.off("join_error"); + }; + } + }, [socket]); + + function handleCommand(commandString: string, socket: any) { const parts = commandString.substr(1).split(" "); const command = parts[0].toLowerCase(); const args = parts.slice(1); + switch (command) { case "nick": @@ -90,7 +110,7 @@ function ChatFooter({ roomId }: { roomId: string }) { let newRoom = { title: roomName, - id: newRoomId, // Use the constant + id: newRoomId, }; console.log("New room: ", newRoomId); setMyRooms([...myRooms, newRoom]); @@ -102,9 +122,14 @@ function ChatFooter({ roomId }: { roomId: string }) { socket?.emit("delete_room", roomId); toast.info("Deleting room..."); break; - case "join": - const joinParam = args.join(" "); - socket?.emit("join", joinParam); + case "join": + if (args.length === 0) { + toast.error("Please specify a room ID to join."); + } else { + const roomIdToJoin = args[0]; + const username = localStorage.getItem("name"); + socket.emit("join", roomIdToJoin, username); + } break; case "quit": const username = localStorage.getItem("name"); From e9c0635ed5d713ad44e1840daa582730ad0c8c56 Mon Sep 17 00:00:00 2001 From: Akkiristo Date: Tue, 13 Feb 2024 22:34:18 +0100 Subject: [PATCH 3/4] Add useRouter hook and update ChatFooter component --- frontend/src/components/Chat/ChatFooter.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/components/Chat/ChatFooter.tsx b/frontend/src/components/Chat/ChatFooter.tsx index aedca94..b634368 100644 --- a/frontend/src/components/Chat/ChatFooter.tsx +++ b/frontend/src/components/Chat/ChatFooter.tsx @@ -10,6 +10,7 @@ import 'react-toastify/dist/ReactToastify.css'; import { toast, ToastContainer } from 'react-toastify'; import { v4 as uuidv4 } from 'uuid'; import { useRoom } from "@/contexts/RoomContext"; +import { useRouter } from "next/navigation"; function ChatFooter({ roomId }: { roomId: string }) { @@ -26,6 +27,7 @@ function ChatFooter({ roomId }: { roomId: string }) { const [image, setImage] = useState(null); const [showToast, setShowToast] = useState(false); const [toastMessage, setToastMessage] = useState(""); + const router = useRouter(); const onEmojiPick = (emojiObj: any) => { setMessage((prevInput) => prevInput + emojiObj.emoji); inputRef.current.focus(); @@ -134,6 +136,8 @@ function ChatFooter({ roomId }: { roomId: string }) { case "quit": const username = localStorage.getItem("name"); socket?.emit("leave_room", username, roomId); + setMyRooms(myRooms.filter((room) => room.id !== roomId)); + router.push("/chat/1"); break; case "users": socket?.emit("users"); From 6c6ab9be7e5b63ecaf347622f5fd75e66f5c1535 Mon Sep 17 00:00:00 2001 From: etienne Date: Tue, 13 Feb 2024 23:19:32 +0100 Subject: [PATCH 4/4] Update docker-compose.yml to use version 3 and add support for HTTPS --- docker-compose.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index a51ae12..371a8f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: "2" +version: "3" services: traefik: image: traefik @@ -8,8 +8,10 @@ services: - "--providers.docker=true" - "--providers.docker.exposedbydefault=false" - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" ports: - "80:80" + - "443:443" - "8080:8080" volumes: - /var/run/docker.sock:/var/run/docker.sock @@ -64,7 +66,7 @@ services: labels: - "traefik.enable=true" - "traefik.http.routers.frontend.rule=HostRegexp(`{any:.*}`)" - - "traefik.http.routers.frontend.entrypoints=web" + - "traefik.http.routers.frontend.entrypoints=web,websecure" - "traefik.http.services.frontend.loadbalancer.server.port=3000" networks: - web