diff --git a/client/src/App.tsx b/client/src/App.tsx index 9693af50..82083e15 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -3,12 +3,18 @@ import GitHubCorner from "./components/GitHubCorner" import Toast from "./components/toast/Toast" import EditorPage from "./pages/EditorPage" import HomePage from "./pages/HomePage" +import Login from "./auth/Login" +import Register from "./auth/Register" +import Profile from "./pages/profile/Profile" const App = () => { return ( <> + } /> + } /> + } /> } /> } /> diff --git a/client/src/auth/Login.tsx b/client/src/auth/Login.tsx new file mode 100644 index 00000000..3ae4cb7e --- /dev/null +++ b/client/src/auth/Login.tsx @@ -0,0 +1,96 @@ +import logo from "@/assets/logo.svg" +import { useAppContext } from "@/context/AppContext"; +import axios from "axios"; +import { useState } from "react"; +import { toast } from "react-hot-toast" +import { useNavigate } from "react-router-dom"; + +function Login() { + const navigate = useNavigate(); + const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || "http://localhost:3000" + + const {setUserInfo} = useAppContext(); + + const [user, setUser] = useState({ + username: "", + password: "" + }); + + const validateForm = () => { + if (user.username.trim().length === 0) { + toast.error("Enter your username"); + } + if (user.password.trim().length === 0) { + toast.error("Enter your password"); + } + return user.username.trim().length > 0 && + user.password.trim().length > 0; + } + + const submitLogin = async (e: React.FormEvent, user: {username: string, password: string}) => { + e.preventDefault(); + validateForm(); + + try { + const res = await axios.post(BACKEND_URL + "/api/users/login", user) + + if(res.status === 201) { + toast.success("Login successful"); + setUserInfo({username: user.username, token: res.data.token, email: res.data.email}); + localStorage.setItem("userInfo", JSON.stringify({username: user.username, token: res.data.token, email: res.data.email})); + navigate("/profile"); + } + } catch (error: unknown) { + if(error instanceof axios.AxiosError && error.response && error.response.status === 400) { + const errorData = error.response.data as { message: string }; + toast.error(errorData.message); + } else { + toast.error("Login failed. Please try again."); + } + } + } + + return ( +
+
+ Logo +
+
{ + submitLogin(e, user); + }} + > +

Login

+ setUser({ ...user, username: e.target.value })} + className="w-full rounded-md border border-gray-500 bg-darkHover px-3 py-3 focus:outline-none" + /> + setUser({ ...user, password: e.target.value })} + className="w-full rounded-md border border-gray-500 bg-darkHover px-3 py-3 focus:outline-none" + /> + +
+
+ + Don't have an account? Register here. + +
+
+ ) +} + +export default Login \ No newline at end of file diff --git a/client/src/auth/Register.tsx b/client/src/auth/Register.tsx new file mode 100644 index 00000000..2b992a51 --- /dev/null +++ b/client/src/auth/Register.tsx @@ -0,0 +1,124 @@ +import logo from "@/assets/logo.svg" +import { useState } from "react"; +import { IoEye } from "react-icons/io5"; +import { IoEyeOff } from "react-icons/io5"; +import { toast } from "react-hot-toast"; +import axios, { AxiosError } from "axios"; +import { useNavigate } from "react-router-dom"; + + +function Register() { + const navigate = useNavigate(); + const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || "http://localhost:3000" + + const [user, setUser] = useState({ + username: "", + email: "", + password: "" + }); + const [showPassword, setShowPassword] = useState(false); + + const validateForm = () => { + if (user.username.trim().length === 0) { + toast.error("Enter your username"); + } + if (user.email.trim().length === 0) { + toast.error("Enter your email"); + } + if (user.password.trim().length === 0) { + toast.error("Enter your password"); + } + if (user.password.trim().length < 6) { + toast.error("Password must be at least 6 characters long"); + } + return user.username.trim().length > 0 && + user.email.trim().length > 0 && + user.password.trim().length >= 6; + } + + const submitRegisteration = async (e: React.FormEvent, user: {username: string, email: string, password: string}) => { + e.preventDefault(); + validateForm(); + + try { + const res = await axios.post(BACKEND_URL + "/api/users/register", user) + + if(res.status === 201) { + toast.success("Registration successful"); + navigate("/login"); + } + } catch (error: unknown) { + if(error instanceof AxiosError && error.response && error.response.status === 400) { + const errorData = error.response.data as { message: string }; + toast.error(errorData.message); + } else { + toast.error("Registration failed. Please try again."); + } + } + } + + return ( +
+
+ Logo +
+
{ + submitRegisteration(e, user); + }} + > +

Register

+ setUser({ ...user, username: e.target.value })} + className="w-full rounded-md border border-gray-500 bg-darkHover px-3 py-3 focus:outline-none" + /> + setUser({ ...user, email: e.target.value })} + className="w-full rounded-md border border-gray-500 bg-darkHover px-3 py-3 focus:outline-none" + /> +
+ setUser({ ...user, password: e.target.value })} + className="w-full rounded-md border border-gray-500 bg-darkHover px-3 py-3 focus:outline-none" + /> + { + showPassword ? + setShowPassword(!showPassword)} + /> : + setShowPassword(!showPassword)} + /> + } +
+ +
+
+ + Already have an account? Login here. + +
+
+ ) +} + +export default Register \ No newline at end of file diff --git a/client/src/components/cards/Card.tsx b/client/src/components/cards/Card.tsx new file mode 100644 index 00000000..b717272f --- /dev/null +++ b/client/src/components/cards/Card.tsx @@ -0,0 +1,78 @@ +import { useAppContext } from "@/context/AppContext"; +import { useSocket } from "@/context/SocketContext"; +import { MouseEvent, useEffect } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { USER_STATUS } from "@/types/user" +import { SocketEvent } from "@/types/socket"; +import toast from "react-hot-toast"; + + +function Card({data}: {data: {name: string, roomId: string} }) { + + const userInfo = localStorage.getItem("userInfo") ? JSON.parse(localStorage.getItem("userInfo") || "") : null; + + const location = useLocation() + const { currentUser, setCurrentUser, status, setStatus } = useAppContext() + const { socket } = useSocket() + + const navigate = useNavigate() + + const joinRoom = (e: MouseEvent) => { + + setCurrentUser({ ...currentUser, roomId: data.roomId }) + setCurrentUser({ ...currentUser, username: userInfo.name }) + + e.preventDefault() + if (status === USER_STATUS.ATTEMPTING_JOIN) return + + toast.loading("Joining room...") + setStatus(USER_STATUS.ATTEMPTING_JOIN) + socket.emit(SocketEvent.JOIN_REQUEST, currentUser) + } + + useEffect(() => { + if (status === USER_STATUS.DISCONNECTED && !socket.connected) { + socket.connect() + return + } + + const isRedirect = sessionStorage.getItem("redirect") || false + + if (status === USER_STATUS.JOINED && !isRedirect) { + const username = currentUser.username + sessionStorage.setItem("redirect", "true") + navigate(`/editor/${data.roomId}`, { + state: { + username, + }, + }) + } else if (status === USER_STATUS.JOINED && isRedirect) { + sessionStorage.removeItem("redirect") + setStatus(USER_STATUS.DISCONNECTED) + socket.disconnect() + socket.connect() + } + }, [currentUser, location.state?.redirect, navigate, setStatus, socket, status]) + + + return ( +
+ Card Image +

{data.name}

+

Room ID : {data.roomId}

+ + + +
+ ) +} + +export default Card \ No newline at end of file diff --git a/client/src/context/AppContext.tsx b/client/src/context/AppContext.tsx index 3b4ae706..6461343f 100644 --- a/client/src/context/AppContext.tsx +++ b/client/src/context/AppContext.tsx @@ -3,7 +3,7 @@ import { AppContext as AppContextType, DrawingData, } from "@/types/app" -import { RemoteUser, USER_STATUS, User } from "@/types/user" +import { RemoteUser, USER_STATUS, User, UserInfo } from "@/types/user" import { ReactNode, createContext, useContext, useState } from "react" const AppContext = createContext(null) @@ -25,6 +25,7 @@ function AppContextProvider({ children }: { children: ReactNode }) { username: "", roomId: "", }) + const [userInfo, setUserInfo] = useState(null) const [activityState, setActivityState] = useState( ACTIVITY_STATE.CODING, ) @@ -43,6 +44,8 @@ function AppContextProvider({ children }: { children: ReactNode }) { setActivityState, drawingData, setDrawingData, + userInfo, + setUserInfo, }} > {children} diff --git a/client/src/pages/profile/Profile.tsx b/client/src/pages/profile/Profile.tsx new file mode 100644 index 00000000..6909bd1d --- /dev/null +++ b/client/src/pages/profile/Profile.tsx @@ -0,0 +1,211 @@ +import React, { useEffect, useState } from 'react' +import logo from "@/assets/logo.svg" +import Card from '@/components/cards/Card'; +import axios from 'axios'; +import toast from 'react-hot-toast'; + +function Profile() { + + const token = localStorage.getItem("userInfo") ? JSON.parse(localStorage.getItem("userInfo") || "").token : null; + const userInfo = localStorage.getItem("userInfo") ? JSON.parse(localStorage.getItem("userInfo") || "") : null; + const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || "http://localhost:3000" + + const [newRoom, setNewRoom] = useState({ + name: "", + roomId: "" + }) + const [existingRoomId, setExistingRoomId] = useState(""); + const [openPopup, setOpenPopup] = useState(false); + const [openExistingRoomPopup, setOpenExistingRoomPopup] = useState(false); + + const [rooms, setRooms] = React.useState<{name: string, roomId: string}[]>([]); + + const fetchRooms = async () => { + try { + const fetchedRooms = await axios.get(BACKEND_URL + "/api/rooms/joined", { + headers: { + Authorization: `Bearer ${token}` + } + }); + setRooms(fetchedRooms.data.rooms); + } catch (error) { + toast.error('Failed to fetch rooms'); + } + }; + + useEffect(() => { + fetchRooms(); + }, [token, BACKEND_URL]); + + const createRoom = async (room: {name: string, roomId: string}) => { + try { + const res = await axios.post(BACKEND_URL + "/api/rooms/create", room, { + headers: { + Authorization: `Bearer ${token}` + } + }); + + if (res.status === 201) { + toast.success('Room created successfully!'); + setOpenPopup(false); + setNewRoom({name: "", roomId: ""}); + setRooms((prevRooms) => [...prevRooms, room]); + } + } catch (error: unknown) { + if (axios.isAxiosError(error) && error.response) { + toast.error(`Error: ${error.response.data.message}`); + } else { + toast.error('An unexpected error occurred.'); + } + } + + } + + const JoinExistingRoom = async (roomId: string) => { + if(roomId.trim() === "") { + toast.error("Room ID cannot be empty"); + return; + } + else if(roomId.length < 5) { + toast.error("Room ID must be at least 5 characters long"); + return; + } + try { + const res = await axios.post(BACKEND_URL + "/api/rooms/joinExistingRoom", {roomId}, { + headers: { + Authorization: `Bearer ${token}` + } + }); + if (res.status === 200) { + toast.success('Joined room successfully!'); + setOpenExistingRoomPopup(false); + setExistingRoomId(""); + fetchRooms(); + } + } catch (error: unknown) { + if (axios.isAxiosError(error) && error.response) { + toast.error(`Error: ${error.response.data.message}`); + } else { + toast.error('An unexpected error occurred.'); + } + } + } + + + + + return ( +
+
+
+ Logo +
+
+

+ welcome, {userInfo?.username} +

+
+
+
+ + + + +
+
+

Rooms

+
+ { + rooms.length === 0 ? ( +

No rooms joined yet.

+ ) : ( + rooms.map((room, index) => ( + + )) + ) + } +
+
+ {openPopup && ( +
+
+

Create New Room

+ setNewRoom({...newRoom, name: e.target.value})} + /> + setNewRoom({...newRoom, roomId: e.target.value})} + /> +
+ + +
+
+
+ )} + + {openExistingRoomPopup && ( +
+
+

Join Existing Room

+ setExistingRoomId(e.target.value)} + /> +
+ + +
+
+
+ )} + + +
+ ) +} + +export default Profile \ No newline at end of file diff --git a/client/src/types/app.ts b/client/src/types/app.ts index d932ff06..c422c518 100644 --- a/client/src/types/app.ts +++ b/client/src/types/app.ts @@ -1,5 +1,5 @@ import { StoreSnapshot, TLRecord } from "@tldraw/tldraw" -import { RemoteUser, User, USER_STATUS } from "./user" +import { RemoteUser, User, USER_STATUS, UserInfo } from "./user" type DrawingData = StoreSnapshot | null @@ -21,6 +21,8 @@ interface AppContext { setActivityState: (state: ACTIVITY_STATE) => void drawingData: DrawingData setDrawingData: (data: DrawingData) => void + userInfo: UserInfo | null + setUserInfo: (userInfo: UserInfo | null) => void } export { ACTIVITY_STATE } diff --git a/client/src/types/user.ts b/client/src/types/user.ts index 7891ad15..343c5cf1 100644 --- a/client/src/types/user.ts +++ b/client/src/types/user.ts @@ -8,6 +8,12 @@ interface User { roomId: string } +interface UserInfo { + username: string + email: string + token: string +} + interface RemoteUser extends User { status: USER_CONNECTION_STATUS cursorPosition: number @@ -27,4 +33,4 @@ enum USER_STATUS { DISCONNECTED = "disconnected", } -export { USER_CONNECTION_STATUS, USER_STATUS, RemoteUser, User } +export { USER_CONNECTION_STATUS, USER_STATUS, RemoteUser, User, UserInfo } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..de8c682c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,291 @@ +{ + "name": "Code-Sync", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "axios": "^1.13.2" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..c6bf7266 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "axios": "^1.13.2" + } +} diff --git a/server/package-lock.json b/server/package-lock.json index 3c325556..a168156f 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,14 +9,21 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "bcrypt": "^6.0.0", + "bcryptjs": "^3.0.3", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.21.2", + "express-async-handler": "^1.2.0", + "jsonwebtoken": "^9.0.3", + "mongodb": "^7.0.0", + "mongoose": "^9.0.1", "socket.io": "^4.7.3" }, "devDependencies": { "@types/dotenv": "^8.2.0", "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^18.15.11", "@types/socket.io": "^3.0.2", "nodemon": "^3.0.2", @@ -61,6 +68,15 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz", + "integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -162,12 +178,30 @@ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "18.19.33", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.33.tgz", @@ -219,6 +253,21 @@ "socket.io": "*" } }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -296,6 +345,29 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -350,6 +422,21 @@ "node": ">=8" } }, + "node_modules/bson": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.0.0.tgz", + "integrity": "sha512-Kwc6Wh4lQ5OmkqqKhYGKIuELXl+EPYSCObVE6bWsp1T/cGkOCBN0I8wF/T44BiuhHyNi1mmKVPXk60d41xZ7kw==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -520,6 +607,15 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -667,6 +763,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-async-handler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/express-async-handler/-/express-async-handler-1.2.0.tgz", + "integrity": "sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -915,11 +1017,110 @@ "node": ">=0.12.0" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.0.0.tgz", + "integrity": "sha512-RKhaOBSPN8L7y4yAgNhDT2602G5FD6QbOIISbjN9D6mjHPeqeg7K+EB5IGSU5o81/X2Gzm3ICnAvQW3x3OP8HA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -941,6 +1142,12 @@ "node": ">= 0.6" } }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -999,6 +1206,110 @@ "node": "*" } }, + "node_modules/mongodb": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", + "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^7.0.0", + "mongodb-connection-string-url": "^7.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": ">=7.0.0 <7.1.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.0.tgz", + "integrity": "sha512-irhhjRVLE20hbkRl4zpAYLnDMM+zIZnp0IDB9akAFFUZp/3XdOfwwddc7y6cNvF2WCEtfTYRwYbIfYa2kVY0og==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongoose": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.0.1.tgz", + "integrity": "sha512-aHPfQx2YX5UwAmMVud7OD4lIz9AEO4jI+oDnRh3lPZq9lrKTiHmOzszVffDMyQHXvrf4NXsJ34kpmAhyYAZGbw==", + "license": "MIT", + "dependencies": { + "kareem": "3.0.0", + "mongodb": "~7.0", + "mpath": "0.9.0", + "mquery": "6.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz", + "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==", + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1012,6 +1323,26 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/nodemon": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.2.tgz", @@ -1160,6 +1491,15 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -1236,7 +1576,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -1335,6 +1674,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1448,6 +1793,15 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1500,6 +1854,18 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -1609,6 +1975,28 @@ "node": ">= 0.8" } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ws": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", @@ -1632,8 +2020,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yn": { "version": "3.1.1", diff --git a/server/package.json b/server/package.json index 1975d25c..65e615c6 100644 --- a/server/package.json +++ b/server/package.json @@ -12,14 +12,21 @@ "author": "", "license": "ISC", "dependencies": { + "bcrypt": "^6.0.0", + "bcryptjs": "^3.0.3", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.21.2", + "express-async-handler": "^1.2.0", + "jsonwebtoken": "^9.0.3", + "mongodb": "^7.0.0", + "mongoose": "^9.0.1", "socket.io": "^4.7.3" }, "devDependencies": { "@types/dotenv": "^8.2.0", "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^18.15.11", "@types/socket.io": "^3.0.2", "nodemon": "^3.0.2", diff --git a/server/src/Middleware/authMiddleware.ts b/server/src/Middleware/authMiddleware.ts new file mode 100644 index 00000000..87c9c8c1 --- /dev/null +++ b/server/src/Middleware/authMiddleware.ts @@ -0,0 +1,34 @@ +import jwt from "jsonwebtoken"; +import User from "../Models/users"; +import asyncHandler from "express-async-handler"; + +export const protect = asyncHandler(async (req: any, res: any, next) => { + let token; + + if ( + req.headers.authorization && + req.headers.authorization.startsWith("Bearer") + ) { + try { + token = req.headers.authorization.split(" ")[1]; + + const jwtSecret = process.env.JWT_SECRET; + if (!jwtSecret) { + throw new Error("JWT_SECRET is not defined"); + } + const decoded = jwt.verify(token, jwtSecret) as { id: string }; + req.user = await User.findById(decoded.id).select("-password"); + req.userId = decoded.id; + next(); + } catch (error) { + console.error(error); + res.status(401); + throw new Error("Not authorized, token failed"); + } + } + + if (!token) { + res.status(401); + throw new Error("Not authorized, no token"); + } +}); \ No newline at end of file diff --git a/server/src/Models/roomId.ts b/server/src/Models/roomId.ts new file mode 100644 index 00000000..fe23a1fc --- /dev/null +++ b/server/src/Models/roomId.ts @@ -0,0 +1,31 @@ +import mongoose from "mongoose"; + +const roomIDSchema = new mongoose.Schema({ + roomId: { + type: String, + required: true, + trim: true, + minlength: 3, + maxlength: 30 + }, + roomName: { + type: String, + required: true, + trim: true, + minlength: 3, + maxlength: 50 + }, + createdBy: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true + }, + members: [{ + type: mongoose.Schema.Types.ObjectId, + ref: "User" + }] +}, { timestamps: true }); + +const RoomID = mongoose.model('RoomID', roomIDSchema); + +export default RoomID; \ No newline at end of file diff --git a/server/src/Models/users.ts b/server/src/Models/users.ts new file mode 100644 index 00000000..d96371c7 --- /dev/null +++ b/server/src/Models/users.ts @@ -0,0 +1,30 @@ +import mongoose from "mongoose"; + +const userSchema = new mongoose.Schema({ + username: { + type: String, + required: true, + unique: true, + trim: true, + minlength: 3, + maxlength: 30 + }, + email: { + type: String, + required: true, + unique: true, + trim: true, + lowercase: true, + match: [/.+@.+\..+/, 'Please enter a valid email address'] + }, + password: { + type: String, + required: true, + minlength: 6, + select: false + }, +}, { timestamps: true }); + +const User = mongoose.model('User', userSchema); + +export default User; \ No newline at end of file diff --git a/server/src/config/GenerateToken.ts b/server/src/config/GenerateToken.ts new file mode 100644 index 00000000..bec3faa5 --- /dev/null +++ b/server/src/config/GenerateToken.ts @@ -0,0 +1,8 @@ +import jsonwebToken from "jsonwebtoken"; + +export const generateToken = (id: string) => { + return jsonwebToken.sign({ id }, process.env.JWT_SECRET as string, { + expiresIn: "30d", + }); +}; + diff --git a/server/src/config/HashedPassword.ts b/server/src/config/HashedPassword.ts new file mode 100644 index 00000000..629b2063 --- /dev/null +++ b/server/src/config/HashedPassword.ts @@ -0,0 +1,6 @@ +import bcrypt from "bcryptjs"; + +export const hashPassword = async (password: string) => { + const salt = await bcrypt.genSalt(10); + return await bcrypt.hash(password, salt); +}; diff --git a/server/src/controller/roomController.ts b/server/src/controller/roomController.ts new file mode 100644 index 00000000..b49cdcf9 --- /dev/null +++ b/server/src/controller/roomController.ts @@ -0,0 +1,77 @@ +import RoomID from "../Models/roomId"; + +export const CreateRoom = async (req: any, res: any) => { + try { + const { name, roomId } = req.body; + + // Check if roomId already exists + const existingRoom = await RoomID.findOne({ roomId }); + if (existingRoom) { + return res.status(400).json({ message: "Room ID already exists" }); + } + + // Create new room + const newRoom = new RoomID({ roomName: name, roomId, createdBy: req.userId }); + await newRoom.save(); + res.status(201).json({ message: "Room created successfully", room: newRoom }); + } catch (error) { + res.status(500).json({ message: "Server error" }); + } +} + +export const GetJoinedRooms = async (req: any, res: any) => { + try { + const rooms = await RoomID.find({ createdBy: req.userId }); + + const memberInRooms = await RoomID.find({ members: req.userId }); + + rooms.push(...memberInRooms.filter(room => !rooms.includes(room))); + + const formattedRooms = rooms.map(room => ({ + name: room.roomName, + roomId: room.roomId + })); + res.status(200).json({ rooms: formattedRooms }); + } catch (error) { + res.status(500).json({ message: "Server error" }); + } +} + +export const JoinExistingRoom = async (req: any, res: any) => { + try { + const { roomId } = req.body; + const room = await RoomID.findOne({ roomId }); + if (!room) { + return res.status(404).json({ message: "Room not found" }); + } + + // Add user to room members if not already a member + if (room.members.includes(req.userId)) { + return res.status(400).json({ message: "Already a member of the room" }); + } + + room.members.push(req.userId); + await room.save(); + + res.status(200).json({ message: "Joined room successfully", room: { name: room.roomName, roomId: room.roomId } }); + } catch (error) { + res.status(500).json({ message: "Server error" }); + } +} + +export const LeaveRoom = async (req: any, res: any) => { + try { + const { roomId } = req.body; + const room = await RoomID.findOne({ roomId }); + if (!room) { + return res.status(404).json({ message: "Room not found" }); + } + + // Remove user from room members + room.members = room.members.filter(memberId => memberId.toString() !== req.userId); + await room.save(); + res.status(200).json({ message: "Left room successfully" }); + } catch (error) { + res.status(500).json({ message: "Server error" }); + } +} \ No newline at end of file diff --git a/server/src/controller/userController.ts b/server/src/controller/userController.ts new file mode 100644 index 00000000..a764b06a --- /dev/null +++ b/server/src/controller/userController.ts @@ -0,0 +1,65 @@ +import { generateToken } from "../config/GenerateToken"; +import { hashPassword } from "../config/HashedPassword"; +import User from "../Models/users"; +import bcrypt from "bcryptjs"; + +export const RegisterUser = async (req: any, res: any) => { + try { + const { username, email, password } = req.body; + + // Check if user already exists + const existingUser = await User.findOne({ email }); + if (existingUser) { + return res.status(400).json({ message: "User already exists" }); + } + + // check if username already exists + const existingUsername = await User.findOne({ username }); + if (existingUsername) { + return res.status(400).json({ message: "Username already taken" }); + } + + + const hashedPassword = await hashPassword(password); + + // Create new user + const newUser = new User({ username, email, password: hashedPassword }); + await newUser.save(); + + res.status(201).json({ message: "User registered successfully" }); + + } catch (error) { + res.status(500).json({ message: "Server Error", error }); + } +} + +export const LoginUser = async (req: any, res: any) => { + try { + const { username, password } = req.body; + + if(!username || !password) { + return res.status(400).json({ message: "Please provide username and password" }); + } + + const user = await User.findOne({ username }).select("+password"); + + if (!user) { + return res.status(400).json({ message: "Invalid credentials", }); + } + + const isPasswordValid = await bcrypt.compare(password, user.password); + + if (!isPasswordValid) { + return res.status(400).json({ message: "Invalid credentials" }); + } + + const email = user.email; + + const token = generateToken(user._id.toString()); + + res.status(201).json({ message: "Login successful", token, email }); + + } catch (error) { + res.status(500).json({ message: "Server Error", error }); + } +} \ No newline at end of file diff --git a/server/src/db/db.ts b/server/src/db/db.ts new file mode 100644 index 00000000..413f9a45 --- /dev/null +++ b/server/src/db/db.ts @@ -0,0 +1,13 @@ +import dotenv from 'dotenv'; +import mongoose from 'mongoose'; + +dotenv.config(); + +export const connectDB = async () => { + try { + const conn = await mongoose.connect(process.env.BACKEND_URL as string); + } catch (error) { + console.error('MongoDB connection error:', error); + process.exit(1); + } +}; diff --git a/server/src/routes/RoomRoutes.ts b/server/src/routes/RoomRoutes.ts new file mode 100644 index 00000000..f4505b34 --- /dev/null +++ b/server/src/routes/RoomRoutes.ts @@ -0,0 +1,15 @@ +import { Router } from "express"; +import { protect } from "../Middleware/authMiddleware"; +import { CreateRoom, GetJoinedRooms, JoinExistingRoom, LeaveRoom } from "../controller/roomController"; + +const router = Router(); + +router.post("/create", protect, CreateRoom); + +router.get("/joined", protect, GetJoinedRooms); + +router.post("/joinExistingRoom", protect, JoinExistingRoom); + +router.post("/leave", protect, LeaveRoom); + +export default router; \ No newline at end of file diff --git a/server/src/routes/userRoutes.ts b/server/src/routes/userRoutes.ts new file mode 100644 index 00000000..b867b7d9 --- /dev/null +++ b/server/src/routes/userRoutes.ts @@ -0,0 +1,12 @@ +import { Router } from "express"; +import { LoginUser, RegisterUser } from "../controller/userController"; + +const router = Router(); + +// register user routes here + +router.post("/register", RegisterUser); + +router.post("/login", LoginUser); + +export default router; \ No newline at end of file diff --git a/server/src/server.ts b/server/src/server.ts index 64291dc3..be42a0ea 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -7,6 +7,11 @@ import { USER_CONNECTION_STATUS, User } from "./types/user" import { Server } from "socket.io" import path from "path" +import {connectDB} from "./db/db" + +import userRoutes from "./routes/userRoutes" +import RoomRoutes from "./routes/RoomRoutes"; + dotenv.config() const app = express() @@ -287,11 +292,16 @@ io.on("connection", (socket) => { const PORT = process.env.PORT || 3000 +connectDB(); + app.get("/", (req: Request, res: Response) => { // Send the index.html file res.sendFile(path.join(__dirname, "..", "public", "index.html")) }) +app.use("/api/users", userRoutes) +app.use("/api/rooms", RoomRoutes); + server.listen(PORT, () => { console.log(`Listening on port ${PORT}`) -}) +}) \ No newline at end of file