From 95c779c112799769e130f70c007c719ac7807b1b Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 2 Oct 2024 14:33:45 +0800 Subject: [PATCH 01/82] Add matching page --- frontend/src/App.tsx | 8 +++- frontend/src/assets/matching.svg | 9 ++++ frontend/src/components/Timer/index.tsx | 47 ++++++++++++++++++++ frontend/src/pages/Matching/index.module.css | 10 +++++ frontend/src/pages/Matching/index.tsx | 32 +++++++++++++ 5 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 frontend/src/assets/matching.svg create mode 100644 frontend/src/components/Timer/index.tsx create mode 100644 frontend/src/pages/Matching/index.module.css create mode 100644 frontend/src/pages/Matching/index.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6de5f61c5b..86804a8a0d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,6 +11,7 @@ import Home from "./pages/Home"; import SignUp from "./pages/SignUp"; import LogIn from "./pages/LogIn"; import ProtectedRoutes from "./components/ProtectedRoutes"; +import Matching from "./pages/Matching"; function App() { return ( @@ -27,10 +28,13 @@ function App() { } /> + + } /> + } /> - }> - }> + }> + }> ); diff --git a/frontend/src/assets/matching.svg b/frontend/src/assets/matching.svg new file mode 100644 index 0000000000..fbb0724816 --- /dev/null +++ b/frontend/src/assets/matching.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/components/Timer/index.tsx b/frontend/src/components/Timer/index.tsx new file mode 100644 index 0000000000..bb712e025d --- /dev/null +++ b/frontend/src/components/Timer/index.tsx @@ -0,0 +1,47 @@ +import { + Box, + CircularProgress, + CircularProgressProps, + Typography, +} from "@mui/material"; + +type TimerProps = { totalTime: number; timeLeft: number }; + +const Timer: React.FC = (props) => { + const { totalTime, timeLeft } = props; + const percentage = (timeLeft / totalTime) * 100; + return ( + + ({ color: theme.palette.grey[200] })} + /> + + + {timeLeft} + + + ); +}; + +export default Timer; diff --git a/frontend/src/pages/Matching/index.module.css b/frontend/src/pages/Matching/index.module.css new file mode 100644 index 0000000000..806d506cc5 --- /dev/null +++ b/frontend/src/pages/Matching/index.module.css @@ -0,0 +1,10 @@ +.fullheight { + flex: 1; +} + +.center { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} diff --git a/frontend/src/pages/Matching/index.tsx b/frontend/src/pages/Matching/index.tsx new file mode 100644 index 0000000000..b5fea77efd --- /dev/null +++ b/frontend/src/pages/Matching/index.tsx @@ -0,0 +1,32 @@ +import React, { useEffect } from "react"; +import AppMargin from "../../components/AppMargin"; +import { Stack, Typography } from "@mui/material"; +import matching from "../../assets/matching.svg"; +import classes from "./index.module.css"; +import Timer from "../../components/Timer"; + +const Matching: React.FC = () => { + const totalTime = 30; + const [timeLeft, setTimeLeft] = React.useState(totalTime); + + useEffect(() => { + const timer = setInterval(() => { + setTimeLeft((prevTime) => (prevTime <= 0 ? 0 : prevTime - 1)); + }, 1000); + return () => clearInterval(timer); + }, []); + + return ( + + + + Finding your practice partner + + + + + + ); +}; + +export default Matching; From d23610a0aff47a7ebafe7f470618f05c33327897 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Thu, 3 Oct 2024 18:59:20 +0800 Subject: [PATCH 02/82] Change timer styles --- frontend/src/components/Timer/index.tsx | 19 +++++++++++++------ frontend/src/pages/Matching/index.tsx | 7 ++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/Timer/index.tsx b/frontend/src/components/Timer/index.tsx index bb712e025d..a4e89faff5 100644 --- a/frontend/src/components/Timer/index.tsx +++ b/frontend/src/components/Timer/index.tsx @@ -8,22 +8,27 @@ import { type TimerProps = { totalTime: number; timeLeft: number }; const Timer: React.FC = (props) => { - const { totalTime, timeLeft } = props; + const { totalTime, timeLeft, thickness, size } = props; const percentage = (timeLeft / totalTime) * 100; + const minutes = Math.floor(timeLeft / 60); + const seconds = timeLeft % 60; + const formattedSeconds = String(seconds).padStart(2, "0"); + const formattedMinutes = String(minutes).padStart(2, "0"); return ( ({ color: theme.palette.grey[200] })} + sx={{ opacity: 0.4 }} + thickness={thickness} /> = (props) => { alignItems: "center", }} > - {timeLeft} + + {formattedMinutes}:{formattedSeconds} + ); diff --git a/frontend/src/pages/Matching/index.tsx b/frontend/src/pages/Matching/index.tsx index b5fea77efd..4c6b0ffc93 100644 --- a/frontend/src/pages/Matching/index.tsx +++ b/frontend/src/pages/Matching/index.tsx @@ -23,7 +23,12 @@ const Matching: React.FC = () => { Finding your practice partner - + ); From f63c9679c4f5d0368eb22f01a9aa1aeac513c607 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 9 Oct 2024 15:39:59 +0800 Subject: [PATCH 03/82] Update docker compose --- docker-compose.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index fb0cfc7f34..7efba767c6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,9 @@ services: - mongo networks: - peerprep-network + volumes: + - ./backend/user-service:/user-service + - /user-service/node_modules restart: on-failure question-service: @@ -22,6 +25,9 @@ services: - user-service networks: - peerprep-network + volumes: + - ./backend/question-service:/question-service + - /question-service/node_modules restart: on-failure frontend: @@ -34,6 +40,9 @@ services: - question-service networks: - peerprep-network + volumes: + - ./frontend:/frontend + - /frontend/node_modules restart: on-failure mongo: From f2beb46eee2e9dd71ac9fb99f67b3e9af6421d07 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 9 Oct 2024 15:40:12 +0800 Subject: [PATCH 04/82] Add stop matching button --- frontend/src/App.tsx | 2 +- frontend/src/components/Navbar/index.tsx | 113 +++++++++++++---------- frontend/src/components/Timer/index.tsx | 3 +- 3 files changed, 66 insertions(+), 52 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 56134c8c96..6ca5dcadf2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -41,7 +41,7 @@ function App() { } /> - + }> } /> } /> diff --git a/frontend/src/components/Navbar/index.tsx b/frontend/src/components/Navbar/index.tsx index ce782e6a63..e8b7647f60 100644 --- a/frontend/src/components/Navbar/index.tsx +++ b/frontend/src/components/Navbar/index.tsx @@ -35,6 +35,8 @@ const Navbar: React.FC = (props) => { const location = useLocation(); const path = location.pathname; + console.log(path); + const auth = useAuth(); const [anchorEl, setAnchorEl] = useState(null); @@ -62,63 +64,76 @@ const Navbar: React.FC = (props) => { }} > - + navigate("/")} > PeerPrep - - {navbarItems - .filter((item) => !item.needsLogin || (item.needsLogin && user)) - .map((item) => ( - - {path == item.link ? {item.label} : item.label} - - ))} - {user ? ( - <> - - - - - - - { - handleClose(); - navigate(`/profile/${user.id}`); - }} + {path !== "/match" ? ( + + {navbarItems + .filter((item) => !item.needsLogin || (item.needsLogin && user)) + .map((item) => ( + + {path == item.link ? {item.label} : item.label} + + ))} + {user ? ( + <> + + + + + + + { + handleClose(); + navigate(`/profile/${user.id}`); + }} + > + Profile + + Logout + + + ) : ( + <> + - - ) : ( - <> - - - - )} - + Sign up + + + + )} + + ) : ( + + )} diff --git a/frontend/src/components/Timer/index.tsx b/frontend/src/components/Timer/index.tsx index a4e89faff5..3c643a6377 100644 --- a/frontend/src/components/Timer/index.tsx +++ b/frontend/src/components/Timer/index.tsx @@ -8,7 +8,7 @@ import { type TimerProps = { totalTime: number; timeLeft: number }; const Timer: React.FC = (props) => { - const { totalTime, timeLeft, thickness, size } = props; + const { totalTime, timeLeft, thickness, size, ...rest } = props; const percentage = (timeLeft / totalTime) * 100; const minutes = Math.floor(timeLeft / 60); const seconds = timeLeft % 60; @@ -29,7 +29,6 @@ const Timer: React.FC = (props) => { size={size} sx={{ position: "absolute" }} thickness={thickness} - {...props} /> Date: Wed, 9 Oct 2024 15:53:22 +0800 Subject: [PATCH 05/82] Render stop matching button on all matching pages --- frontend/src/App.tsx | 4 +++- frontend/src/components/Navbar/index.tsx | 5 ++--- frontend/src/utils/url.ts | 4 ++++ 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 frontend/src/utils/url.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6ca5dcadf2..c28aa54fb3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -41,8 +41,10 @@ function App() { } /> - }> + }> } /> + Matched} /> + Timeout} /> } /> diff --git a/frontend/src/components/Navbar/index.tsx b/frontend/src/components/Navbar/index.tsx index e8b7647f60..be696441bb 100644 --- a/frontend/src/components/Navbar/index.tsx +++ b/frontend/src/components/Navbar/index.tsx @@ -18,6 +18,7 @@ import { useNavigate, useLocation } from "react-router-dom"; import { useAuth } from "../../contexts/AuthContext"; import { useState } from "react"; import { USE_AUTH_ERROR_MESSAGE } from "../../utils/constants"; +import { isMatchingPage } from "../../utils/url"; type NavbarItem = { label: string; link: string; needsLogin: boolean }; @@ -35,8 +36,6 @@ const Navbar: React.FC = (props) => { const location = useLocation(); const path = location.pathname; - console.log(path); - const auth = useAuth(); const [anchorEl, setAnchorEl] = useState(null); @@ -73,7 +72,7 @@ const Navbar: React.FC = (props) => { > PeerPrep - {path !== "/match" ? ( + {!isMatchingPage(path) ? ( {navbarItems .filter((item) => !item.needsLogin || (item.needsLogin && user)) diff --git a/frontend/src/utils/url.ts b/frontend/src/utils/url.ts new file mode 100644 index 0000000000..2fd68afc0f --- /dev/null +++ b/frontend/src/utils/url.ts @@ -0,0 +1,4 @@ +export const isMatchingPage = (path: string) => { + const pattern = /^(?\/matching)(?\/.*)*$/; + return pattern.test(path); +}; From f082a20ed0ab10b9e9194f57151238e12332f30c Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 9 Oct 2024 16:07:30 +0800 Subject: [PATCH 06/82] Update timer props --- frontend/src/App.tsx | 4 ++-- frontend/src/components/Timer/index.tsx | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c28aa54fb3..bae331cb11 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -48,8 +48,8 @@ function App() { } /> - }> - }> + } /> + } /> ); diff --git a/frontend/src/components/Timer/index.tsx b/frontend/src/components/Timer/index.tsx index 3c643a6377..f5c9e3aec9 100644 --- a/frontend/src/components/Timer/index.tsx +++ b/frontend/src/components/Timer/index.tsx @@ -22,6 +22,7 @@ const Timer: React.FC = (props) => { value={100} sx={{ opacity: 0.4 }} thickness={thickness} + {...rest} /> = (props) => { size={size} sx={{ position: "absolute" }} thickness={thickness} + {...rest} /> Date: Wed, 9 Oct 2024 16:20:11 +0800 Subject: [PATCH 07/82] Add test cases for timer component and fix existing test cases --- frontend/jest.config.ts | 2 +- frontend/setupTest.ts | 1 + .../src/components/Navbar/Navbar.test.tsx | 8 ++++++- .../ProfileDetails/ProfileDetailstest.tsx | 1 - .../QuestionCategoryAutoComplete.test.tsx | 1 - .../QuestionDetail/QuestionDetail.test.tsx | 7 +++--- .../QuestionImage/QuestionImage.test.tsx | 3 +-- .../QuestionImageContainer.test.tsx | 23 +++++++++--------- .../QuestionImageDialog.test.tsx | 7 +++--- .../QuestionMarkdown.test.tsx | 7 +++--- .../ServerError/ServerError.test.tsx | 1 - frontend/src/components/Timer/Timer.test.tsx | 24 +++++++++++++++++++ frontend/src/components/Timer/index.tsx | 5 +++- 13 files changed, 58 insertions(+), 32 deletions(-) create mode 100644 frontend/setupTest.ts create mode 100644 frontend/src/components/Timer/Timer.test.tsx diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts index d002a6e54c..a9c6917456 100644 --- a/frontend/jest.config.ts +++ b/frontend/jest.config.ts @@ -142,7 +142,7 @@ const config: Config = { // setupFiles: [], // A list of paths to modules that run some code to configure or set up the testing framework before each test - // setupFilesAfterEnv: [], + setupFilesAfterEnv: ["/setupTest.ts"], // The number of seconds after which a test is considered as slow and reported as such in the results. // slowTestThreshold: 5, diff --git a/frontend/setupTest.ts b/frontend/setupTest.ts new file mode 100644 index 0000000000..d0de870dc5 --- /dev/null +++ b/frontend/setupTest.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom"; diff --git a/frontend/src/components/Navbar/Navbar.test.tsx b/frontend/src/components/Navbar/Navbar.test.tsx index 247459787f..111148b505 100644 --- a/frontend/src/components/Navbar/Navbar.test.tsx +++ b/frontend/src/components/Navbar/Navbar.test.tsx @@ -1,5 +1,4 @@ import { fireEvent, render, screen } from "@testing-library/react"; -import "@testing-library/jest-dom"; import axios from "axios"; import { faker } from "@faker-js/faker"; import * as hooks from "../../contexts/AuthContext"; @@ -46,6 +45,7 @@ describe("Navigation routes", () => { signup: jest.fn(), login: jest.fn(), logout: jest.fn(), + setUser: jest.fn(), loading: false, user: { id: "1", @@ -76,6 +76,7 @@ describe("Unauthenticated user", () => { login: jest.fn(), logout: jest.fn(), loading: false, + setUser: jest.fn(), user: null, })); render( @@ -92,6 +93,7 @@ describe("Unauthenticated user", () => { signup: jest.fn(), login: jest.fn(), logout: jest.fn(), + setUser: jest.fn(), loading: false, user: null, })); @@ -133,6 +135,7 @@ describe("Authenticated user", () => { signup: jest.fn(), login: jest.fn(), logout: jest.fn(), + setUser: jest.fn(), loading: false, user: { id: "1", @@ -182,6 +185,7 @@ describe("Authenticated user", () => { signup: jest.fn(), login: jest.fn(), logout: jest.fn(), + setUser: jest.fn(), loading: false, user: { id: "1", @@ -235,6 +239,7 @@ describe("Authenticated user", () => { signup: jest.fn(), login: jest.fn(), logout: jest.fn(), + setUser: jest.fn(), loading: false, user: { id: "1", @@ -287,6 +292,7 @@ describe("Authenticated user", () => { login: jest.fn(), logout: jest.fn(), loading: false, + setUser: jest.fn(), user: { id: "1", username, diff --git a/frontend/src/components/ProfileDetails/ProfileDetailstest.tsx b/frontend/src/components/ProfileDetails/ProfileDetailstest.tsx index cddf47b020..d790d44a0f 100644 --- a/frontend/src/components/ProfileDetails/ProfileDetailstest.tsx +++ b/frontend/src/components/ProfileDetails/ProfileDetailstest.tsx @@ -1,5 +1,4 @@ import { render, screen } from "@testing-library/react"; -import "@testing-library/jest-dom"; import { faker } from "@faker-js/faker"; import ProfileDetails from "."; diff --git a/frontend/src/components/QuestionCategoryAutoComplete/QuestionCategoryAutoComplete.test.tsx b/frontend/src/components/QuestionCategoryAutoComplete/QuestionCategoryAutoComplete.test.tsx index ee69dc0d13..ad1fb95732 100644 --- a/frontend/src/components/QuestionCategoryAutoComplete/QuestionCategoryAutoComplete.test.tsx +++ b/frontend/src/components/QuestionCategoryAutoComplete/QuestionCategoryAutoComplete.test.tsx @@ -1,5 +1,4 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; -import "@testing-library/jest-dom"; import QuestionCategoryAutoComplete from "."; jest.mock("../../utils/api", () => ({ diff --git a/frontend/src/components/QuestionDetail/QuestionDetail.test.tsx b/frontend/src/components/QuestionDetail/QuestionDetail.test.tsx index 0515fb89d8..02e8ba433a 100644 --- a/frontend/src/components/QuestionDetail/QuestionDetail.test.tsx +++ b/frontend/src/components/QuestionDetail/QuestionDetail.test.tsx @@ -1,5 +1,4 @@ import { render, screen } from "@testing-library/react"; -import "@testing-library/jest-dom"; import QuestionDetail from "."; jest.mock("@uiw/react-md-editor", () => ({ @@ -25,7 +24,7 @@ describe("Question details", () => { complexity={complexity} categories={categories} description={description} - />, + /> ); expect(screen.getByText(title)).toBeInTheDocument(); }); @@ -41,7 +40,7 @@ describe("Question details", () => { complexity={complexity} categories={categories} description={description} - />, + /> ); expect(screen.getByText(complexity)).toBeInTheDocument(); }); @@ -57,7 +56,7 @@ describe("Question details", () => { complexity={complexity} categories={categories} description={description} - />, + /> ); expect(screen.getByText(categories[0])).toBeInTheDocument(); expect(screen.getByText(categories[1])).toBeInTheDocument(); diff --git a/frontend/src/components/QuestionImage/QuestionImage.test.tsx b/frontend/src/components/QuestionImage/QuestionImage.test.tsx index 287a585b74..8d33e517be 100644 --- a/frontend/src/components/QuestionImage/QuestionImage.test.tsx +++ b/frontend/src/components/QuestionImage/QuestionImage.test.tsx @@ -1,5 +1,4 @@ import { fireEvent, render, screen } from "@testing-library/react"; -import "@testing-library/jest-dom"; import QuestionImage from "."; Object.assign(navigator, { @@ -27,7 +26,7 @@ describe("Question Image", () => { fireEvent.click(copyButton); expect(navigator.clipboard.writeText).toHaveBeenCalledWith( - `![image](${url})`, + `![image](${url})` ); }); diff --git a/frontend/src/components/QuestionImageContainer/QuestionImageContainer.test.tsx b/frontend/src/components/QuestionImageContainer/QuestionImageContainer.test.tsx index 6aea0c67ad..317628bbba 100644 --- a/frontend/src/components/QuestionImageContainer/QuestionImageContainer.test.tsx +++ b/frontend/src/components/QuestionImageContainer/QuestionImageContainer.test.tsx @@ -1,5 +1,4 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; -import "@testing-library/jest-dom"; import QuestionImageContainer from "."; import { questionClient } from "../../utils/api"; @@ -27,7 +26,7 @@ describe("Question Image Container", () => { Object.defineProperty(window, "localStorage", { value: mockLocalStorage, writable: true, - }), + }) ); it("Question Image Container is rendered with no uploaded images", () => { @@ -38,11 +37,11 @@ describe("Question Image Container", () => { , + /> ); const uploadImageMessage = screen.getByText( - "Click to upload images. The maximum image size accepted is 5MB.", + "Click to upload images. The maximum image size accepted is 5MB." ); expect(uploadImageMessage).toBeInTheDocument(); }); @@ -55,7 +54,7 @@ describe("Question Image Container", () => { , + /> ); const images = screen.getAllByAltText("question image"); @@ -70,7 +69,7 @@ describe("Question Image Container", () => { , + /> ); const image = screen.getByAltText("question image"); @@ -110,7 +109,7 @@ describe("Question Image Container", () => { , + /> ); const file = new File(["file"], "file.png", { type: "image/png" }); @@ -126,7 +125,7 @@ describe("Question Image Container", () => { Authorization: `Bearer ${mockLocalStorage.getItem("token")}`, "Content-Type": "multipart/form-data", }, - }), + }) ); expect(setUploadedImagesUrl).toHaveBeenCalled(); @@ -141,7 +140,7 @@ describe("Question Image Container", () => { , + /> ); const file = new File(["file"], "file.txt", { type: "text/plain" }); @@ -159,7 +158,7 @@ describe("Question Image Container", () => { , + /> ); const input = screen.getByTestId("file-input"); @@ -185,7 +184,7 @@ describe("Question Image Container", () => { , + /> ); const file = new File(["file"], "file.png", { type: "image/png" }); @@ -205,7 +204,7 @@ describe("Question Image Container", () => { , + /> ); const input = screen.getByTestId("file-input"); diff --git a/frontend/src/components/QuestionImageDialog/QuestionImageDialog.test.tsx b/frontend/src/components/QuestionImageDialog/QuestionImageDialog.test.tsx index 9e1059e6ef..912f512fad 100644 --- a/frontend/src/components/QuestionImageDialog/QuestionImageDialog.test.tsx +++ b/frontend/src/components/QuestionImageDialog/QuestionImageDialog.test.tsx @@ -1,5 +1,4 @@ import { render, screen } from "@testing-library/react"; -import "@testing-library/jest-dom"; import QuestionImageDialog from "."; describe("Question Image Dialog", () => { @@ -12,7 +11,7 @@ describe("Question Image Dialog", () => { value={url} open={true} handleClose={mockHandleClose} - />, + /> ); const image = screen.getByAltText("question image enlarged"); @@ -27,7 +26,7 @@ describe("Question Image Dialog", () => { value={url} open={false} handleClose={mockHandleClose} - />, + /> ); expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); @@ -39,7 +38,7 @@ describe("Question Image Dialog", () => { value={url} open={true} handleClose={mockHandleClose} - />, + /> ); const closeButton = screen.getByRole("button", { name: "close" }); diff --git a/frontend/src/components/QuestionMarkdown/QuestionMarkdown.test.tsx b/frontend/src/components/QuestionMarkdown/QuestionMarkdown.test.tsx index d39bcfcb9a..ebb0b8a40e 100644 --- a/frontend/src/components/QuestionMarkdown/QuestionMarkdown.test.tsx +++ b/frontend/src/components/QuestionMarkdown/QuestionMarkdown.test.tsx @@ -1,5 +1,4 @@ import { fireEvent, render, screen } from "@testing-library/react"; -import "@testing-library/jest-dom"; import QuestionMarkdown from "."; jest.mock("@uiw/react-md-editor", () => ({ @@ -29,7 +28,7 @@ describe("Question Markdown", () => { , + /> ); const textarea = screen.getByPlaceholderText("Description"); @@ -44,7 +43,7 @@ describe("Question Markdown", () => { , + /> ); const textarea = screen.getByPlaceholderText("Description"); @@ -58,7 +57,7 @@ describe("Question Markdown", () => { , + /> ); const textarea = screen.getByPlaceholderText("Description"); diff --git a/frontend/src/components/ServerError/ServerError.test.tsx b/frontend/src/components/ServerError/ServerError.test.tsx index 10e801a31a..284321f0d7 100644 --- a/frontend/src/components/ServerError/ServerError.test.tsx +++ b/frontend/src/components/ServerError/ServerError.test.tsx @@ -1,5 +1,4 @@ import { render, screen } from "@testing-library/react"; -import "@testing-library/jest-dom"; import ServerError from "."; describe("Not found", () => { diff --git a/frontend/src/components/Timer/Timer.test.tsx b/frontend/src/components/Timer/Timer.test.tsx new file mode 100644 index 0000000000..ad1417fdcc --- /dev/null +++ b/frontend/src/components/Timer/Timer.test.tsx @@ -0,0 +1,24 @@ +import { render, screen } from "@testing-library/react"; +import Timer from "."; + +describe("Timer", () => { + it("Timer is rendered", () => { + render(); + expect(screen.getByTestId("timer")).toBeInTheDocument(); + }); + + it("Timer is rendered with correct seconds displayed", () => { + render(); + expect(screen.getByText("00:30")).toBeInTheDocument(); + }); + + it("Timer is rendered with correct minutes displayed", () => { + render(); + expect(screen.getByText("01:00")).toBeInTheDocument(); + }); + + it("Timer is rendered with correct minutes and seconds displayed", () => { + render(); + expect(screen.getByText("01:10")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/Timer/index.tsx b/frontend/src/components/Timer/index.tsx index f5c9e3aec9..70d34a8687 100644 --- a/frontend/src/components/Timer/index.tsx +++ b/frontend/src/components/Timer/index.tsx @@ -15,7 +15,10 @@ const Timer: React.FC = (props) => { const formattedSeconds = String(seconds).padStart(2, "0"); const formattedMinutes = String(minutes).padStart(2, "0"); return ( - + Date: Wed, 9 Oct 2024 17:35:44 +0800 Subject: [PATCH 08/82] Set up matching service --- backend/matching-service/.env.sample | 4 + backend/matching-service/README.md | 21 + backend/matching-service/app.ts | 30 + backend/matching-service/eslint.config.js | 22 + backend/matching-service/jest.config.ts | 199 + backend/matching-service/package-lock.json | 6758 +++++++++++++++++ backend/matching-service/package.json | 41 + backend/matching-service/server.ts | 11 + .../src/routes/matchingRoutes.ts | 5 + backend/matching-service/swagger.yml | 5 + backend/matching-service/tsconfig.json | 110 + backend/question-service/.env.sample | 1 + 12 files changed, 7207 insertions(+) create mode 100644 backend/matching-service/.env.sample create mode 100644 backend/matching-service/README.md create mode 100644 backend/matching-service/app.ts create mode 100644 backend/matching-service/eslint.config.js create mode 100644 backend/matching-service/jest.config.ts create mode 100644 backend/matching-service/package-lock.json create mode 100644 backend/matching-service/package.json create mode 100644 backend/matching-service/server.ts create mode 100644 backend/matching-service/src/routes/matchingRoutes.ts create mode 100644 backend/matching-service/swagger.yml create mode 100644 backend/matching-service/tsconfig.json diff --git a/backend/matching-service/.env.sample b/backend/matching-service/.env.sample new file mode 100644 index 0000000000..583d11993e --- /dev/null +++ b/backend/matching-service/.env.sample @@ -0,0 +1,4 @@ +NODE_ENV=development +PORT=3002 + +ORIGINS=http://localhost:5173,http://127.0.0.1:5173 diff --git a/backend/matching-service/README.md b/backend/matching-service/README.md new file mode 100644 index 0000000000..aa94908a84 --- /dev/null +++ b/backend/matching-service/README.md @@ -0,0 +1,21 @@ +# Matching Service + +## Setting-up Matching Service + +1. In the `matching-service` directory, create a copy of the `.env.sample` file and name it `.env`. + +## Running Matching Service without Docker + +1. Follow the instructions [here](https://nodejs.org/en/download/package-manager) to set up Node v20. + +2. Open Command Line/Terminal and navigate into the `matching-service` directory. + +3. Run the command: `npm install`. This will install all the necessary dependencies. + +4. Run the command `npm start` to start the Matching Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. + +## After running + +1. To view Matching Service documentation, go to http://localhost:3002/docs. + +2. Using applications like Postman, you can interact with the Matching Service on port 3002. If you wish to change this, please update the `.env` file. diff --git a/backend/matching-service/app.ts b/backend/matching-service/app.ts new file mode 100644 index 0000000000..e6cd808b1e --- /dev/null +++ b/backend/matching-service/app.ts @@ -0,0 +1,30 @@ +import express, { Request, Response } from "express"; +import dotenv from "dotenv"; +import swaggerUi from "swagger-ui-express"; +import yaml from "yaml"; +import fs from "fs"; +import cors from "cors"; + +import matchingRoutes from "./src/routes/matchingRoutes.ts"; + +dotenv.config(); + +const allowedOrigins = process.env.ORIGINS + ? process.env.ORIGINS.split(",") + : ["http://localhost:5173", "http://127.0.0.1:5173"]; + +const file = fs.readFileSync("./swagger.yml", "utf-8"); +const swaggerDocument = yaml.parse(file); + +const app = express(); + +app.use(cors({ origin: allowedOrigins, credentials: true })); +app.options("*", cors({ origin: allowedOrigins, credentials: true })); + +app.use("/api/matching", matchingRoutes); +app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); +app.get("/", (req: Request, res: Response) => { + res.status(200).json({ message: "Hello world from matching service" }); +}); + +export default app; diff --git a/backend/matching-service/eslint.config.js b/backend/matching-service/eslint.config.js new file mode 100644 index 0000000000..32d12d9801 --- /dev/null +++ b/backend/matching-service/eslint.config.js @@ -0,0 +1,22 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default [ + { files: ["**/*.{js,mjs,cjs,ts}"] }, + { languageOptions: { globals: globals.node } }, + { + rules: { + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + }, + }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, +]; diff --git a/backend/matching-service/jest.config.ts b/backend/matching-service/jest.config.ts new file mode 100644 index 0000000000..151d29ec19 --- /dev/null +++ b/backend/matching-service/jest.config.ts @@ -0,0 +1,199 @@ +/** + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ + +import type { Config } from "jest"; + +const config: Config = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/04/ng4c26hj1ksdsy_7x_21kvx80000gn/T/jest_dx", + + // Automatically clear mock calls, instances, contexts and results before every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: undefined, + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "v8", + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: undefined, + + // A path to a custom dependency extractor + // dependencyExtractor: undefined, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // The default configuration for fake timers + // fakeTimers: { + // "enableGlobally": false + // }, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "mjs", + // "cjs", + // "jsx", + // "ts", + // "tsx", + // "json", + // "node" + // ], + + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + preset: "ts-jest", + + // Run tests from one or more projects + // projects: undefined, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state before every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: undefined, + + // Automatically restore mock state and implementation before every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + // testEnvironment: "jest-environment-node", + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, + + // This option allows use of a custom test runner + // testRunner: "jest-circus/runner", + + // A map from regular expressions to paths to transformers + // transform: undefined, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/", + // "\\.pnp\\.[^\\/]+$" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: undefined, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; + +export default config; diff --git a/backend/matching-service/package-lock.json b/backend/matching-service/package-lock.json new file mode 100644 index 0000000000..46b4f6c001 --- /dev/null +++ b/backend/matching-service/package-lock.json @@ -0,0 +1,6758 @@ +{ + "name": "matching-service", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "matching-service", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.21.1", + "swagger-ui-express": "^5.0.1", + "yaml": "^2.5.1" + }, + "devDependencies": { + "@eslint/js": "^9.12.0", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.13", + "@types/node": "^22.7.5", + "@types/supertest": "^6.0.2", + "@types/swagger-ui-express": "^4.1.6", + "cross-env": "^7.0.3", + "eslint": "^9.12.0", + "globals": "^15.11.0", + "jest": "^29.7.0", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "tsx": "^4.19.1", + "typescript": "^5.6.3", + "typescript-eslint": "^8.8.1" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@ampproject/remapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.25.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.7.tgz", + "integrity": "sha512-9ickoLz+hcXCeh7jrcin+/SLWm+GkxE2kTvoYyp38p4WkdFXfQJxDFGWp/YHjiKLPx06z2A7W8XKuqbReXDzsw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.7.tgz", + "integrity": "sha512-yJ474Zv3cwiSOO9nXJuqzvwEeM+chDuQ8GJirw+pZ91sCGCyOZ3dJkVE09fTV0VEVzXyLWhh3G/AolYTPX7Mow==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helpers": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/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==", + "dev": true + }, + "node_modules/@babel/generator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", + "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.7.tgz", + "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.7.tgz", + "integrity": "sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.7.tgz", + "integrity": "sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.7.tgz", + "integrity": "sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.7.tgz", + "integrity": "sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/traverse/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==", + "dev": true + }, + "node_modules/@babel/types": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.7.tgz", + "integrity": "sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/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==", + "dev": true + }, + "node_modules/@eslint/core": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", + "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/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==", + "dev": true + }, + "node_modules/@eslint/js": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.12.0.tgz", + "integrity": "sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", + "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", + "dev": true, + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", + "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", + "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.0", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz", + "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.13", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.13.tgz", + "integrity": "sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true + }, + "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/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/qs": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", + "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", + "dev": true, + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", + "integrity": "sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz", + "integrity": "sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/type-utils": "8.8.1", + "@typescript-eslint/utils": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.1.tgz", + "integrity": "sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/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==", + "dev": true + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz", + "integrity": "sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.1.tgz", + "integrity": "sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "8.8.1", + "@typescript-eslint/utils": "8.8.1", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/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==", + "dev": true + }, + "node_modules/@typescript-eslint/types": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.1.tgz", + "integrity": "sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz", + "integrity": "sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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==", + "dev": true + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.1.tgz", + "integrity": "sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz", + "integrity": "sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.8.1", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001667", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz", + "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "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==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.33", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.33.tgz", + "integrity": "sha512-+cYTcFB1QqD4j4LegwLfpCNxifb6dDFUAwk6RsLusCwIaZI6or2f+q8rs5tTB2YC53HhOlIbEaqHMAAC8IOIwA==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "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==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.12.0.tgz", + "integrity": "sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.6.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.12.0", + "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.5", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.1.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/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==", + "dev": true + }, + "node_modules/espree": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "dev": true, + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.1.tgz", + "integrity": "sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og==", + "dev": true, + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.11.0.tgz", + "integrity": "sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "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==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/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==", + "dev": true + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=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==", + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/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==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", + "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", + "dev": true, + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^3.5.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/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==", + "dev": true + }, + "node_modules/supertest": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.0.0.tgz", + "integrity": "sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==", + "dev": true, + "dependencies": { + "methods": "^1.1.2", + "superagent": "^9.0.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.17.14", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", + "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==" + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsx": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.1.tgz", + "integrity": "sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==", + "dev": true, + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.8.1.tgz", + "integrity": "sha512-R0dsXFt6t4SAFjUSKFjMh4pXDtq04SsFKCVGDP3ZOzNP7itF0jBcZYU4fMsZr4y7O7V7Nc751dDeESbe4PbQMQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.8.1", + "@typescript-eslint/parser": "8.8.1", + "@typescript-eslint/utils": "8.8.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/backend/matching-service/package.json b/backend/matching-service/package.json new file mode 100644 index 0000000000..650e4f6e4a --- /dev/null +++ b/backend/matching-service/package.json @@ -0,0 +1,41 @@ +{ + "name": "matching-service", + "version": "1.0.0", + "main": "server.ts", + "type": "module", + "scripts": { + "start": "tsx server.ts", + "dev": "tsx watch server.ts", + "test": "cross-env NODE_ENV=test && jest", + "test:watch": "cross-env NODE_ENV=test && jest --watch", + "lint": "eslint ." + }, + "author": "", + "license": "ISC", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.21.1", + "swagger-ui-express": "^5.0.1", + "yaml": "^2.5.1" + }, + "devDependencies": { + "@eslint/js": "^9.12.0", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.13", + "@types/node": "^22.7.5", + "@types/supertest": "^6.0.2", + "@types/swagger-ui-express": "^4.1.6", + "cross-env": "^7.0.3", + "eslint": "^9.12.0", + "globals": "^15.11.0", + "jest": "^29.7.0", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "tsx": "^4.19.1", + "typescript": "^5.6.3", + "typescript-eslint": "^8.8.1" + } +} diff --git a/backend/matching-service/server.ts b/backend/matching-service/server.ts new file mode 100644 index 0000000000..a5d3980945 --- /dev/null +++ b/backend/matching-service/server.ts @@ -0,0 +1,11 @@ +import app from "./app.ts"; + +const PORT = process.env.PORT || 3002; + +if (process.env.NODE_ENV !== "test") { + app.listen(PORT, () => { + console.log( + `Matching service server listening on http://localhost:${PORT}` + ); + }); +} diff --git a/backend/matching-service/src/routes/matchingRoutes.ts b/backend/matching-service/src/routes/matchingRoutes.ts new file mode 100644 index 0000000000..9da7196e6e --- /dev/null +++ b/backend/matching-service/src/routes/matchingRoutes.ts @@ -0,0 +1,5 @@ +import express from "express"; + +const router = express.Router(); + +export default router; diff --git a/backend/matching-service/swagger.yml b/backend/matching-service/swagger.yml new file mode 100644 index 0000000000..440578422a --- /dev/null +++ b/backend/matching-service/swagger.yml @@ -0,0 +1,5 @@ +openapi: 3.0.0 + +info: + title: Matching Service + version: 1.0.0 diff --git a/backend/matching-service/tsconfig.json b/backend/matching-service/tsconfig.json new file mode 100644 index 0000000000..ea143d1bd8 --- /dev/null +++ b/backend/matching-service/tsconfig.json @@ -0,0 +1,110 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "ESNext" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + "allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */, + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + "noEmit": true /* Disable emitting files from a compilation. */, + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/backend/question-service/.env.sample b/backend/question-service/.env.sample index fb1103500f..8c6b2bfb82 100644 --- a/backend/question-service/.env.sample +++ b/backend/question-service/.env.sample @@ -1,4 +1,5 @@ NODE_ENV=development +PORT=3000 MONGO_CLOUD_URI= MONGO_LOCAL_URI= From 242b2225ddbaa09b492fd6ff89e90b5edef9a3d8 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Thu, 10 Oct 2024 09:09:26 +0800 Subject: [PATCH 09/82] Set up matching web socket --- backend/matching-service/package-lock.json | 213 +++++++++++++++++- backend/matching-service/package.json | 2 + backend/matching-service/server.ts | 9 +- .../src/websocket/websocket.ts | 44 ++++ .../src/websocket/websocketHandlers.ts | 111 +++++++++ frontend/package-lock.json | 80 +++++++ frontend/package.json | 1 + frontend/src/pages/Home/index.tsx | 140 ++++++++---- 8 files changed, 553 insertions(+), 47 deletions(-) create mode 100644 backend/matching-service/src/websocket/websocket.ts create mode 100644 backend/matching-service/src/websocket/websocketHandlers.ts diff --git a/backend/matching-service/package-lock.json b/backend/matching-service/package-lock.json index 46b4f6c001..6429b01765 100644 --- a/backend/matching-service/package-lock.json +++ b/backend/matching-service/package-lock.json @@ -12,6 +12,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", + "socket.io": "^4.8.0", "swagger-ui-express": "^5.0.1", "yaml": "^2.5.1" }, @@ -21,6 +22,7 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.13", "@types/node": "^22.7.5", + "@types/socket.io": "^3.0.2", "@types/supertest": "^6.0.2", "@types/swagger-ui-express": "^4.1.6", "cross-env": "^7.0.3", @@ -1821,6 +1823,11 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -1905,6 +1912,11 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -1915,7 +1927,6 @@ "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -2021,7 +2032,6 @@ "version": "22.7.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", - "dev": true, "dependencies": { "undici-types": "~6.19.2" } @@ -2059,6 +2069,16 @@ "@types/send": "*" } }, + "node_modules/@types/socket.io": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.2.tgz", + "integrity": "sha512-pu0sN9m5VjCxBZVK8hW37ZcMe8rjn4HHggBN5CbaRTvFwv5jOmuIRZEuddsBPa9Th0ts0SIo3Niukq+95cMBbQ==", + "deprecated": "This is a stub types definition. socket.io provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "socket.io": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -2675,6 +2695,14 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -3258,6 +3286,63 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.1.tgz", + "integrity": "sha512-NEpDCw9hrvBW+hVEOK4T7v0jFJ++KgtPl4jKFwsZVfG1XhS0dCrSb3VMb9gPAd7VAdW52VT1EnaNiU2vM8C0og==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/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==" + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -6008,6 +6093,107 @@ "node": ">=8" } }, + "node_modules/socket.io": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/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==" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/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==" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/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==" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6515,8 +6701,7 @@ "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/unpipe": { "version": "1.0.0", @@ -6680,6 +6865,26 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/backend/matching-service/package.json b/backend/matching-service/package.json index 650e4f6e4a..85209cb95e 100644 --- a/backend/matching-service/package.json +++ b/backend/matching-service/package.json @@ -16,6 +16,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", + "socket.io": "^4.8.0", "swagger-ui-express": "^5.0.1", "yaml": "^2.5.1" }, @@ -25,6 +26,7 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.13", "@types/node": "^22.7.5", + "@types/socket.io": "^3.0.2", "@types/supertest": "^6.0.2", "@types/swagger-ui-express": "^4.1.6", "cross-env": "^7.0.3", diff --git a/backend/matching-service/server.ts b/backend/matching-service/server.ts index a5d3980945..0fe9b6805f 100644 --- a/backend/matching-service/server.ts +++ b/backend/matching-service/server.ts @@ -1,9 +1,16 @@ +import http from "http"; import app from "./app.ts"; +import Websocket from "./src/websocket/websocket.ts"; +import { handleMatchRequest } from "./src/websocket/websocketHandlers.ts"; + +const server = http.createServer(app); +const io = Websocket.getInstance(server); +io.initSocketHandlers([{ path: "/matching", handler: handleMatchRequest }]); const PORT = process.env.PORT || 3002; if (process.env.NODE_ENV !== "test") { - app.listen(PORT, () => { + server.listen(PORT, () => { console.log( `Matching service server listening on http://localhost:${PORT}` ); diff --git a/backend/matching-service/src/websocket/websocket.ts b/backend/matching-service/src/websocket/websocket.ts new file mode 100644 index 0000000000..6564f81369 --- /dev/null +++ b/backend/matching-service/src/websocket/websocket.ts @@ -0,0 +1,44 @@ +import http from "http"; +import { Server, Socket } from "socket.io"; + +// Adapted from https://dev.to/nickfelix/how-to-implement-socketio-using-typescript-3ne2 +const WEBSOCKET_CORS = { + origin: "*", + methods: ["GET", "POST"], +}; + +interface MatchRequestParams { + user: string; + complexities: string[]; + categories: string[]; + languages: string[]; + timeout: number; +} + +class Websocket extends Server { + private static io: Websocket; + + constructor(server: http.Server) { + super(server, { + cors: WEBSOCKET_CORS, + }); + } + + public static getInstance(server: http.Server): Websocket { + if (!Websocket.io) { + Websocket.io = new Websocket(server); + } + + return Websocket.io; + } + + public initSocketHandlers(socketHandlers: Array) { + socketHandlers.forEach((element) => { + Websocket.io.of(element.path, (socket: Socket) => { + element.handler(socket); + }); + }); + } +} + +export default Websocket; diff --git a/backend/matching-service/src/websocket/websocketHandlers.ts b/backend/matching-service/src/websocket/websocketHandlers.ts new file mode 100644 index 0000000000..8a150bd0d0 --- /dev/null +++ b/backend/matching-service/src/websocket/websocketHandlers.ts @@ -0,0 +1,111 @@ +import { Socket } from "socket.io"; + +interface MatchRequest { + user: string; + complexities: string[]; + categories: string[]; + languages: string[]; + timeout: number; +} + +interface MatchQueueItem { + socket: Socket; + user: string; + complexities: string[]; + categories: string[]; + languages: string[]; + timeout: NodeJS.Timeout; + acceptedMatch: boolean; +} + +const matchedPairs: { user1: MatchQueueItem; user2: MatchQueueItem }[] = []; + +export const handleMatchRequest = (socket: Socket) => { + socket.on("match_request", (matchRequest: MatchRequest) => { + const { user, complexities, categories, languages, timeout } = matchRequest; + + const matchTimeout = setTimeout(() => { + socket.emit("match_timeout"); + }, timeout * 1000); + + const matchQueueItem: MatchQueueItem = { + socket: socket, + user: user, + complexities: complexities, + categories: categories, + languages: languages, + timeout: matchTimeout, + acceptedMatch: false, + }; + + // TODO: add to queue, don't match user if matchQueueItem.socket.disconnected + // appendToMatchQueue(matchQueueItem); + + // TODO: in queue service, if match is found + const partner: MatchQueueItem = { + socket: socket, + user: "OtherUser123", + complexities: complexities, + categories: categories, + languages: languages, + timeout: matchTimeout, + acceptedMatch: false, + }; + clearTimeout(matchQueueItem.timeout); + clearTimeout(partner.timeout); + matchedPairs.push({ user1: matchQueueItem, user2: partner }); + matchQueueItem.socket.emit("match_found", partner.user); + partner.socket.emit("match_found", matchQueueItem.user); + + const acceptanceTimeout = setTimeout(() => { + matchQueueItem.socket.emit("match_unsuccessful"); + partner.socket.emit("match_unsuccessful"); + }, 10 * 1000); + matchQueueItem.timeout = acceptanceTimeout; + partner.timeout = acceptanceTimeout; + }); + + socket.on("match_accepted", () => { + const matchedPair = matchedPairs.find( + (pair) => + pair.user1.socket.id === socket.id || pair.user2.socket.id === socket.id + )!; + + const [matchQueueItem, partner] = + matchedPair.user1.socket.id === socket.id + ? [matchedPair.user1, matchedPair.user2] + : [matchedPair.user2, matchedPair.user1]; + + clearTimeout(matchQueueItem.timeout); + + if (partner.acceptedMatch) { + socket.emit("match_successful", partner.user); + } else { + matchQueueItem.acceptedMatch = true; + } + }); + + socket.on("match_declined", () => { + const index = matchedPairs.findIndex( + (pair) => + pair.user1.socket.id === socket.id || pair.user2.socket.id === socket.id + ); + + if (index === -1) { + socket.emit("match_unsuccessful"); + return; + } + + const matchedPair = matchedPairs.splice(index, 1)[0]; + + const [matchQueueItem, partner] = + matchedPair.user1.socket.id === socket.id + ? [matchedPair.user1, matchedPair.user2] + : [matchedPair.user2, matchedPair.user1]; + + clearTimeout(matchQueueItem.timeout); + clearTimeout(partner.timeout); + socket.disconnect(); + partner.socket.emit("match_unsuccessful"); + }); +}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e2274b3bcc..006d964fc1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,6 +24,7 @@ "react-material-ui-carousel": "^3.4.2", "react-router-dom": "^6.26.2", "react-toastify": "^10.0.5", + "socket.io-client": "^4.8.0", "vite-plugin-svgr": "^4.2.0" }, "devDependencies": { @@ -3966,6 +3967,11 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", @@ -6199,6 +6205,46 @@ "dev": true, "license": "MIT" }, + "node_modules/engine.io-client": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz", + "integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -12114,6 +12160,32 @@ "tslib": "^2.0.3" } }, + "node_modules/socket.io-client": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz", + "integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -13125,6 +13197,14 @@ "dev": true, "license": "MIT" }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz", + "integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/frontend/package.json b/frontend/package.json index 2343fb3c71..f6cfbc02fa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "react-material-ui-carousel": "^3.4.2", "react-router-dom": "^6.26.2", "react-toastify": "^10.0.5", + "socket.io-client": "^4.8.0", "vite-plugin-svgr": "^4.2.0" }, "devDependencies": { diff --git a/frontend/src/pages/Home/index.tsx b/frontend/src/pages/Home/index.tsx index 9d4a8e6457..dfdb85516e 100644 --- a/frontend/src/pages/Home/index.tsx +++ b/frontend/src/pages/Home/index.tsx @@ -1,40 +1,95 @@ import { - // Autocomplete, - // Button, - // Card, - // FormControl, - // Grid2, - // TextField, + Autocomplete, + Box, + Button, + Card, + FormControl, + Grid2, + TextField, Typography, } from "@mui/material"; -// import { useEffect, useReducer, useState } from "react"; +import { useEffect, useReducer, useState } from "react"; import classes from "./index.module.css"; import AppMargin from "../../components/AppMargin"; -// import { -// complexityList, -// languageList, -// maxMatchTimeout, -// minMatchTimeout, -// } from "../../utils/constants"; -// import reducer, { -// getQuestionCategories, -// initialState, -// } from "../../reducers/questionReducer"; -// import CustomChip from "../../components/CustomChip"; -// import homepageImage from "/homepage_image.svg"; +import { + complexityList, + languageList, + maxMatchTimeout, + minMatchTimeout, + USE_AUTH_ERROR_MESSAGE, +} from "../../utils/constants"; +import reducer, { + getQuestionCategories, + initialState, +} from "../../reducers/questionReducer"; +import CustomChip from "../../components/CustomChip"; +import homepageImage from "/homepage_image.svg"; +import { io } from "socket.io-client"; +import { useAuth } from "../../contexts/AuthContext"; const Home: React.FC = () => { - // const [complexity, setComplexity] = useState([]); - // const [selectedCategories, setSelectedCategories] = useState([]); - // const [language, setLanguage] = useState([]); - // const [timeout, setTimeout] = useState(30); + const [complexity, setComplexity] = useState([]); + const [selectedCategories, setSelectedCategories] = useState([]); + const [language, setLanguage] = useState([]); + const [timeout, setTimeout] = useState(30); + + const [state, dispatch] = useReducer(reducer, initialState); + + useEffect(() => { + getQuestionCategories(dispatch); + }, []); + + const auth = useAuth(); + if (!auth) { + throw new Error(USE_AUTH_ERROR_MESSAGE); + } + const { user } = auth; // TODO: if (!user) + + const handleFindMatch = () => { + const socket = io("http://localhost:3002/matching"); + + // TODO: pass socket to matching pages for stop matching button + // socket.disconnect(); + + // socket.on("connect", () => { + // console.log("Socket connected"); + // }); + + socket.emit("match_request", { + user: user!.id, + complexities: complexity, + categories: selectedCategories, + languages: language, + timeout: timeout, + }); + + socket.on("match_found", (partner) => { + console.log(`Potential match partner: ${partner}`); + // TODO: the user may accept / decline the match + // socket.emit("match_accepted"); + // socket.emit("match_declined"); + }); + + socket.on("match_successful", (partner) => { + console.log(`Successful match partner: ${partner}`); + socket.disconnect(); + }); + + socket.on("match_unsuccessful", () => { + console.log("Match unsuccessful"); + socket.disconnect(); + }); - // const [state, dispatch] = useReducer(reducer, initialState); + socket.on("match_timeout", () => { + console.log("Match timeout"); + socket.disconnect(); + }); - // useEffect(() => { - // getQuestionCategories(dispatch); - // }, []); + socket.on("disconnect", () => { + console.log("Oops, something went wrong"); + }); + }; return ( { Specify your question preferences and sit back as we find you the best match. - {/* { height: "auto", objectFit: "contain", }} - /> */} - {/* + { color="primary" fullWidth sx={{ marginTop: 2 }} - disabled={ - isNaN(timeout) || - timeout < minMatchTimeout || - timeout > maxMatchTimeout || - complexity.length == 0 || - selectedCategories.length == 0 || - language.length == 0 - } + // disabled={ + // isNaN(timeout) || + // timeout < minMatchTimeout || + // timeout > maxMatchTimeout || + // complexity.length == 0 || + // selectedCategories.length == 0 || + // language.length == 0 + // } onClick={() => { - alert( - `${complexity}, ${selectedCategories}, ${language}, ${timeout}` - ); + // alert( + // `${complexity}, ${selectedCategories}, ${language}, ${timeout}` + // ); + handleFindMatch(); }} > Find my match! - */} + ); }; From b0f8409ab73fbacc8173bd713f3fa8ba285965ad Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Thu, 10 Oct 2024 14:20:11 +0800 Subject: [PATCH 10/82] Change dockerfile commands and add env variables in docker compose --- backend/question-service/Dockerfile | 2 +- backend/user-service/Dockerfile | 2 +- docker-compose.yml | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/question-service/Dockerfile b/backend/question-service/Dockerfile index 0f8e144f33..958ead0382 100644 --- a/backend/question-service/Dockerfile +++ b/backend/question-service/Dockerfile @@ -10,4 +10,4 @@ COPY . . EXPOSE 3000 -CMD ["npm", "start"] +CMD ["npm", "run", "dev"] diff --git a/backend/user-service/Dockerfile b/backend/user-service/Dockerfile index 0ff78036f7..8fc18a9843 100644 --- a/backend/user-service/Dockerfile +++ b/backend/user-service/Dockerfile @@ -10,4 +10,4 @@ COPY . . EXPOSE 3001 -CMD ["npm", "start"] +CMD ["npm", "run", "dev"] diff --git a/docker-compose.yml b/docker-compose.yml index 7efba767c6..74f527304b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,8 @@ services: user-service: image: peerprep/user-service build: ./backend/user-service + environment: + - CHOKIDAR_USEPOLLING=true env_file: ./backend/user-service/.env ports: - 3001:3001 @@ -17,6 +19,8 @@ services: question-service: image: peerprep/question-service build: ./backend/question-service + environment: + - CHOKIDAR_USEPOLLING=true env_file: ./backend/question-service/.env ports: - 3000:3000 @@ -33,6 +37,8 @@ services: frontend: image: peerprep/frontend build: ./frontend + environment: + - CHOKIDAR_USEPOLLING=true ports: - 5173:5173 depends_on: From 0b41406d66c9e5f9475d53558bd23ced8225c9dd Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Fri, 11 Oct 2024 10:26:44 +0800 Subject: [PATCH 11/82] Add matched and timeout page --- .husky/pre-commit | 17 +++++-- backend/question-service/Dockerfile | 2 +- backend/user-service/Dockerfile | 2 +- docker-compose.yml | 15 ++++++ frontend/src/App.tsx | 7 +++ frontend/src/assets/timeout.svg | 1 + frontend/src/pages/Matched/index.module.css | 10 ++++ frontend/src/pages/Matched/index.tsx | 55 +++++++++++++++++++++ frontend/src/pages/Timeout/index.module.css | 10 ++++ frontend/src/pages/Timeout/index.tsx | 43 ++++++++++++++++ 10 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 frontend/src/assets/timeout.svg create mode 100644 frontend/src/pages/Matched/index.module.css create mode 100644 frontend/src/pages/Matched/index.tsx create mode 100644 frontend/src/pages/Timeout/index.module.css create mode 100644 frontend/src/pages/Timeout/index.tsx diff --git a/.husky/pre-commit b/.husky/pre-commit index 4604c22e58..d346aaf15e 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,7 +1,14 @@ -cd ./frontend && npm run lint && npm run test -cd .. +cd ./frontend +npm run lint +npm run test -- --maxWorkers=50% +cd .. -cd ./backend/user-service && npm run lint && npm run test -cd ../.. +cd ./backend/user-service +npm run lint +npm run test -- --maxWorkers=50% +cd ../.. -cd ./backend/question-service && npm run lint && npm run test +cd ./backend/question-service +npm run lint +npm run test -- --maxWorkers=50% +cd ../.. diff --git a/backend/question-service/Dockerfile b/backend/question-service/Dockerfile index 0f8e144f33..958ead0382 100644 --- a/backend/question-service/Dockerfile +++ b/backend/question-service/Dockerfile @@ -10,4 +10,4 @@ COPY . . EXPOSE 3000 -CMD ["npm", "start"] +CMD ["npm", "run", "dev"] diff --git a/backend/user-service/Dockerfile b/backend/user-service/Dockerfile index 0ff78036f7..8fc18a9843 100644 --- a/backend/user-service/Dockerfile +++ b/backend/user-service/Dockerfile @@ -10,4 +10,4 @@ COPY . . EXPOSE 3001 -CMD ["npm", "start"] +CMD ["npm", "run", "dev"] diff --git a/docker-compose.yml b/docker-compose.yml index fb0cfc7f34..74f527304b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,8 @@ services: user-service: image: peerprep/user-service build: ./backend/user-service + environment: + - CHOKIDAR_USEPOLLING=true env_file: ./backend/user-service/.env ports: - 3001:3001 @@ -9,11 +11,16 @@ services: - mongo networks: - peerprep-network + volumes: + - ./backend/user-service:/user-service + - /user-service/node_modules restart: on-failure question-service: image: peerprep/question-service build: ./backend/question-service + environment: + - CHOKIDAR_USEPOLLING=true env_file: ./backend/question-service/.env ports: - 3000:3000 @@ -22,11 +29,16 @@ services: - user-service networks: - peerprep-network + volumes: + - ./backend/question-service:/question-service + - /question-service/node_modules restart: on-failure frontend: image: peerprep/frontend build: ./frontend + environment: + - CHOKIDAR_USEPOLLING=true ports: - 5173:5173 depends_on: @@ -34,6 +46,9 @@ services: - question-service networks: - peerprep-network + volumes: + - ./frontend:/frontend + - /frontend/node_modules restart: on-failure mongo: diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2cb6704908..06ae72b5a3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,6 +10,8 @@ import Landing from "./pages/Landing"; import Home from "./pages/Home"; import SignUp from "./pages/SignUp"; import LogIn from "./pages/LogIn"; +import Matched from "./pages/Matched"; +import Timeout from "./pages/Timeout"; import ProtectedRoutes from "./components/ProtectedRoutes"; import Layout from "./components/Layout"; import AuthProvider from "./contexts/AuthContext"; @@ -40,6 +42,11 @@ function App() { } /> + }> + Matching...} /> + } /> + } /> + } /> }> diff --git a/frontend/src/assets/timeout.svg b/frontend/src/assets/timeout.svg new file mode 100644 index 0000000000..bb6d027c4a --- /dev/null +++ b/frontend/src/assets/timeout.svg @@ -0,0 +1 @@ +feeling blue \ No newline at end of file diff --git a/frontend/src/pages/Matched/index.module.css b/frontend/src/pages/Matched/index.module.css new file mode 100644 index 0000000000..806d506cc5 --- /dev/null +++ b/frontend/src/pages/Matched/index.module.css @@ -0,0 +1,10 @@ +.fullheight { + flex: 1; +} + +.center { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} diff --git a/frontend/src/pages/Matched/index.tsx b/frontend/src/pages/Matched/index.tsx new file mode 100644 index 0000000000..e030aadaed --- /dev/null +++ b/frontend/src/pages/Matched/index.tsx @@ -0,0 +1,55 @@ +import { useNavigate } from "react-router-dom"; +import AppMargin from "../../components/AppMargin"; +import { Avatar, Box, Button, Stack, Typography } from "@mui/material"; +import classes from "./index.module.css"; + +const Matched: React.FC = () => { + const navigate = useNavigate(); + + return ( + + + It's a match! + + + + + ({ + width: "120px", + height: "2px", + backgroundColor: theme.palette.secondary.contrastText, + margin: "0 10px", + })} + /> + + + + + Practice with @johnjoe? + + + + + + + + ); +}; + +export default Matched; diff --git a/frontend/src/pages/Timeout/index.module.css b/frontend/src/pages/Timeout/index.module.css new file mode 100644 index 0000000000..806d506cc5 --- /dev/null +++ b/frontend/src/pages/Timeout/index.module.css @@ -0,0 +1,10 @@ +.fullheight { + flex: 1; +} + +.center { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} diff --git a/frontend/src/pages/Timeout/index.tsx b/frontend/src/pages/Timeout/index.tsx new file mode 100644 index 0000000000..570996f891 --- /dev/null +++ b/frontend/src/pages/Timeout/index.tsx @@ -0,0 +1,43 @@ +import { useNavigate } from "react-router-dom"; +import AppMargin from "../../components/AppMargin"; +import { Button, Stack, Typography } from "@mui/material"; +import timeout from "../../assets/timeout.svg"; +import classes from "./index.module.css"; + +const Timeout: React.FC = () => { + const navigate = useNavigate(); + + return ( + + + Oops, timeout... + + + + + Unfortunately, we could not find a match. + + + + + + + + + ); +}; + +export default Timeout; From 43a9c9c28ad6fd3a0a8a25cafe46857a1cda21b2 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Fri, 11 Oct 2024 10:38:57 +0800 Subject: [PATCH 12/82] Update husky --- frontend/src/pages/Matched/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/Matched/index.tsx b/frontend/src/pages/Matched/index.tsx index e030aadaed..cb141cd003 100644 --- a/frontend/src/pages/Matched/index.tsx +++ b/frontend/src/pages/Matched/index.tsx @@ -32,7 +32,7 @@ const Matched: React.FC = () => { - Practice with @johnjoe? + Practice with @john? + + ); diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index fcb1251ef7..d17964509d 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -86,12 +86,14 @@ export const COLLABORATIVE_EDITOR_PATH = "/collaborative_editor.png"; // Socket Events export const SOCKET_DISCONNECT = "disconnect"; export const SOCKET_CLIENT_DISCONNECT = "io client disconnect"; +export const SOCKET_RECONNECT_SUCCESS = "reconnect"; export const SOCKET_RECONNECT_FAILED = "reconnect_failed"; // Match Events export const MATCH_REQUEST = "match_request"; export const MATCH_TIMEOUT = "match_timeout"; export const MATCH_FOUND = "match_found"; +export const MATCH_IN_PROGRESS = "match_in_progress"; export const MATCH_RECEIVED = "match_received"; export const MATCH_ACCEPTED = "match_accepted"; export const MATCH_DECLINED = "match_declined"; diff --git a/frontend/src/utils/matchSocket.ts b/frontend/src/utils/matchSocket.ts new file mode 100644 index 0000000000..e58820f6f6 --- /dev/null +++ b/frontend/src/utils/matchSocket.ts @@ -0,0 +1,7 @@ +import { io } from "socket.io-client"; + +const MATCH_SOCKET_URL = "http://localhost:3002"; + +export const matchSocket = io(MATCH_SOCKET_URL, { + reconnectionAttempts: 3, +}); From 523d47c6bee02bb33134226edb5d0b523207b371 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Sat, 12 Oct 2024 02:27:05 +0800 Subject: [PATCH 15/82] Prevent duplicate socket listeners on client side --- frontend/src/handlers/matchHandler.ts | 91 ++++++++++++++++----------- 1 file changed, 53 insertions(+), 38 deletions(-) diff --git a/frontend/src/handlers/matchHandler.ts b/frontend/src/handlers/matchHandler.ts index 388ffdb6d9..ffc1793200 100644 --- a/frontend/src/handlers/matchHandler.ts +++ b/frontend/src/handlers/matchHandler.ts @@ -44,7 +44,6 @@ export class MatchHandler { }; private openConnection = () => { - matchSocket.removeAllListeners(); this.initSocketListeners(); matchSocket.connect(); }; @@ -55,43 +54,59 @@ export class MatchHandler { }; private initSocketListeners = () => { - matchSocket.on(MATCH_FOUND, ({ matchId, user1, user2 }) => { - this.setMatchDetails(matchId, user1, user2); - matchSocket.emit(MATCH_RECEIVED, this.matchId); - }); - - matchSocket.on(MATCH_IN_PROGRESS, () => { - console.log("Matching in progress... / Match already found!"); - }); - - matchSocket.on(MATCH_SUCCESSFUL, () => { - console.log("Match successful"); - this.closeConnection(); - }); - - matchSocket.on(MATCH_UNSUCCESSFUL, () => { - console.log("Match unsuccessful"); - this.closeConnection(); - }); - - matchSocket.on(MATCH_TIMEOUT, () => { - console.log("Match timeout"); - this.closeConnection(); - }); - - matchSocket.on(SOCKET_DISCONNECT, (reason) => { - if (reason !== SOCKET_CLIENT_DISCONNECT) { - console.log("Oops, something went wrong! Reconnecting..."); - } - }); - - matchSocket.io.on(SOCKET_RECONNECT_SUCCESS, () => { - console.log("Reconnected!"); - }); - - matchSocket.io.on(SOCKET_RECONNECT_FAILED, () => { - console.log("Oops, something went wrong! Please try again later."); - }); + if (!matchSocket.hasListeners(MATCH_FOUND)) { + matchSocket.on(MATCH_FOUND, ({ matchId, user1, user2 }) => { + this.setMatchDetails(matchId, user1, user2); + matchSocket.emit(MATCH_RECEIVED, this.matchId); + }); + } + + if (!matchSocket.hasListeners(MATCH_IN_PROGRESS)) { + matchSocket.on(MATCH_IN_PROGRESS, () => { + console.log("Matching in progress... / Match already found!"); + }); + } + + if (!matchSocket.hasListeners(MATCH_SUCCESSFUL)) { + matchSocket.on(MATCH_SUCCESSFUL, () => { + console.log("Match successful"); + this.closeConnection(); + }); + } + + if (!matchSocket.hasListeners(MATCH_UNSUCCESSFUL)) { + matchSocket.on(MATCH_UNSUCCESSFUL, () => { + console.log("Match unsuccessful"); + this.closeConnection(); + }); + } + + if (!matchSocket.hasListeners(MATCH_TIMEOUT)) { + matchSocket.on(MATCH_TIMEOUT, () => { + console.log("Match timeout"); + this.closeConnection(); + }); + } + + if (!matchSocket.hasListeners(SOCKET_DISCONNECT)) { + matchSocket.on(SOCKET_DISCONNECT, (reason) => { + if (reason !== SOCKET_CLIENT_DISCONNECT) { + console.log("Oops, something went wrong! Reconnecting..."); + } + }); + } + + if (!matchSocket.io.hasListeners(SOCKET_RECONNECT_SUCCESS)) { + matchSocket.io.on(SOCKET_RECONNECT_SUCCESS, () => { + console.log("Reconnected!"); + }); + } + + if (!matchSocket.io.hasListeners(SOCKET_RECONNECT_FAILED)) { + matchSocket.io.on(SOCKET_RECONNECT_FAILED, () => { + console.log("Oops, something went wrong! Please try again later."); + }); + } }; findMatch = ( From a1d76e766b181c2e15e408b5926f428cd314afb4 Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Sat, 12 Oct 2024 20:03:14 +0800 Subject: [PATCH 16/82] set up mq - set up rabbitmq - implement matching logic partially --- backend/matching-service/.dockerignore | 5 + backend/matching-service/.env.sample | 3 + backend/matching-service/Dockerfile | 13 ++ backend/matching-service/README.md | 4 + backend/matching-service/config/rabbitmq.ts | 50 ++++++++ backend/matching-service/package-lock.json | 128 ++++++++++++++++++++ backend/matching-service/package.json | 2 + backend/matching-service/server.ts | 38 +++++- backend/matching-service/utils/mq_utils.ts | 61 ++++++++++ docker-compose.yml | 32 +++++ 10 files changed, 331 insertions(+), 5 deletions(-) create mode 100644 backend/matching-service/.dockerignore create mode 100644 backend/matching-service/Dockerfile create mode 100644 backend/matching-service/config/rabbitmq.ts create mode 100644 backend/matching-service/utils/mq_utils.ts diff --git a/backend/matching-service/.dockerignore b/backend/matching-service/.dockerignore new file mode 100644 index 0000000000..4b060f7ed4 --- /dev/null +++ b/backend/matching-service/.dockerignore @@ -0,0 +1,5 @@ +coverage +node_modules +tests +.env* +*.md \ No newline at end of file diff --git a/backend/matching-service/.env.sample b/backend/matching-service/.env.sample index 583d11993e..7530eabbf0 100644 --- a/backend/matching-service/.env.sample +++ b/backend/matching-service/.env.sample @@ -1,4 +1,7 @@ NODE_ENV=development PORT=3002 +RABBITMQ_DEFAULT_USER=admin +RABBITMQ_DEFAULT_PASS=password + ORIGINS=http://localhost:5173,http://127.0.0.1:5173 diff --git a/backend/matching-service/Dockerfile b/backend/matching-service/Dockerfile new file mode 100644 index 0000000000..8e0332a94e --- /dev/null +++ b/backend/matching-service/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /matching-service + +COPY package*.json ./ + +RUN npm ci + +COPY . . + +EXPOSE 3002 + +CMD ["npm", "start"] \ No newline at end of file diff --git a/backend/matching-service/README.md b/backend/matching-service/README.md index aa94908a84..5f5332c50b 100644 --- a/backend/matching-service/README.md +++ b/backend/matching-service/README.md @@ -4,6 +4,10 @@ 1. In the `matching-service` directory, create a copy of the `.env.sample` file and name it `.env`. +2. To set up credentials for RabbitMq, update `RABBITMQ_DEFAULT_USER`, `RABBITMQ_DEFAULT_PASS` of the `.env` file. + +3. You can access RabbitMq management user interface locally with the username in `RABBITMQ_DEFAULT_USER` and password in `RABBITMQ_DEFAULT_PASS` at http://localhost:15672. + ## Running Matching Service without Docker 1. Follow the instructions [here](https://nodejs.org/en/download/package-manager) to set up Node v20. diff --git a/backend/matching-service/config/rabbitmq.ts b/backend/matching-service/config/rabbitmq.ts new file mode 100644 index 0000000000..ece6a4fb3b --- /dev/null +++ b/backend/matching-service/config/rabbitmq.ts @@ -0,0 +1,50 @@ +import amqplib, { Connection } from "amqplib"; +import dotenv from "dotenv"; +import { matchUsers } from "../utils/mq_utils"; + +dotenv.config(); + +let mrConnection: Connection; +const queue = "match_requests"; + +export const connectRabbitMq = async () => { + try { + mrConnection = await amqplib.connect( + `amqp://${process.env.RABBITMQ_DEFAULT_USER}:${process.env.RABBITMQ_DEFAULT_PASS}@rabbitmq` + ); + const consumerChannel = await mrConnection.createChannel(); + await consumerChannel.assertQueue(queue); + + consumerChannel.consume(queue, async (msg) => { + if (msg !== null) { + try { + await matchUsers(msg.content.toString()); + } catch (error) { + console.error(error); + } + consumerChannel.ack(msg); + } + }); + } catch (error) { + console.error(error); + process.exit(1); + } +}; + +type MatchRequestMessage = { + userId: string; + categories: string[] | string; + complexities: string[] | string; + sentTimestamp: number; + ttlInSecs: number; +}; + +export const sendRabbitMq = async (data: MatchRequestMessage) => { + try { + const senderChannel = await mrConnection.createChannel(); + senderChannel.sendToQueue(queue, Buffer.from(JSON.stringify(data))); + } catch (error) { + console.log(error); + throw new Error("Failed to send match request"); + } +}; diff --git a/backend/matching-service/package-lock.json b/backend/matching-service/package-lock.json index 46b4f6c001..f7eea5eaa4 100644 --- a/backend/matching-service/package-lock.json +++ b/backend/matching-service/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "amqplib": "^0.10.4", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", @@ -17,6 +18,7 @@ }, "devDependencies": { "@eslint/js": "^9.12.0", + "@types/amqplib": "^0.10.5", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/jest": "^29.5.13", @@ -35,6 +37,49 @@ "typescript-eslint": "^8.8.1" } }, + "node_modules/@acuminous/bitsyntax": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@acuminous/bitsyntax/-/bitsyntax-0.1.2.tgz", + "integrity": "sha512-29lUK80d1muEQqiUsSo+3A0yP6CdspgC95EnKBMi22Xlwt79i/En4Vr67+cXhU+cZjbti3TgGGC5wy1stIywVQ==", + "license": "MIT", + "dependencies": { + "buffer-more-ints": "~1.0.0", + "debug": "^4.3.4", + "safe-buffer": "~5.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/@acuminous/bitsyntax/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@acuminous/bitsyntax/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/@acuminous/bitsyntax/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -1845,6 +1890,16 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/amqplib": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.5.tgz", + "integrity": "sha512-/cSykxROY7BWwDoi4Y4/jLAuZTshZxd8Ey1QYa/VaXriMotBDoou7V/twJiOSHzU6t1Kp1AHAUXGCgqq+6DNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2472,6 +2527,21 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/amqplib": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.4.tgz", + "integrity": "sha512-DMZ4eCEjAVdX1II2TfIUpJhfKAuoCeDIo/YyETbfAqehHTXxxs7WOOd+N1Xxr4cKhx12y23zk8/os98FxlZHrw==", + "license": "MIT", + "dependencies": { + "@acuminous/bitsyntax": "^0.1.2", + "buffer-more-ints": "~1.0.0", + "readable-stream": "1.x >=1.1.9", + "url-parse": "~1.5.10" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2779,6 +2849,12 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/buffer-more-ints": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==", + "license": "MIT" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -3008,6 +3084,12 @@ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -4277,6 +4359,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5690,6 +5778,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5738,6 +5832,18 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, + "node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5747,6 +5853,12 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -6062,6 +6174,12 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -6565,6 +6683,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/backend/matching-service/package.json b/backend/matching-service/package.json index 650e4f6e4a..36aef786bb 100644 --- a/backend/matching-service/package.json +++ b/backend/matching-service/package.json @@ -13,6 +13,7 @@ "author": "", "license": "ISC", "dependencies": { + "amqplib": "^0.10.4", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", @@ -21,6 +22,7 @@ }, "devDependencies": { "@eslint/js": "^9.12.0", + "@types/amqplib": "^0.10.5", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/jest": "^29.5.13", diff --git a/backend/matching-service/server.ts b/backend/matching-service/server.ts index a5d3980945..cd2b9774ee 100644 --- a/backend/matching-service/server.ts +++ b/backend/matching-service/server.ts @@ -1,11 +1,39 @@ import app from "./app.ts"; +import { connectRabbitMq } from "./config/rabbitmq.ts"; const PORT = process.env.PORT || 3002; if (process.env.NODE_ENV !== "test") { - app.listen(PORT, () => { - console.log( - `Matching service server listening on http://localhost:${PORT}` - ); - }); + connectRabbitMq() + .then(() => { + console.log("RabbitMq connected!"); + + app.listen(PORT, () => { + console.log( + `Matching service server listening on http://localhost:${PORT}` + ); + }); + + //can use this to test if rabbitmq works for you (import sendRabbitMq from rabbitmq.ts first) + /*const message1 = { + userId: "1", + categories: "Algorithms", + complexities: "Easy", + sentTimestamp: Date.now(), + ttlInSecs: 30, + }; + sendRabbitMq(message1); + const message2 = { + userId: "2", + categories: "Algorithms", + complexities: "Medium", + sentTimestamp: Date.now(), + ttlInSecs: 30, + }; + sendRabbitMq(message2);*/ + }) + .catch((err) => { + console.error("Failed to connect to RabbitMq"); + console.error(err); + }); } diff --git a/backend/matching-service/utils/mq_utils.ts b/backend/matching-service/utils/mq_utils.ts new file mode 100644 index 0000000000..547603b915 --- /dev/null +++ b/backend/matching-service/utils/mq_utils.ts @@ -0,0 +1,61 @@ +type MatchRequestMessage = { + userId: string; + categories: string[] | string; + complexities: string[] | string; + sentTimestamp: number; + ttlInSecs: number; +}; + +const matchingRequests = new Map(); + +export const matchUsers = async (newRequest: string) => { + const newRequestJson = JSON.parse(newRequest); + for (const [uid, pendingRequest] of matchingRequests) { + if (isExpired(pendingRequest)) { + matchingRequests.delete(uid); + continue; + } + if (isExpired(newRequestJson)) { + return; + } + + if (isMatch(newRequestJson, pendingRequest)) { + //TODO message websocket + /*try { + + } catch (error) { + console.log("Failed to send message to websocket:", error); + }*/ + console.log(`matched ${uid} and ${newRequestJson.userId}`); + return; + } + } + matchingRequests.set(newRequestJson.userId, newRequestJson); +}; + +const isExpired = (data: MatchRequestMessage): boolean => { + return Date.now() - data.sentTimestamp >= data.ttlInSecs * 1000; +}; + +const isMatch = ( + req1: MatchRequestMessage, + req2: MatchRequestMessage +): boolean => { + const cat1 = Array.isArray(req1.categories) + ? req1.categories + : [req1.categories]; + const cat2 = Array.isArray(req2.categories) + ? req2.categories + : [req2.categories]; + const comp1 = Array.isArray(req1.complexities) + ? req1.complexities + : [req1.complexities]; + const comp2 = Array.isArray(req2.complexities) + ? req2.complexities + : [req2.complexities]; + + const hasCommonCat = cat1.some((elem) => cat2.includes(elem)); + const hasCommonComp = comp1.some((elem) => comp2.includes(elem)); + + return hasCommonCat && hasCommonComp; +}; diff --git a/docker-compose.yml b/docker-compose.yml index fb0cfc7f34..312429baad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,21 @@ services: - peerprep-network restart: on-failure + matching-service: + image: peerprep/matching-service + build: ./backend/matching-service + env_file: ./backend/matching-service/.env + ports: + - 3002:3002 + depends_on: + rabbitmq: + condition: service_healthy + user-service: + condition: service_started + networks: + - peerprep-network + restart: on-failure + frontend: image: peerprep/frontend build: ./frontend @@ -59,6 +74,23 @@ services: - mongo env_file: ./backend/.env + rabbitmq: + image: rabbitmq:4.0-management + container_name: rabbitmq + restart: always + ports: + - 5672:5672 + - 15672:15672 + networks: + - peerprep-network + env_file: ./backend/matching-service/.env + healthcheck: + test: rabbitmq-diagnostics check_port_connectivity + interval: 10s + timeout: 10s + retries: 10 + + volumes: mongo-data: From 000536b20acce61a6b23c1deb3ae502a36c3f6f7 Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Sat, 12 Oct 2024 20:05:51 +0800 Subject: [PATCH 17/82] format files --- backend/matching-service/.dockerignore | 2 +- backend/matching-service/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/matching-service/.dockerignore b/backend/matching-service/.dockerignore index 4b060f7ed4..4abc77f632 100644 --- a/backend/matching-service/.dockerignore +++ b/backend/matching-service/.dockerignore @@ -2,4 +2,4 @@ coverage node_modules tests .env* -*.md \ No newline at end of file +*.md diff --git a/backend/matching-service/Dockerfile b/backend/matching-service/Dockerfile index 8e0332a94e..58314525e0 100644 --- a/backend/matching-service/Dockerfile +++ b/backend/matching-service/Dockerfile @@ -10,4 +10,4 @@ COPY . . EXPOSE 3002 -CMD ["npm", "start"] \ No newline at end of file +CMD ["npm", "start"] From 8e7589c1db1ab503f12ce830f879d82212c68afd Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Sat, 12 Oct 2024 20:36:27 +0800 Subject: [PATCH 18/82] Update README --- backend/matching-service/README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/matching-service/README.md b/backend/matching-service/README.md index 5f5332c50b..7f7f899f80 100644 --- a/backend/matching-service/README.md +++ b/backend/matching-service/README.md @@ -4,19 +4,21 @@ 1. In the `matching-service` directory, create a copy of the `.env.sample` file and name it `.env`. -2. To set up credentials for RabbitMq, update `RABBITMQ_DEFAULT_USER`, `RABBITMQ_DEFAULT_PASS` of the `.env` file. +2. To set up credentials for RabbitMq, update `RABBITMQ_DEFAULT_USER`, `RABBITMQ_DEFAULT_PASS` of the `.env` file. If you are running Matching Service individually, update `RABBITMQ_DEFAULT_USER` and `RABBITMQ_DEFAULT_PASS` to RabbitMq's default username `guest` and password `guest` respectively. If you are running Matching Service with the other services using the docker-compose file, you can update `RABBITMQ_DEFAULT_USER` and `RABBITMQ_DEFAULT_PASS` to whatever you want. 3. You can access RabbitMq management user interface locally with the username in `RABBITMQ_DEFAULT_USER` and password in `RABBITMQ_DEFAULT_PASS` at http://localhost:15672. -## Running Matching Service without Docker +## Running Matching Service Individually with Docker -1. Follow the instructions [here](https://nodejs.org/en/download/package-manager) to set up Node v20. +1. Set up and run RabbitMq locally on your computer with the command `docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:4.0-management`. -2. Open Command Line/Terminal and navigate into the `matching-service` directory. +2. Follow the instructions [here](https://nodejs.org/en/download/package-manager) to set up Node v20. -3. Run the command: `npm install`. This will install all the necessary dependencies. +3. Open Command Line/Terminal and navigate into the `matching-service` directory. -4. Run the command `npm start` to start the Matching Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. +4. Run the command: `npm install`. This will install all the necessary dependencies. + +5. Run the command `npm start` to start the Matching Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. ## After running From a3c9b43524418df2990bf6b95dbd101dbe2496a8 Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Sat, 12 Oct 2024 21:19:07 +0800 Subject: [PATCH 19/82] update rabbitmq env --- backend/matching-service/.env.sample | 13 ++++++++++--- backend/matching-service/README.md | 7 ++++--- backend/matching-service/config/rabbitmq.ts | 4 +--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/backend/matching-service/.env.sample b/backend/matching-service/.env.sample index 7530eabbf0..3ee6b5d853 100644 --- a/backend/matching-service/.env.sample +++ b/backend/matching-service/.env.sample @@ -1,7 +1,14 @@ NODE_ENV=development PORT=3002 -RABBITMQ_DEFAULT_USER=admin -RABBITMQ_DEFAULT_PASS=password - ORIGINS=http://localhost:5173,http://127.0.0.1:5173 + + +## FOR RABBITMQ, comment out the variables under whichever use case (1) or (2) that is not applicable +# (1) RabbitMq for running matching service individually +RABBITMQ_ADDR=amqp://localhost:5672 #comment out if use case is (2) + +# (2) RabbitMq for running matching service with other services using docker compose +RABBITMQ_DEFAULT_USER=admin #comment out if use case is (1) +RABBITMQ_DEFAULT_PASS=password #comment out if use case is (1) +RABBITMQ_ADDR=amqp://admin:password@rabbitmq:5672 #comment out if use case is (1) diff --git a/backend/matching-service/README.md b/backend/matching-service/README.md index 7f7f899f80..beeeffae49 100644 --- a/backend/matching-service/README.md +++ b/backend/matching-service/README.md @@ -2,11 +2,12 @@ ## Setting-up Matching Service -1. In the `matching-service` directory, create a copy of the `.env.sample` file and name it `.env`. +1. In the `matching-service` directory, create a copy of the `.env.sample` file and name it `.env`. If you are looking to run matching service with the other services using docker-compose, comment out the variable `RABBITMQ_ADDR` under use case (1) in the .env file. Otherwise, if you are looking to run matching service individually, comment out the variables `RABBITMQ_DEFAULT_USER`, `RABBITMQ_DEFAULT_PASS` and `RABBITMQ_ADDR` under use case (2) in the .env file. -2. To set up credentials for RabbitMq, update `RABBITMQ_DEFAULT_USER`, `RABBITMQ_DEFAULT_PASS` of the `.env` file. If you are running Matching Service individually, update `RABBITMQ_DEFAULT_USER` and `RABBITMQ_DEFAULT_PASS` to RabbitMq's default username `guest` and password `guest` respectively. If you are running Matching Service with the other services using the docker-compose file, you can update `RABBITMQ_DEFAULT_USER` and `RABBITMQ_DEFAULT_PASS` to whatever you want. +2. If you are running matching service together with other services using docker-compose, to set up credentials for RabbitMq, update the RabbitMq variables in the `.env` file. Update `RABBITMQ_DEFAULT_USER` and `RABBITMQ_DEFAULT_PASS` to what you want, then update `RABBITMQ_ADDR` to be `amqp://:@rabbitmq:5672`. +You can access RabbitMq management user interface locally with the username in `RABBITMQ_DEFAULT_USER` and password in `RABBITMQ_DEFAULT_PASS` at http://localhost:15672. -3. You can access RabbitMq management user interface locally with the username in `RABBITMQ_DEFAULT_USER` and password in `RABBITMQ_DEFAULT_PASS` at http://localhost:15672. +3. If you are running matching service individually, you can access RabbitMq management user interface locally with the username `guest` and password `guest` at http://localhost:15672. ## Running Matching Service Individually with Docker diff --git a/backend/matching-service/config/rabbitmq.ts b/backend/matching-service/config/rabbitmq.ts index ece6a4fb3b..21733047b1 100644 --- a/backend/matching-service/config/rabbitmq.ts +++ b/backend/matching-service/config/rabbitmq.ts @@ -9,9 +9,7 @@ const queue = "match_requests"; export const connectRabbitMq = async () => { try { - mrConnection = await amqplib.connect( - `amqp://${process.env.RABBITMQ_DEFAULT_USER}:${process.env.RABBITMQ_DEFAULT_PASS}@rabbitmq` - ); + mrConnection = await amqplib.connect(`${process.env.RABBITMQ_ADDR}`); const consumerChannel = await mrConnection.createChannel(); await consumerChannel.assertQueue(queue); From 93c4984264aee62b44c1f450ac48030bcbd60f31 Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Sat, 12 Oct 2024 21:22:53 +0800 Subject: [PATCH 20/82] update readme --- backend/matching-service/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/matching-service/README.md b/backend/matching-service/README.md index beeeffae49..03ae4aa4df 100644 --- a/backend/matching-service/README.md +++ b/backend/matching-service/README.md @@ -7,7 +7,7 @@ 2. If you are running matching service together with other services using docker-compose, to set up credentials for RabbitMq, update the RabbitMq variables in the `.env` file. Update `RABBITMQ_DEFAULT_USER` and `RABBITMQ_DEFAULT_PASS` to what you want, then update `RABBITMQ_ADDR` to be `amqp://:@rabbitmq:5672`. You can access RabbitMq management user interface locally with the username in `RABBITMQ_DEFAULT_USER` and password in `RABBITMQ_DEFAULT_PASS` at http://localhost:15672. -3. If you are running matching service individually, you can access RabbitMq management user interface locally with the username `guest` and password `guest` at http://localhost:15672. +3. If you are running matching service individually, you do not need to make any changes to `RABBITMQ_ADDR`. You can access RabbitMq management user interface locally with the username `guest` and password `guest` at http://localhost:15672. ## Running Matching Service Individually with Docker From 667f44cada1d8a443c9da8b990c546f9f5f8004c Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Sat, 12 Oct 2024 21:52:52 +0800 Subject: [PATCH 21/82] standardise matching-service docker with other services --- backend/matching-service/Dockerfile | 2 +- docker-compose.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/matching-service/Dockerfile b/backend/matching-service/Dockerfile index 58314525e0..55b971d60b 100644 --- a/backend/matching-service/Dockerfile +++ b/backend/matching-service/Dockerfile @@ -10,4 +10,4 @@ COPY . . EXPOSE 3002 -CMD ["npm", "start"] +CMD ["npm", "run", "dev"] diff --git a/docker-compose.yml b/docker-compose.yml index 312429baad..863709bb80 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,6 +27,8 @@ services: matching-service: image: peerprep/matching-service build: ./backend/matching-service + environment: + - CHOKIDAR_USEPOLLING=true env_file: ./backend/matching-service/.env ports: - 3002:3002 @@ -37,6 +39,9 @@ services: condition: service_started networks: - peerprep-network + volumes: + - ./backend/matching-service:/matching-service + - /matching-service/node_modules restart: on-failure frontend: From 7b9cb1637f31282bd61950de69c5d7e8582f2775 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Mon, 14 Oct 2024 00:27:28 +0800 Subject: [PATCH 22/82] Merge development branch --- .husky/pre-commit | 17 ++- backend/matching-service/.dockerignore | 5 + backend/matching-service/.env.sample | 10 ++ backend/matching-service/Dockerfile | 13 ++ backend/matching-service/README.md | 19 ++- backend/matching-service/config/rabbitmq.ts | 48 +++++++ backend/matching-service/package-lock.json | 128 ++++++++++++++++++ backend/matching-service/package.json | 2 + backend/matching-service/server.ts | 38 +++++- backend/matching-service/utils/mq_utils.ts | 61 +++++++++ backend/question-service/Dockerfile | 2 +- backend/user-service/Dockerfile | 2 +- docker-compose.yml | 52 +++++++ frontend/jest.config.ts | 2 +- frontend/setupTest.ts | 1 + frontend/src/App.tsx | 12 +- frontend/src/assets/matching.svg | 9 ++ frontend/src/assets/timeout.svg | 1 + .../src/components/Navbar/Navbar.test.tsx | 8 +- frontend/src/components/Navbar/index.tsx | 112 ++++++++------- .../ProfileDetails/ProfileDetailstest.tsx | 1 - .../QuestionCategoryAutoComplete.test.tsx | 1 - .../QuestionDetail/QuestionDetail.test.tsx | 7 +- .../QuestionImage/QuestionImage.test.tsx | 3 +- .../QuestionImageContainer.test.tsx | 23 ++-- .../QuestionImageDialog.test.tsx | 7 +- .../QuestionMarkdown.test.tsx | 7 +- .../ServerError/ServerError.test.tsx | 1 - frontend/src/components/Timer/Timer.test.tsx | 24 ++++ frontend/src/components/Timer/index.tsx | 58 ++++++++ frontend/src/pages/Matched/index.module.css | 10 ++ frontend/src/pages/Matched/index.tsx | 55 ++++++++ frontend/src/pages/Matching/index.module.css | 10 ++ frontend/src/pages/Matching/index.tsx | 37 +++++ frontend/src/pages/Timeout/index.module.css | 10 ++ frontend/src/pages/Timeout/index.tsx | 43 ++++++ frontend/src/utils/url.ts | 4 + 37 files changed, 743 insertions(+), 100 deletions(-) create mode 100644 backend/matching-service/.dockerignore create mode 100644 backend/matching-service/Dockerfile create mode 100644 backend/matching-service/config/rabbitmq.ts create mode 100644 backend/matching-service/utils/mq_utils.ts create mode 100644 frontend/setupTest.ts create mode 100644 frontend/src/assets/matching.svg create mode 100644 frontend/src/assets/timeout.svg create mode 100644 frontend/src/components/Timer/Timer.test.tsx create mode 100644 frontend/src/components/Timer/index.tsx create mode 100644 frontend/src/pages/Matched/index.module.css create mode 100644 frontend/src/pages/Matched/index.tsx create mode 100644 frontend/src/pages/Matching/index.module.css create mode 100644 frontend/src/pages/Matching/index.tsx create mode 100644 frontend/src/pages/Timeout/index.module.css create mode 100644 frontend/src/pages/Timeout/index.tsx create mode 100644 frontend/src/utils/url.ts diff --git a/.husky/pre-commit b/.husky/pre-commit index 4604c22e58..d346aaf15e 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,7 +1,14 @@ -cd ./frontend && npm run lint && npm run test -cd .. +cd ./frontend +npm run lint +npm run test -- --maxWorkers=50% +cd .. -cd ./backend/user-service && npm run lint && npm run test -cd ../.. +cd ./backend/user-service +npm run lint +npm run test -- --maxWorkers=50% +cd ../.. -cd ./backend/question-service && npm run lint && npm run test +cd ./backend/question-service +npm run lint +npm run test -- --maxWorkers=50% +cd ../.. diff --git a/backend/matching-service/.dockerignore b/backend/matching-service/.dockerignore new file mode 100644 index 0000000000..4abc77f632 --- /dev/null +++ b/backend/matching-service/.dockerignore @@ -0,0 +1,5 @@ +coverage +node_modules +tests +.env* +*.md diff --git a/backend/matching-service/.env.sample b/backend/matching-service/.env.sample index 583d11993e..3ee6b5d853 100644 --- a/backend/matching-service/.env.sample +++ b/backend/matching-service/.env.sample @@ -2,3 +2,13 @@ NODE_ENV=development PORT=3002 ORIGINS=http://localhost:5173,http://127.0.0.1:5173 + + +## FOR RABBITMQ, comment out the variables under whichever use case (1) or (2) that is not applicable +# (1) RabbitMq for running matching service individually +RABBITMQ_ADDR=amqp://localhost:5672 #comment out if use case is (2) + +# (2) RabbitMq for running matching service with other services using docker compose +RABBITMQ_DEFAULT_USER=admin #comment out if use case is (1) +RABBITMQ_DEFAULT_PASS=password #comment out if use case is (1) +RABBITMQ_ADDR=amqp://admin:password@rabbitmq:5672 #comment out if use case is (1) diff --git a/backend/matching-service/Dockerfile b/backend/matching-service/Dockerfile new file mode 100644 index 0000000000..55b971d60b --- /dev/null +++ b/backend/matching-service/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /matching-service + +COPY package*.json ./ + +RUN npm ci + +COPY . . + +EXPOSE 3002 + +CMD ["npm", "run", "dev"] diff --git a/backend/matching-service/README.md b/backend/matching-service/README.md index aa94908a84..03ae4aa4df 100644 --- a/backend/matching-service/README.md +++ b/backend/matching-service/README.md @@ -2,17 +2,24 @@ ## Setting-up Matching Service -1. In the `matching-service` directory, create a copy of the `.env.sample` file and name it `.env`. +1. In the `matching-service` directory, create a copy of the `.env.sample` file and name it `.env`. If you are looking to run matching service with the other services using docker-compose, comment out the variable `RABBITMQ_ADDR` under use case (1) in the .env file. Otherwise, if you are looking to run matching service individually, comment out the variables `RABBITMQ_DEFAULT_USER`, `RABBITMQ_DEFAULT_PASS` and `RABBITMQ_ADDR` under use case (2) in the .env file. -## Running Matching Service without Docker +2. If you are running matching service together with other services using docker-compose, to set up credentials for RabbitMq, update the RabbitMq variables in the `.env` file. Update `RABBITMQ_DEFAULT_USER` and `RABBITMQ_DEFAULT_PASS` to what you want, then update `RABBITMQ_ADDR` to be `amqp://:@rabbitmq:5672`. +You can access RabbitMq management user interface locally with the username in `RABBITMQ_DEFAULT_USER` and password in `RABBITMQ_DEFAULT_PASS` at http://localhost:15672. -1. Follow the instructions [here](https://nodejs.org/en/download/package-manager) to set up Node v20. +3. If you are running matching service individually, you do not need to make any changes to `RABBITMQ_ADDR`. You can access RabbitMq management user interface locally with the username `guest` and password `guest` at http://localhost:15672. -2. Open Command Line/Terminal and navigate into the `matching-service` directory. +## Running Matching Service Individually with Docker -3. Run the command: `npm install`. This will install all the necessary dependencies. +1. Set up and run RabbitMq locally on your computer with the command `docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:4.0-management`. -4. Run the command `npm start` to start the Matching Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. +2. Follow the instructions [here](https://nodejs.org/en/download/package-manager) to set up Node v20. + +3. Open Command Line/Terminal and navigate into the `matching-service` directory. + +4. Run the command: `npm install`. This will install all the necessary dependencies. + +5. Run the command `npm start` to start the Matching Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. ## After running diff --git a/backend/matching-service/config/rabbitmq.ts b/backend/matching-service/config/rabbitmq.ts new file mode 100644 index 0000000000..21733047b1 --- /dev/null +++ b/backend/matching-service/config/rabbitmq.ts @@ -0,0 +1,48 @@ +import amqplib, { Connection } from "amqplib"; +import dotenv from "dotenv"; +import { matchUsers } from "../utils/mq_utils"; + +dotenv.config(); + +let mrConnection: Connection; +const queue = "match_requests"; + +export const connectRabbitMq = async () => { + try { + mrConnection = await amqplib.connect(`${process.env.RABBITMQ_ADDR}`); + const consumerChannel = await mrConnection.createChannel(); + await consumerChannel.assertQueue(queue); + + consumerChannel.consume(queue, async (msg) => { + if (msg !== null) { + try { + await matchUsers(msg.content.toString()); + } catch (error) { + console.error(error); + } + consumerChannel.ack(msg); + } + }); + } catch (error) { + console.error(error); + process.exit(1); + } +}; + +type MatchRequestMessage = { + userId: string; + categories: string[] | string; + complexities: string[] | string; + sentTimestamp: number; + ttlInSecs: number; +}; + +export const sendRabbitMq = async (data: MatchRequestMessage) => { + try { + const senderChannel = await mrConnection.createChannel(); + senderChannel.sendToQueue(queue, Buffer.from(JSON.stringify(data))); + } catch (error) { + console.log(error); + throw new Error("Failed to send match request"); + } +}; diff --git a/backend/matching-service/package-lock.json b/backend/matching-service/package-lock.json index 2be822fc7c..1c23f4e4f7 100644 --- a/backend/matching-service/package-lock.json +++ b/backend/matching-service/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "amqplib": "^0.10.4", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", @@ -19,6 +20,7 @@ }, "devDependencies": { "@eslint/js": "^9.12.0", + "@types/amqplib": "^0.10.5", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/jest": "^29.5.13", @@ -39,6 +41,49 @@ "typescript-eslint": "^8.8.1" } }, + "node_modules/@acuminous/bitsyntax": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@acuminous/bitsyntax/-/bitsyntax-0.1.2.tgz", + "integrity": "sha512-29lUK80d1muEQqiUsSo+3A0yP6CdspgC95EnKBMi22Xlwt79i/En4Vr67+cXhU+cZjbti3TgGGC5wy1stIywVQ==", + "license": "MIT", + "dependencies": { + "buffer-more-ints": "~1.0.0", + "debug": "^4.3.4", + "safe-buffer": "~5.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/@acuminous/bitsyntax/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@acuminous/bitsyntax/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/@acuminous/bitsyntax/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -1854,6 +1899,16 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/amqplib": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.5.tgz", + "integrity": "sha512-/cSykxROY7BWwDoi4Y4/jLAuZTshZxd8Ey1QYa/VaXriMotBDoou7V/twJiOSHzU6t1Kp1AHAUXGCgqq+6DNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2500,6 +2555,21 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/amqplib": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.4.tgz", + "integrity": "sha512-DMZ4eCEjAVdX1II2TfIUpJhfKAuoCeDIo/YyETbfAqehHTXxxs7WOOd+N1Xxr4cKhx12y23zk8/os98FxlZHrw==", + "license": "MIT", + "dependencies": { + "@acuminous/bitsyntax": "^0.1.2", + "buffer-more-ints": "~1.0.0", + "readable-stream": "1.x >=1.1.9", + "url-parse": "~1.5.10" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2815,6 +2885,12 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/buffer-more-ints": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==", + "license": "MIT" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -3044,6 +3120,12 @@ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -4370,6 +4452,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5783,6 +5871,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5831,6 +5925,18 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, + "node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5840,6 +5946,12 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -6256,6 +6368,12 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -6758,6 +6876,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/backend/matching-service/package.json b/backend/matching-service/package.json index 8f19150f1b..eff673d2ff 100644 --- a/backend/matching-service/package.json +++ b/backend/matching-service/package.json @@ -13,6 +13,7 @@ "author": "", "license": "ISC", "dependencies": { + "amqplib": "^0.10.4", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", @@ -23,6 +24,7 @@ }, "devDependencies": { "@eslint/js": "^9.12.0", + "@types/amqplib": "^0.10.5", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/jest": "^29.5.13", diff --git a/backend/matching-service/server.ts b/backend/matching-service/server.ts index d85e658606..bbf891e7d7 100644 --- a/backend/matching-service/server.ts +++ b/backend/matching-service/server.ts @@ -2,6 +2,7 @@ import http from "http"; import app, { allowedOrigins } from "./app.ts"; import { handleWebsocketMatchEvents } from "./src/handlers/websocketHandler.ts"; import { Server } from "socket.io"; +import { connectRabbitMq } from "./config/rabbitmq.ts"; const server = http.createServer(app); export const io = new Server(server, { @@ -19,9 +20,36 @@ io.on("connection", (socket) => { const PORT = process.env.PORT || 3002; if (process.env.NODE_ENV !== "test") { - server.listen(PORT, () => { - console.log( - `Matching service server listening on http://localhost:${PORT}` - ); - }); + connectRabbitMq() + .then(() => { + console.log("RabbitMq connected!"); + + server.listen(PORT, () => { + console.log( + `Matching service server listening on http://localhost:${PORT}` + ); + }); + + //can use this to test if rabbitmq works for you (import sendRabbitMq from rabbitmq.ts first) + /*const message1 = { + userId: "1", + categories: "Algorithms", + complexities: "Easy", + sentTimestamp: Date.now(), + ttlInSecs: 30, + }; + sendRabbitMq(message1); + const message2 = { + userId: "2", + categories: "Algorithms", + complexities: "Medium", + sentTimestamp: Date.now(), + ttlInSecs: 30, + }; + sendRabbitMq(message2);*/ + }) + .catch((err) => { + console.error("Failed to connect to RabbitMq"); + console.error(err); + }); } diff --git a/backend/matching-service/utils/mq_utils.ts b/backend/matching-service/utils/mq_utils.ts new file mode 100644 index 0000000000..547603b915 --- /dev/null +++ b/backend/matching-service/utils/mq_utils.ts @@ -0,0 +1,61 @@ +type MatchRequestMessage = { + userId: string; + categories: string[] | string; + complexities: string[] | string; + sentTimestamp: number; + ttlInSecs: number; +}; + +const matchingRequests = new Map(); + +export const matchUsers = async (newRequest: string) => { + const newRequestJson = JSON.parse(newRequest); + for (const [uid, pendingRequest] of matchingRequests) { + if (isExpired(pendingRequest)) { + matchingRequests.delete(uid); + continue; + } + if (isExpired(newRequestJson)) { + return; + } + + if (isMatch(newRequestJson, pendingRequest)) { + //TODO message websocket + /*try { + + } catch (error) { + console.log("Failed to send message to websocket:", error); + }*/ + console.log(`matched ${uid} and ${newRequestJson.userId}`); + return; + } + } + matchingRequests.set(newRequestJson.userId, newRequestJson); +}; + +const isExpired = (data: MatchRequestMessage): boolean => { + return Date.now() - data.sentTimestamp >= data.ttlInSecs * 1000; +}; + +const isMatch = ( + req1: MatchRequestMessage, + req2: MatchRequestMessage +): boolean => { + const cat1 = Array.isArray(req1.categories) + ? req1.categories + : [req1.categories]; + const cat2 = Array.isArray(req2.categories) + ? req2.categories + : [req2.categories]; + const comp1 = Array.isArray(req1.complexities) + ? req1.complexities + : [req1.complexities]; + const comp2 = Array.isArray(req2.complexities) + ? req2.complexities + : [req2.complexities]; + + const hasCommonCat = cat1.some((elem) => cat2.includes(elem)); + const hasCommonComp = comp1.some((elem) => comp2.includes(elem)); + + return hasCommonCat && hasCommonComp; +}; diff --git a/backend/question-service/Dockerfile b/backend/question-service/Dockerfile index 0f8e144f33..958ead0382 100644 --- a/backend/question-service/Dockerfile +++ b/backend/question-service/Dockerfile @@ -10,4 +10,4 @@ COPY . . EXPOSE 3000 -CMD ["npm", "start"] +CMD ["npm", "run", "dev"] diff --git a/backend/user-service/Dockerfile b/backend/user-service/Dockerfile index 0ff78036f7..8fc18a9843 100644 --- a/backend/user-service/Dockerfile +++ b/backend/user-service/Dockerfile @@ -10,4 +10,4 @@ COPY . . EXPOSE 3001 -CMD ["npm", "start"] +CMD ["npm", "run", "dev"] diff --git a/docker-compose.yml b/docker-compose.yml index fb0cfc7f34..da863855ff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,8 @@ services: user-service: image: peerprep/user-service build: ./backend/user-service + environment: + - CHOKIDAR_USEPOLLING=true env_file: ./backend/user-service/.env ports: - 3001:3001 @@ -9,11 +11,16 @@ services: - mongo networks: - peerprep-network + volumes: + - ./backend/user-service:/user-service + - /user-service/node_modules restart: on-failure question-service: image: peerprep/question-service build: ./backend/question-service + environment: + - CHOKIDAR_USEPOLLING=true env_file: ./backend/question-service/.env ports: - 3000:3000 @@ -22,11 +29,36 @@ services: - user-service networks: - peerprep-network + volumes: + - ./backend/question-service:/question-service + - /question-service/node_modules + restart: on-failure + + matching-service: + image: peerprep/matching-service + build: ./backend/matching-service + environment: + - CHOKIDAR_USEPOLLING=true + env_file: ./backend/matching-service/.env + ports: + - 3002:3002 + depends_on: + rabbitmq: + condition: service_healthy + user-service: + condition: service_started + networks: + - peerprep-network + volumes: + - ./backend/matching-service:/matching-service + - /matching-service/node_modules restart: on-failure frontend: image: peerprep/frontend build: ./frontend + environment: + - CHOKIDAR_USEPOLLING=true ports: - 5173:5173 depends_on: @@ -34,6 +66,9 @@ services: - question-service networks: - peerprep-network + volumes: + - ./frontend:/frontend + - /frontend/node_modules restart: on-failure mongo: @@ -59,6 +94,23 @@ services: - mongo env_file: ./backend/.env + rabbitmq: + image: rabbitmq:4.0-management + container_name: rabbitmq + restart: always + ports: + - 5672:5672 + - 15672:15672 + networks: + - peerprep-network + env_file: ./backend/matching-service/.env + healthcheck: + test: rabbitmq-diagnostics check_port_connectivity + interval: 10s + timeout: 10s + retries: 10 + + volumes: mongo-data: diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts index d002a6e54c..a9c6917456 100644 --- a/frontend/jest.config.ts +++ b/frontend/jest.config.ts @@ -142,7 +142,7 @@ const config: Config = { // setupFiles: [], // A list of paths to modules that run some code to configure or set up the testing framework before each test - // setupFilesAfterEnv: [], + setupFilesAfterEnv: ["/setupTest.ts"], // The number of seconds after which a test is considered as slow and reported as such in the results. // slowTestThreshold: 5, diff --git a/frontend/setupTest.ts b/frontend/setupTest.ts new file mode 100644 index 0000000000..d0de870dc5 --- /dev/null +++ b/frontend/setupTest.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom"; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2cb6704908..2a05ce7053 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,7 +10,10 @@ import Landing from "./pages/Landing"; import Home from "./pages/Home"; import SignUp from "./pages/SignUp"; import LogIn from "./pages/LogIn"; +import Matched from "./pages/Matched"; +import Timeout from "./pages/Timeout"; import ProtectedRoutes from "./components/ProtectedRoutes"; +import Matching from "./pages/Matching"; import Layout from "./components/Layout"; import AuthProvider from "./contexts/AuthContext"; import ProfileContextProvider from "./contexts/ProfileContext"; @@ -40,10 +43,15 @@ function App() { } /> + }> + } /> + } /> + } /> + } /> - }> - }> + } /> + } /> ); diff --git a/frontend/src/assets/matching.svg b/frontend/src/assets/matching.svg new file mode 100644 index 0000000000..fbb0724816 --- /dev/null +++ b/frontend/src/assets/matching.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/assets/timeout.svg b/frontend/src/assets/timeout.svg new file mode 100644 index 0000000000..bb6d027c4a --- /dev/null +++ b/frontend/src/assets/timeout.svg @@ -0,0 +1 @@ +feeling blue \ No newline at end of file diff --git a/frontend/src/components/Navbar/Navbar.test.tsx b/frontend/src/components/Navbar/Navbar.test.tsx index 247459787f..111148b505 100644 --- a/frontend/src/components/Navbar/Navbar.test.tsx +++ b/frontend/src/components/Navbar/Navbar.test.tsx @@ -1,5 +1,4 @@ import { fireEvent, render, screen } from "@testing-library/react"; -import "@testing-library/jest-dom"; import axios from "axios"; import { faker } from "@faker-js/faker"; import * as hooks from "../../contexts/AuthContext"; @@ -46,6 +45,7 @@ describe("Navigation routes", () => { signup: jest.fn(), login: jest.fn(), logout: jest.fn(), + setUser: jest.fn(), loading: false, user: { id: "1", @@ -76,6 +76,7 @@ describe("Unauthenticated user", () => { login: jest.fn(), logout: jest.fn(), loading: false, + setUser: jest.fn(), user: null, })); render( @@ -92,6 +93,7 @@ describe("Unauthenticated user", () => { signup: jest.fn(), login: jest.fn(), logout: jest.fn(), + setUser: jest.fn(), loading: false, user: null, })); @@ -133,6 +135,7 @@ describe("Authenticated user", () => { signup: jest.fn(), login: jest.fn(), logout: jest.fn(), + setUser: jest.fn(), loading: false, user: { id: "1", @@ -182,6 +185,7 @@ describe("Authenticated user", () => { signup: jest.fn(), login: jest.fn(), logout: jest.fn(), + setUser: jest.fn(), loading: false, user: { id: "1", @@ -235,6 +239,7 @@ describe("Authenticated user", () => { signup: jest.fn(), login: jest.fn(), logout: jest.fn(), + setUser: jest.fn(), loading: false, user: { id: "1", @@ -287,6 +292,7 @@ describe("Authenticated user", () => { login: jest.fn(), logout: jest.fn(), loading: false, + setUser: jest.fn(), user: { id: "1", username, diff --git a/frontend/src/components/Navbar/index.tsx b/frontend/src/components/Navbar/index.tsx index ce782e6a63..be696441bb 100644 --- a/frontend/src/components/Navbar/index.tsx +++ b/frontend/src/components/Navbar/index.tsx @@ -18,6 +18,7 @@ import { useNavigate, useLocation } from "react-router-dom"; import { useAuth } from "../../contexts/AuthContext"; import { useState } from "react"; import { USE_AUTH_ERROR_MESSAGE } from "../../utils/constants"; +import { isMatchingPage } from "../../utils/url"; type NavbarItem = { label: string; link: string; needsLogin: boolean }; @@ -62,63 +63,76 @@ const Navbar: React.FC = (props) => { }} > - + navigate("/")} > PeerPrep - - {navbarItems - .filter((item) => !item.needsLogin || (item.needsLogin && user)) - .map((item) => ( - - {path == item.link ? {item.label} : item.label} - - ))} - {user ? ( - <> - - - - - - - { - handleClose(); - navigate(`/profile/${user.id}`); - }} + {!isMatchingPage(path) ? ( + + {navbarItems + .filter((item) => !item.needsLogin || (item.needsLogin && user)) + .map((item) => ( + - Profile - - Logout - - - ) : ( - <> - - - - )} - + {path == item.link ? {item.label} : item.label} + + ))} + {user ? ( + <> + + + + + + + { + handleClose(); + navigate(`/profile/${user.id}`); + }} + > + Profile + + Logout + + + ) : ( + <> + + + + )} + + ) : ( + + )} diff --git a/frontend/src/components/ProfileDetails/ProfileDetailstest.tsx b/frontend/src/components/ProfileDetails/ProfileDetailstest.tsx index cddf47b020..d790d44a0f 100644 --- a/frontend/src/components/ProfileDetails/ProfileDetailstest.tsx +++ b/frontend/src/components/ProfileDetails/ProfileDetailstest.tsx @@ -1,5 +1,4 @@ import { render, screen } from "@testing-library/react"; -import "@testing-library/jest-dom"; import { faker } from "@faker-js/faker"; import ProfileDetails from "."; diff --git a/frontend/src/components/QuestionCategoryAutoComplete/QuestionCategoryAutoComplete.test.tsx b/frontend/src/components/QuestionCategoryAutoComplete/QuestionCategoryAutoComplete.test.tsx index ee69dc0d13..ad1fb95732 100644 --- a/frontend/src/components/QuestionCategoryAutoComplete/QuestionCategoryAutoComplete.test.tsx +++ b/frontend/src/components/QuestionCategoryAutoComplete/QuestionCategoryAutoComplete.test.tsx @@ -1,5 +1,4 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; -import "@testing-library/jest-dom"; import QuestionCategoryAutoComplete from "."; jest.mock("../../utils/api", () => ({ diff --git a/frontend/src/components/QuestionDetail/QuestionDetail.test.tsx b/frontend/src/components/QuestionDetail/QuestionDetail.test.tsx index 0515fb89d8..02e8ba433a 100644 --- a/frontend/src/components/QuestionDetail/QuestionDetail.test.tsx +++ b/frontend/src/components/QuestionDetail/QuestionDetail.test.tsx @@ -1,5 +1,4 @@ import { render, screen } from "@testing-library/react"; -import "@testing-library/jest-dom"; import QuestionDetail from "."; jest.mock("@uiw/react-md-editor", () => ({ @@ -25,7 +24,7 @@ describe("Question details", () => { complexity={complexity} categories={categories} description={description} - />, + /> ); expect(screen.getByText(title)).toBeInTheDocument(); }); @@ -41,7 +40,7 @@ describe("Question details", () => { complexity={complexity} categories={categories} description={description} - />, + /> ); expect(screen.getByText(complexity)).toBeInTheDocument(); }); @@ -57,7 +56,7 @@ describe("Question details", () => { complexity={complexity} categories={categories} description={description} - />, + /> ); expect(screen.getByText(categories[0])).toBeInTheDocument(); expect(screen.getByText(categories[1])).toBeInTheDocument(); diff --git a/frontend/src/components/QuestionImage/QuestionImage.test.tsx b/frontend/src/components/QuestionImage/QuestionImage.test.tsx index 287a585b74..8d33e517be 100644 --- a/frontend/src/components/QuestionImage/QuestionImage.test.tsx +++ b/frontend/src/components/QuestionImage/QuestionImage.test.tsx @@ -1,5 +1,4 @@ import { fireEvent, render, screen } from "@testing-library/react"; -import "@testing-library/jest-dom"; import QuestionImage from "."; Object.assign(navigator, { @@ -27,7 +26,7 @@ describe("Question Image", () => { fireEvent.click(copyButton); expect(navigator.clipboard.writeText).toHaveBeenCalledWith( - `![image](${url})`, + `![image](${url})` ); }); diff --git a/frontend/src/components/QuestionImageContainer/QuestionImageContainer.test.tsx b/frontend/src/components/QuestionImageContainer/QuestionImageContainer.test.tsx index 6aea0c67ad..317628bbba 100644 --- a/frontend/src/components/QuestionImageContainer/QuestionImageContainer.test.tsx +++ b/frontend/src/components/QuestionImageContainer/QuestionImageContainer.test.tsx @@ -1,5 +1,4 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; -import "@testing-library/jest-dom"; import QuestionImageContainer from "."; import { questionClient } from "../../utils/api"; @@ -27,7 +26,7 @@ describe("Question Image Container", () => { Object.defineProperty(window, "localStorage", { value: mockLocalStorage, writable: true, - }), + }) ); it("Question Image Container is rendered with no uploaded images", () => { @@ -38,11 +37,11 @@ describe("Question Image Container", () => { , + /> ); const uploadImageMessage = screen.getByText( - "Click to upload images. The maximum image size accepted is 5MB.", + "Click to upload images. The maximum image size accepted is 5MB." ); expect(uploadImageMessage).toBeInTheDocument(); }); @@ -55,7 +54,7 @@ describe("Question Image Container", () => { , + /> ); const images = screen.getAllByAltText("question image"); @@ -70,7 +69,7 @@ describe("Question Image Container", () => { , + /> ); const image = screen.getByAltText("question image"); @@ -110,7 +109,7 @@ describe("Question Image Container", () => { , + /> ); const file = new File(["file"], "file.png", { type: "image/png" }); @@ -126,7 +125,7 @@ describe("Question Image Container", () => { Authorization: `Bearer ${mockLocalStorage.getItem("token")}`, "Content-Type": "multipart/form-data", }, - }), + }) ); expect(setUploadedImagesUrl).toHaveBeenCalled(); @@ -141,7 +140,7 @@ describe("Question Image Container", () => { , + /> ); const file = new File(["file"], "file.txt", { type: "text/plain" }); @@ -159,7 +158,7 @@ describe("Question Image Container", () => { , + /> ); const input = screen.getByTestId("file-input"); @@ -185,7 +184,7 @@ describe("Question Image Container", () => { , + /> ); const file = new File(["file"], "file.png", { type: "image/png" }); @@ -205,7 +204,7 @@ describe("Question Image Container", () => { , + /> ); const input = screen.getByTestId("file-input"); diff --git a/frontend/src/components/QuestionImageDialog/QuestionImageDialog.test.tsx b/frontend/src/components/QuestionImageDialog/QuestionImageDialog.test.tsx index 9e1059e6ef..912f512fad 100644 --- a/frontend/src/components/QuestionImageDialog/QuestionImageDialog.test.tsx +++ b/frontend/src/components/QuestionImageDialog/QuestionImageDialog.test.tsx @@ -1,5 +1,4 @@ import { render, screen } from "@testing-library/react"; -import "@testing-library/jest-dom"; import QuestionImageDialog from "."; describe("Question Image Dialog", () => { @@ -12,7 +11,7 @@ describe("Question Image Dialog", () => { value={url} open={true} handleClose={mockHandleClose} - />, + /> ); const image = screen.getByAltText("question image enlarged"); @@ -27,7 +26,7 @@ describe("Question Image Dialog", () => { value={url} open={false} handleClose={mockHandleClose} - />, + /> ); expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); @@ -39,7 +38,7 @@ describe("Question Image Dialog", () => { value={url} open={true} handleClose={mockHandleClose} - />, + /> ); const closeButton = screen.getByRole("button", { name: "close" }); diff --git a/frontend/src/components/QuestionMarkdown/QuestionMarkdown.test.tsx b/frontend/src/components/QuestionMarkdown/QuestionMarkdown.test.tsx index d39bcfcb9a..ebb0b8a40e 100644 --- a/frontend/src/components/QuestionMarkdown/QuestionMarkdown.test.tsx +++ b/frontend/src/components/QuestionMarkdown/QuestionMarkdown.test.tsx @@ -1,5 +1,4 @@ import { fireEvent, render, screen } from "@testing-library/react"; -import "@testing-library/jest-dom"; import QuestionMarkdown from "."; jest.mock("@uiw/react-md-editor", () => ({ @@ -29,7 +28,7 @@ describe("Question Markdown", () => { , + /> ); const textarea = screen.getByPlaceholderText("Description"); @@ -44,7 +43,7 @@ describe("Question Markdown", () => { , + /> ); const textarea = screen.getByPlaceholderText("Description"); @@ -58,7 +57,7 @@ describe("Question Markdown", () => { , + /> ); const textarea = screen.getByPlaceholderText("Description"); diff --git a/frontend/src/components/ServerError/ServerError.test.tsx b/frontend/src/components/ServerError/ServerError.test.tsx index 10e801a31a..284321f0d7 100644 --- a/frontend/src/components/ServerError/ServerError.test.tsx +++ b/frontend/src/components/ServerError/ServerError.test.tsx @@ -1,5 +1,4 @@ import { render, screen } from "@testing-library/react"; -import "@testing-library/jest-dom"; import ServerError from "."; describe("Not found", () => { diff --git a/frontend/src/components/Timer/Timer.test.tsx b/frontend/src/components/Timer/Timer.test.tsx new file mode 100644 index 0000000000..ad1417fdcc --- /dev/null +++ b/frontend/src/components/Timer/Timer.test.tsx @@ -0,0 +1,24 @@ +import { render, screen } from "@testing-library/react"; +import Timer from "."; + +describe("Timer", () => { + it("Timer is rendered", () => { + render(); + expect(screen.getByTestId("timer")).toBeInTheDocument(); + }); + + it("Timer is rendered with correct seconds displayed", () => { + render(); + expect(screen.getByText("00:30")).toBeInTheDocument(); + }); + + it("Timer is rendered with correct minutes displayed", () => { + render(); + expect(screen.getByText("01:00")).toBeInTheDocument(); + }); + + it("Timer is rendered with correct minutes and seconds displayed", () => { + render(); + expect(screen.getByText("01:10")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/Timer/index.tsx b/frontend/src/components/Timer/index.tsx new file mode 100644 index 0000000000..70d34a8687 --- /dev/null +++ b/frontend/src/components/Timer/index.tsx @@ -0,0 +1,58 @@ +import { + Box, + CircularProgress, + CircularProgressProps, + Typography, +} from "@mui/material"; + +type TimerProps = { totalTime: number; timeLeft: number }; + +const Timer: React.FC = (props) => { + const { totalTime, timeLeft, thickness, size, ...rest } = props; + const percentage = (timeLeft / totalTime) * 100; + const minutes = Math.floor(timeLeft / 60); + const seconds = timeLeft % 60; + const formattedSeconds = String(seconds).padStart(2, "0"); + const formattedMinutes = String(minutes).padStart(2, "0"); + return ( + + + + + + {formattedMinutes}:{formattedSeconds} + + + + ); +}; + +export default Timer; diff --git a/frontend/src/pages/Matched/index.module.css b/frontend/src/pages/Matched/index.module.css new file mode 100644 index 0000000000..806d506cc5 --- /dev/null +++ b/frontend/src/pages/Matched/index.module.css @@ -0,0 +1,10 @@ +.fullheight { + flex: 1; +} + +.center { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} diff --git a/frontend/src/pages/Matched/index.tsx b/frontend/src/pages/Matched/index.tsx new file mode 100644 index 0000000000..cb141cd003 --- /dev/null +++ b/frontend/src/pages/Matched/index.tsx @@ -0,0 +1,55 @@ +import { useNavigate } from "react-router-dom"; +import AppMargin from "../../components/AppMargin"; +import { Avatar, Box, Button, Stack, Typography } from "@mui/material"; +import classes from "./index.module.css"; + +const Matched: React.FC = () => { + const navigate = useNavigate(); + + return ( + + + It's a match! + + + + + ({ + width: "120px", + height: "2px", + backgroundColor: theme.palette.secondary.contrastText, + margin: "0 10px", + })} + /> + + + + + Practice with @john? + + + + + + + + ); +}; + +export default Matched; diff --git a/frontend/src/pages/Matching/index.module.css b/frontend/src/pages/Matching/index.module.css new file mode 100644 index 0000000000..806d506cc5 --- /dev/null +++ b/frontend/src/pages/Matching/index.module.css @@ -0,0 +1,10 @@ +.fullheight { + flex: 1; +} + +.center { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} diff --git a/frontend/src/pages/Matching/index.tsx b/frontend/src/pages/Matching/index.tsx new file mode 100644 index 0000000000..4c6b0ffc93 --- /dev/null +++ b/frontend/src/pages/Matching/index.tsx @@ -0,0 +1,37 @@ +import React, { useEffect } from "react"; +import AppMargin from "../../components/AppMargin"; +import { Stack, Typography } from "@mui/material"; +import matching from "../../assets/matching.svg"; +import classes from "./index.module.css"; +import Timer from "../../components/Timer"; + +const Matching: React.FC = () => { + const totalTime = 30; + const [timeLeft, setTimeLeft] = React.useState(totalTime); + + useEffect(() => { + const timer = setInterval(() => { + setTimeLeft((prevTime) => (prevTime <= 0 ? 0 : prevTime - 1)); + }, 1000); + return () => clearInterval(timer); + }, []); + + return ( + + + + Finding your practice partner + + + + + + ); +}; + +export default Matching; diff --git a/frontend/src/pages/Timeout/index.module.css b/frontend/src/pages/Timeout/index.module.css new file mode 100644 index 0000000000..806d506cc5 --- /dev/null +++ b/frontend/src/pages/Timeout/index.module.css @@ -0,0 +1,10 @@ +.fullheight { + flex: 1; +} + +.center { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} diff --git a/frontend/src/pages/Timeout/index.tsx b/frontend/src/pages/Timeout/index.tsx new file mode 100644 index 0000000000..570996f891 --- /dev/null +++ b/frontend/src/pages/Timeout/index.tsx @@ -0,0 +1,43 @@ +import { useNavigate } from "react-router-dom"; +import AppMargin from "../../components/AppMargin"; +import { Button, Stack, Typography } from "@mui/material"; +import timeout from "../../assets/timeout.svg"; +import classes from "./index.module.css"; + +const Timeout: React.FC = () => { + const navigate = useNavigate(); + + return ( + + + Oops, timeout... + + + + + Unfortunately, we could not find a match. + + + + + + + + + ); +}; + +export default Timeout; diff --git a/frontend/src/utils/url.ts b/frontend/src/utils/url.ts new file mode 100644 index 0000000000..2fd68afc0f --- /dev/null +++ b/frontend/src/utils/url.ts @@ -0,0 +1,4 @@ +export const isMatchingPage = (path: string) => { + const pattern = /^(?\/matching)(?\/.*)*$/; + return pattern.test(path); +}; From af8fb3da9affda689f86b0e60dc09f8bfe594df3 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Tue, 15 Oct 2024 16:38:00 +0800 Subject: [PATCH 23/82] Integrate matching websocket in BE and FE --- backend/matching-service/config/rabbitmq.ts | 22 +- backend/matching-service/server.ts | 20 +- .../src/handlers/matchHandler.ts | 93 +++---- .../src/handlers/queueHandler.ts | 49 ---- .../src/handlers/websocketHandler.ts | 39 +-- .../matching-service/src/types/matchTypes.ts | 8 +- .../matching-service/src/utils/constants.ts | 8 +- .../matching-service/src/utils/mq_utils.ts | 48 ++++ backend/matching-service/utils/mq_utils.ts | 61 ----- frontend/src/App.tsx | 67 ++--- frontend/src/components/Navbar/index.tsx | 18 +- frontend/src/contexts/MatchContext.tsx | 245 ++++++++++++++++++ frontend/src/handlers/matchHandler.ts | 139 ---------- .../src/pages/CollabSandbox/index.module.css | 10 + frontend/src/pages/CollabSandbox/index.tsx | 32 +++ frontend/src/pages/Home/index.tsx | 22 +- frontend/src/pages/Matched/index.tsx | 42 ++- frontend/src/pages/Matching/index.tsx | 31 ++- frontend/src/pages/Timeout/index.tsx | 17 +- frontend/src/utils/constants.ts | 20 +- 20 files changed, 553 insertions(+), 438 deletions(-) delete mode 100644 backend/matching-service/src/handlers/queueHandler.ts create mode 100644 backend/matching-service/src/utils/mq_utils.ts delete mode 100644 backend/matching-service/utils/mq_utils.ts create mode 100644 frontend/src/contexts/MatchContext.tsx delete mode 100644 frontend/src/handlers/matchHandler.ts create mode 100644 frontend/src/pages/CollabSandbox/index.module.css create mode 100644 frontend/src/pages/CollabSandbox/index.tsx diff --git a/backend/matching-service/config/rabbitmq.ts b/backend/matching-service/config/rabbitmq.ts index 21733047b1..d93278aefb 100644 --- a/backend/matching-service/config/rabbitmq.ts +++ b/backend/matching-service/config/rabbitmq.ts @@ -1,6 +1,7 @@ import amqplib, { Connection } from "amqplib"; import dotenv from "dotenv"; -import { matchUsers } from "../utils/mq_utils"; +import { matchUsers } from "../src/utils/mq_utils"; +import { MatchItem } from "../src/types/matchTypes"; dotenv.config(); @@ -15,11 +16,7 @@ export const connectRabbitMq = async () => { consumerChannel.consume(queue, async (msg) => { if (msg !== null) { - try { - await matchUsers(msg.content.toString()); - } catch (error) { - console.error(error); - } + matchUsers(msg.content.toString()); consumerChannel.ack(msg); } }); @@ -29,20 +26,13 @@ export const connectRabbitMq = async () => { } }; -type MatchRequestMessage = { - userId: string; - categories: string[] | string; - complexities: string[] | string; - sentTimestamp: number; - ttlInSecs: number; -}; - -export const sendRabbitMq = async (data: MatchRequestMessage) => { +export const sendRabbitMq = async (data: MatchItem): Promise => { try { const senderChannel = await mrConnection.createChannel(); senderChannel.sendToQueue(queue, Buffer.from(JSON.stringify(data))); + return true; } catch (error) { console.log(error); - throw new Error("Failed to send match request"); + return false; } }; diff --git a/backend/matching-service/server.ts b/backend/matching-service/server.ts index bbf891e7d7..7a9e7618c5 100644 --- a/backend/matching-service/server.ts +++ b/backend/matching-service/server.ts @@ -10,7 +10,7 @@ export const io = new Server(server, { origin: allowedOrigins, methods: ["GET", "POST"], }, - connectionStateRecovery: {}, + connectionStateRecovery: {}, // TODO: rejoin room? }); io.on("connection", (socket) => { @@ -29,24 +29,6 @@ if (process.env.NODE_ENV !== "test") { `Matching service server listening on http://localhost:${PORT}` ); }); - - //can use this to test if rabbitmq works for you (import sendRabbitMq from rabbitmq.ts first) - /*const message1 = { - userId: "1", - categories: "Algorithms", - complexities: "Easy", - sentTimestamp: Date.now(), - ttlInSecs: 30, - }; - sendRabbitMq(message1); - const message2 = { - userId: "2", - categories: "Algorithms", - complexities: "Medium", - sentTimestamp: Date.now(), - ttlInSecs: 30, - }; - sendRabbitMq(message2);*/ }) .catch((err) => { console.error("Failed to connect to RabbitMq"); diff --git a/backend/matching-service/src/handlers/matchHandler.ts b/backend/matching-service/src/handlers/matchHandler.ts index 24e3ab8248..e1e0b49b9e 100644 --- a/backend/matching-service/src/handlers/matchHandler.ts +++ b/backend/matching-service/src/handlers/matchHandler.ts @@ -2,48 +2,50 @@ import { io } from "../../server"; import { Match, MatchItem, MatchRequest } from "../types/matchTypes"; import { v4 as uuidv4 } from "uuid"; import { - MATCH_ACCEPTANCE_TIMEOUT, MATCH_FOUND, MATCH_IN_PROGRESS, MATCH_SUCCESSFUL, - MATCH_TIMEOUT, MATCH_UNSUCCESSFUL, + MATCH_REQUEST_ERROR, } from "../utils/constants"; -import { appendToMatchQueue } from "./queueHandler"; import { Socket } from "socket.io"; +import { sendRabbitMq } from "../../config/rabbitmq"; const matches: Match = {}; +export const userSockets: Map = new Map(); -export const createMatchItem = (socket: Socket, matchRequest: MatchRequest) => { +export const createMatchItem = async ( + socket: Socket, + matchRequest: MatchRequest +): Promise => { const { user, complexities, categories, languages, timeout } = matchRequest; - const matchTimeout = setTimeout(() => { - socket.emit(MATCH_TIMEOUT); - }, timeout * 1000); + if (userSockets.has(user.id)) { + console.log(`user request exists: ${user.username}`); + socket.emit(MATCH_IN_PROGRESS); + return false; + } + + userSockets.set(user.id, socket); const matchQueueItem: MatchItem = { - socket: socket, user: user, complexities: complexities, categories: categories, languages: languages, - timeout: matchTimeout, + sentTimestamp: Date.now(), + ttlInSecs: timeout, acceptedMatch: false, }; - const result = appendToMatchQueue(matchQueueItem); + const result = await sendRabbitMq(matchQueueItem); if (!result) { - socket.emit(MATCH_IN_PROGRESS); + socket.emit(MATCH_REQUEST_ERROR); } + return result; }; -export const createMatch = (matchItems: MatchItem[]) => { - const matchItem1 = matchItems[0]; - const matchItem2 = matchItems[1]; - - clearTimeout(matchItem1.timeout); - clearTimeout(matchItem2.timeout); - +export const createMatch = (matchItem1: MatchItem, matchItem2: MatchItem) => { const matchId = uuidv4(); matches[matchId] = { item1: matchItem1, @@ -52,8 +54,9 @@ export const createMatch = (matchItems: MatchItem[]) => { accepted: false, }; - matchItem1.socket.join(matchId); - matchItem2.socket.join(matchId); + // check for disconnection? or just send the match (disconnected user will timeout anyway) + userSockets.get(matchItem1.user.id)!.join(matchId); + userSockets.get(matchItem2.user.id)!.join(matchId); io.to(matchId).emit(MATCH_FOUND, { matchId: matchId, user1: matchItem1.user, @@ -61,20 +64,6 @@ export const createMatch = (matchItems: MatchItem[]) => { }); }; -export const setMatchTimeout = (matchId: string) => { - const match = matches[matchId]; - if (!match) { - return; - } - - const timeout = setTimeout(() => { - io.to(matchId).emit(MATCH_UNSUCCESSFUL); - delete matches[matchId]; - }, MATCH_ACCEPTANCE_TIMEOUT); - - match.timeout = timeout; -}; - export const handleMatchAcceptance = (matchId: string) => { const match = matches[matchId]; if (!match) { @@ -82,27 +71,39 @@ export const handleMatchAcceptance = (matchId: string) => { } if (match.accepted) { - clearTimeout(match.timeout!); io.to(matchId).emit(MATCH_SUCCESSFUL); - delete matches[matchId]; } else { match.accepted = true; } }; -export const handleMatchDecline = (matchId: string) => { +export const handleRematch = ( + socket: Socket, + matchId: string, + rematchRequest: MatchRequest +) => { const match = matches[matchId]; - if (!match) { - return; + if (match) { + delete matches[matchId]; + socket.to(matchId).emit(MATCH_UNSUCCESSFUL); } - clearTimeout(match.timeout!); - io.to(matchId).emit(MATCH_UNSUCCESSFUL); - delete matches[matchId]; + createMatchItem(socket, rematchRequest); }; -export const isUserMatched = (userId: string): boolean => { - return !!Object.values(matches).find( - (match) => match.item1.user.id === userId || match.item2.user.id === userId - ); +export const handleMatchTermination = (terminatedSocket: Socket) => { + for (const [uid, socket] of userSockets) { + if (socket.id === terminatedSocket.id) { + userSockets.delete(uid); + break; + } + } + + // TODO: no access to rooms + const matchId = Array.from(terminatedSocket.rooms)[1]; + const match = matches[matchId]; + if (match) { + delete matches[matchId]; + terminatedSocket.to(matchId).emit(MATCH_UNSUCCESSFUL); + } }; diff --git a/backend/matching-service/src/handlers/queueHandler.ts b/backend/matching-service/src/handlers/queueHandler.ts deleted file mode 100644 index f9bceff426..0000000000 --- a/backend/matching-service/src/handlers/queueHandler.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { MatchItem } from "../types/matchTypes"; -import { createMatch, isUserMatched } from "./matchHandler"; - -/* Basic queue set-up for websocket testing (feel free to replace with the actual queueing mechanism) */ - -const matchQueue: MatchItem[] = []; - -setInterval(() => { - findMatch(); -}, 5000); - -const findMatch = () => { - matchQueue.forEach((item) => - console.log(`${item.user.username} is ${item.socket.connected}`) - ); - if (matchQueue.length < 2) { - return; - } - - const matchedItems = []; - while (matchedItems.length < 2 && matchQueue.length > 0) { - const matchItem = matchQueue.shift()!; - if (matchItem.socket.connected) { - matchedItems.push(matchItem); - } - } - - if (matchedItems.length === 2) { - createMatch(matchedItems); - } else { - matchedItems.reverse().forEach((item) => matchQueue.unshift(item)); - } -}; - -export const appendToMatchQueue = (item: MatchItem) => { - if ( - matchQueue.find( - (queueItem) => - queueItem.user.id === item.user.id && queueItem.socket.connected - ) || - isUserMatched(item.user.id) - ) { - return false; - } - - console.log(item.user.username); - matchQueue.push(item); - return true; -}; diff --git a/backend/matching-service/src/handlers/websocketHandler.ts b/backend/matching-service/src/handlers/websocketHandler.ts index 5db340bf7c..c7dae38598 100644 --- a/backend/matching-service/src/handlers/websocketHandler.ts +++ b/backend/matching-service/src/handlers/websocketHandler.ts @@ -1,32 +1,41 @@ import { Socket } from "socket.io"; import { MATCH_ACCEPTED, - MATCH_DECLINED, - MATCH_RECEIVED, + REMATCH_REQUEST, MATCH_REQUEST, + SOCKET_CLIENT_DISCONNECT, + SOCKET_DISCONNECT, } from "../utils/constants"; import { MatchRequest } from "../types/matchTypes"; import { createMatchItem, handleMatchAcceptance, - handleMatchDecline, - setMatchTimeout, + handleMatchTermination, + handleRematch, } from "./matchHandler"; export const handleWebsocketMatchEvents = (socket: Socket) => { - socket.on(MATCH_REQUEST, (matchRequest: MatchRequest) => { - createMatchItem(socket, matchRequest); - }); + socket.on( + MATCH_REQUEST, + async (matchRequest: MatchRequest, callback: (result: boolean) => void) => { + const result = await createMatchItem(socket, matchRequest); + callback(result); + } + ); - socket.on(MATCH_RECEIVED, (matchId: string) => { - setMatchTimeout(matchId); - }); + socket.on(MATCH_ACCEPTED, (matchId: string) => + handleMatchAcceptance(matchId) + ); - socket.on(MATCH_ACCEPTED, (matchId: string) => { - handleMatchAcceptance(matchId); - }); + socket.on(REMATCH_REQUEST, (matchId: string, rematchRequest: MatchRequest) => + handleRematch(socket, matchId, rematchRequest) + ); - socket.on(MATCH_DECLINED, (matchId: string) => { - handleMatchDecline(matchId); + // TODO: handle client reconnect failure + socket.on(SOCKET_DISCONNECT, (reason) => { + if (reason === SOCKET_CLIENT_DISCONNECT) { + console.log("Client manually disconnected"); + handleMatchTermination(socket); + } }); }; diff --git a/backend/matching-service/src/types/matchTypes.ts b/backend/matching-service/src/types/matchTypes.ts index 79b07f4891..98c0ccd1de 100644 --- a/backend/matching-service/src/types/matchTypes.ts +++ b/backend/matching-service/src/types/matchTypes.ts @@ -1,9 +1,7 @@ -import { Socket } from "socket.io"; - export interface MatchUser { id: string; username: string; - profile: string; + profile?: string; } export interface MatchRequest { @@ -15,12 +13,12 @@ export interface MatchRequest { } export interface MatchItem { - socket: Socket; user: MatchUser; complexities: string[]; categories: string[]; languages: string[]; - timeout: NodeJS.Timeout; + sentTimestamp: number; + ttlInSecs: number; acceptedMatch: boolean; } diff --git a/backend/matching-service/src/utils/constants.ts b/backend/matching-service/src/utils/constants.ts index 13440425be..920ea58632 100644 --- a/backend/matching-service/src/utils/constants.ts +++ b/backend/matching-service/src/utils/constants.ts @@ -1,12 +1,12 @@ /* Websocket Match Events */ export const MATCH_REQUEST = "match_request"; -export const MATCH_TIMEOUT = "match_timeout"; +export const MATCH_REQUEST_ERROR = "match_request_error"; export const MATCH_FOUND = "match_found"; export const MATCH_IN_PROGRESS = "match_in_progress"; -export const MATCH_RECEIVED = "match_received"; export const MATCH_ACCEPTED = "match_accepted"; -export const MATCH_DECLINED = "match_declined"; +export const REMATCH_REQUEST = "rematch_request"; export const MATCH_SUCCESSFUL = "match_successful"; export const MATCH_UNSUCCESSFUL = "match_unsuccessful"; -export const MATCH_ACCEPTANCE_TIMEOUT = 10000; +export const SOCKET_DISCONNECT = "disconnect"; +export const SOCKET_CLIENT_DISCONNECT = "client namespace disconnect"; diff --git a/backend/matching-service/src/utils/mq_utils.ts b/backend/matching-service/src/utils/mq_utils.ts new file mode 100644 index 0000000000..c899d7e64b --- /dev/null +++ b/backend/matching-service/src/utils/mq_utils.ts @@ -0,0 +1,48 @@ +import { createMatch, userSockets } from "../handlers/matchHandler"; +import { MatchItem } from "../types/matchTypes"; + +const matchingRequests = new Map(); + +export const matchUsers = (newRequest: string) => { + const newRequestJson = JSON.parse(newRequest) as MatchItem; + const newRequestUid = newRequestJson.user.id; + for (const [uid, pendingRequest] of matchingRequests) { + if ( + isExpired(pendingRequest) || + !userSockets.has(uid) || + uid === newRequestUid + ) { + matchingRequests.delete(uid); + continue; + } + if (isExpired(newRequestJson) || !userSockets.has(newRequestUid)) { + return; + } + + if (isMatch(newRequestJson, pendingRequest)) { + createMatch(pendingRequest, newRequestJson); + console.log(`matched ${uid} and ${newRequestUid}`); + return; + } + } + matchingRequests.set(newRequestUid, newRequestJson); +}; + +const isExpired = (data: MatchItem): boolean => { + return Date.now() - data.sentTimestamp >= data.ttlInSecs * 1000; +}; + +const isMatch = (req1: MatchItem, req2: MatchItem): boolean => { + const hasCommonCategory = req1.categories.some((elem) => + req1.categories.includes(elem) + ); + const hasCommonComplexity = req1.complexities.some((elem) => + req2.complexities.includes(elem) + ); + const hasCommonLanguage = req1.languages.some((elem) => + req2.languages.includes(elem) + ); + + // return hasCommonCategory && hasCommonComplexity && hasCommonLanguage; + return true; +}; diff --git a/backend/matching-service/utils/mq_utils.ts b/backend/matching-service/utils/mq_utils.ts deleted file mode 100644 index 547603b915..0000000000 --- a/backend/matching-service/utils/mq_utils.ts +++ /dev/null @@ -1,61 +0,0 @@ -type MatchRequestMessage = { - userId: string; - categories: string[] | string; - complexities: string[] | string; - sentTimestamp: number; - ttlInSecs: number; -}; - -const matchingRequests = new Map(); - -export const matchUsers = async (newRequest: string) => { - const newRequestJson = JSON.parse(newRequest); - for (const [uid, pendingRequest] of matchingRequests) { - if (isExpired(pendingRequest)) { - matchingRequests.delete(uid); - continue; - } - if (isExpired(newRequestJson)) { - return; - } - - if (isMatch(newRequestJson, pendingRequest)) { - //TODO message websocket - /*try { - - } catch (error) { - console.log("Failed to send message to websocket:", error); - }*/ - console.log(`matched ${uid} and ${newRequestJson.userId}`); - return; - } - } - matchingRequests.set(newRequestJson.userId, newRequestJson); -}; - -const isExpired = (data: MatchRequestMessage): boolean => { - return Date.now() - data.sentTimestamp >= data.ttlInSecs * 1000; -}; - -const isMatch = ( - req1: MatchRequestMessage, - req2: MatchRequestMessage -): boolean => { - const cat1 = Array.isArray(req1.categories) - ? req1.categories - : [req1.categories]; - const cat2 = Array.isArray(req2.categories) - ? req2.categories - : [req2.categories]; - const comp1 = Array.isArray(req1.complexities) - ? req1.complexities - : [req1.complexities]; - const comp2 = Array.isArray(req2.complexities) - ? req2.complexities - : [req2.complexities]; - - const hasCommonCat = cat1.some((elem) => cat2.includes(elem)); - const hasCommonComp = comp1.some((elem) => comp2.includes(elem)); - - return hasCommonCat && hasCommonComp; -}; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2a05ce7053..157e9033b1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,42 +17,49 @@ import Matching from "./pages/Matching"; import Layout from "./components/Layout"; import AuthProvider from "./contexts/AuthContext"; import ProfileContextProvider from "./contexts/ProfileContext"; +import MatchProvider from "./contexts/MatchContext"; +import CollabSandbox from "./pages/CollabSandbox"; function App() { return ( - - }> - } /> - }> - } /> - - - } /> - } /> - }> - } /> - } /> + + + }> + } /> + }> + } /> + + } /> + } /> + }> + } /> + } /> + + + + + + } + /> + }> + } /> + } /> + } /> + + }> + } /> + + } /> - - - - } - /> - }> - } /> - } /> - } /> - - } /> - - } /> - } /> - + } /> + } /> + + ); } diff --git a/frontend/src/components/Navbar/index.tsx b/frontend/src/components/Navbar/index.tsx index be696441bb..2604959cf8 100644 --- a/frontend/src/components/Navbar/index.tsx +++ b/frontend/src/components/Navbar/index.tsx @@ -17,8 +17,12 @@ import AppMargin from "../AppMargin"; import { useNavigate, useLocation } from "react-router-dom"; import { useAuth } from "../../contexts/AuthContext"; import { useState } from "react"; -import { USE_AUTH_ERROR_MESSAGE } from "../../utils/constants"; +import { + USE_AUTH_ERROR_MESSAGE, + USE_MATCH_ERROR_MESSAGE, +} from "../../utils/constants"; import { isMatchingPage } from "../../utils/url"; +import { useMatch } from "../../contexts/MatchContext"; type NavbarItem = { label: string; link: string; needsLogin: boolean }; @@ -45,6 +49,12 @@ const Navbar: React.FC = (props) => { const { logout, user } = auth; + const match = useMatch(); + if (!match) { + throw new Error(USE_MATCH_ERROR_MESSAGE); + } + const { stopMatch } = match; + const handleClick = (event: React.MouseEvent) => setAnchorEl(event.currentTarget); @@ -125,11 +135,7 @@ const Navbar: React.FC = (props) => { )} ) : ( - )} diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx new file mode 100644 index 0000000000..6bf232f2b0 --- /dev/null +++ b/frontend/src/contexts/MatchContext.tsx @@ -0,0 +1,245 @@ +/* eslint-disable react-refresh/only-export-components */ + +import { createContext, useContext, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { matchSocket } from "../utils/matchSocket"; +import { minMatchTimeout, USE_AUTH_ERROR_MESSAGE } from "../utils/constants"; +import { useAuth } from "./AuthContext"; +import { toast } from "react-toastify"; + +type MatchUser = { + id: string; + username: string; + profile?: string; +}; + +type MatchCriteria = { + complexities: string[]; + categories: string[]; + languages: string[]; + timeout: number; +}; + +enum MatchEvents { + MATCH_REQUEST = "match_request", + MATCH_REQUEST_ERROR = "match_request_error", + MATCH_FOUND = "match_found", + MATCH_IN_PROGRESS = "match_in_progress", + MATCH_ACCEPTED = "match_accepted", + REMATCH_REQUEST = "rematch_request", + MATCH_SUCCESSFUL = "match_successful", + MATCH_UNSUCCESSFUL = "match_unsuccessful", + + SOCKET_DISCONNECT = "disconnect", + SOCKET_CLIENT_DISCONNECT = "io client disconnect", + SOCKET_RECONNECT_SUCCESS = "reconnect", + SOCKET_RECONNECT_FAILED = "reconnect_failed", +} + +type MatchContextType = { + closeConnection: (path: string) => void; + findMatch: ( + complexities: string[], + categories: string[], + languages: string[], + timeout: number + ) => void; + retryMatch: () => void; + acceptMatch: () => void; + rematch: () => void; + stopMatch: () => void; + matchUser: MatchUser | null; + matchCriteria: MatchCriteria; + matchId: string | null; + partner: MatchUser | null; +}; + +const MatchContext = createContext(null); + +const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { + const { children } = props; + const navigate = useNavigate(); + + const auth = useAuth(); + if (!auth) { + throw new Error(USE_AUTH_ERROR_MESSAGE); + } + const { user } = auth; + const [matchUser, _setMatchUser] = useState( + user + ? { + id: user.id, + username: user.username, + profile: user.profilePictureUrl, + } + : null + ); + + const [matchCriteria, setMatchCriteria] = useState({ + complexities: [], + categories: [], + languages: [], + timeout: minMatchTimeout, + }); + const [matchId, setMatchId] = useState(null); + const [partner, setPartner] = useState(null); + + const closeConnection = (path: string) => { + matchSocket.removeAllListeners(); + matchSocket.disconnect(); + setMatchCriteria({ + complexities: [], + categories: [], + languages: [], + timeout: minMatchTimeout, + }); + setMatchId(null); + setPartner(null); + navigate(path, { replace: true }); + }; + + const openConnection = () => { + initSocketListeners(); + matchSocket.connect(); + }; + + const initSocketListeners = () => { + if (!matchSocket.hasListeners(MatchEvents.MATCH_FOUND)) { + matchSocket.on(MatchEvents.MATCH_FOUND, ({ matchId, user1, user2 }) => { + setMatchId(matchId); + matchUser?.id === user1.id ? setPartner(user2) : setPartner(user1); + }); + } + + if (!matchSocket.hasListeners(MatchEvents.MATCH_IN_PROGRESS)) { + matchSocket.on(MatchEvents.MATCH_IN_PROGRESS, () => { + toast.error("You can only have 1 match at a time!"); + }); + } + + if (!matchSocket.hasListeners(MatchEvents.MATCH_SUCCESSFUL)) { + matchSocket.on(MatchEvents.MATCH_SUCCESSFUL, () => { + navigate("/collaboration", { replace: true }); + }); + } + + if (!matchSocket.hasListeners(MatchEvents.MATCH_UNSUCCESSFUL)) { + matchSocket.on(MatchEvents.MATCH_UNSUCCESSFUL, () => { + toast.error("Matching unsuccessful!"); + closeConnection("/home"); + }); + } + + if (!matchSocket.hasListeners(MatchEvents.MATCH_REQUEST_ERROR)) { + matchSocket.on(MatchEvents.MATCH_REQUEST_ERROR, () => { + toast.error("Error sending match request! Please try again later."); + }); + } + + if (!matchSocket.hasListeners(MatchEvents.SOCKET_DISCONNECT)) { + matchSocket.on(MatchEvents.SOCKET_DISCONNECT, (reason) => { + if (reason !== MatchEvents.SOCKET_CLIENT_DISCONNECT) { + toast.error("Connection error! Reconnecting..."); + } + }); + } + + if (!matchSocket.io.hasListeners(MatchEvents.SOCKET_RECONNECT_SUCCESS)) { + matchSocket.io.on(MatchEvents.SOCKET_RECONNECT_SUCCESS, () => { + toast.success("Reconnected!"); + initSocketListeners(); // TODO: check + }); + } + + if (!matchSocket.io.hasListeners(MatchEvents.SOCKET_RECONNECT_FAILED)) { + matchSocket.io.on(MatchEvents.SOCKET_RECONNECT_FAILED, () => { + console.log("Oops, something went wrong! Please try again later."); + }); + } + }; + + const findMatch = ( + complexities: string[], + categories: string[], + languages: string[], + timeout: number + ) => { + openConnection(); + matchSocket.emit( + MatchEvents.MATCH_REQUEST, + { + user: matchUser, + complexities: complexities, + categories: categories, + languages: languages, + timeout: timeout, + }, + (result: boolean) => { + if (result) { + setMatchCriteria({ + complexities, + categories, + languages, + timeout, + }); + navigate("/matching", { replace: true }); + } + } + ); + }; + + const retryMatch = () => { + findMatch( + matchCriteria.complexities, + matchCriteria.categories, + matchCriteria.languages, + matchCriteria.timeout + ); + }; + + const acceptMatch = () => { + matchSocket.emit(MatchEvents.MATCH_ACCEPTED, matchId); + }; + + const rematch = () => { + const rematchRequest = { + user: matchUser, + complexities: matchCriteria.complexities, + categories: matchCriteria.categories, + languages: matchCriteria.languages, + timeout: matchCriteria.timeout, + }; + matchSocket.emit(MatchEvents.REMATCH_REQUEST, matchId, rematchRequest); + + setMatchId(null); + setPartner(null); + navigate("/matching", { replace: true }); + }; + + const stopMatch = () => { + closeConnection("/home"); + }; + + return ( + + {children} + + ); +}; + +export const useMatch = () => useContext(MatchContext); + +export default MatchProvider; diff --git a/frontend/src/handlers/matchHandler.ts b/frontend/src/handlers/matchHandler.ts deleted file mode 100644 index ffc1793200..0000000000 --- a/frontend/src/handlers/matchHandler.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { - MATCH_ACCEPTED, - MATCH_DECLINED, - MATCH_FOUND, - MATCH_IN_PROGRESS, - MATCH_RECEIVED, - MATCH_REQUEST, - MATCH_SUCCESSFUL, - MATCH_TIMEOUT, - MATCH_UNSUCCESSFUL, - SOCKET_CLIENT_DISCONNECT, - SOCKET_DISCONNECT, - SOCKET_RECONNECT_FAILED, - SOCKET_RECONNECT_SUCCESS, -} from "../utils/constants"; -import { matchSocket } from "../utils/matchSocket"; - -interface MatchUser { - id: string; - username: string; - profile?: string; -} - -export class MatchHandler { - matchId?: string; - user: MatchUser; - partner?: MatchUser; - - constructor(user: MatchUser) { - this.user = user; - } - - private setMatchDetails = ( - matchId: string, - user1: MatchUser, - user2: MatchUser - ) => { - this.matchId = matchId; - user1.id !== this.user.id ? (this.partner = user1) : (this.partner = user2); - - console.log(`Match ID: ${this.matchId}`); - console.log(`User: ${this.user!.username}`); - console.log(`Partner: ${this.partner!.username}`); - }; - - private openConnection = () => { - this.initSocketListeners(); - matchSocket.connect(); - }; - - private closeConnection = () => { - matchSocket.removeAllListeners(); - matchSocket.disconnect(); - }; - - private initSocketListeners = () => { - if (!matchSocket.hasListeners(MATCH_FOUND)) { - matchSocket.on(MATCH_FOUND, ({ matchId, user1, user2 }) => { - this.setMatchDetails(matchId, user1, user2); - matchSocket.emit(MATCH_RECEIVED, this.matchId); - }); - } - - if (!matchSocket.hasListeners(MATCH_IN_PROGRESS)) { - matchSocket.on(MATCH_IN_PROGRESS, () => { - console.log("Matching in progress... / Match already found!"); - }); - } - - if (!matchSocket.hasListeners(MATCH_SUCCESSFUL)) { - matchSocket.on(MATCH_SUCCESSFUL, () => { - console.log("Match successful"); - this.closeConnection(); - }); - } - - if (!matchSocket.hasListeners(MATCH_UNSUCCESSFUL)) { - matchSocket.on(MATCH_UNSUCCESSFUL, () => { - console.log("Match unsuccessful"); - this.closeConnection(); - }); - } - - if (!matchSocket.hasListeners(MATCH_TIMEOUT)) { - matchSocket.on(MATCH_TIMEOUT, () => { - console.log("Match timeout"); - this.closeConnection(); - }); - } - - if (!matchSocket.hasListeners(SOCKET_DISCONNECT)) { - matchSocket.on(SOCKET_DISCONNECT, (reason) => { - if (reason !== SOCKET_CLIENT_DISCONNECT) { - console.log("Oops, something went wrong! Reconnecting..."); - } - }); - } - - if (!matchSocket.io.hasListeners(SOCKET_RECONNECT_SUCCESS)) { - matchSocket.io.on(SOCKET_RECONNECT_SUCCESS, () => { - console.log("Reconnected!"); - }); - } - - if (!matchSocket.io.hasListeners(SOCKET_RECONNECT_FAILED)) { - matchSocket.io.on(SOCKET_RECONNECT_FAILED, () => { - console.log("Oops, something went wrong! Please try again later."); - }); - } - }; - - findMatch = ( - complexities: string[], - categories: string[], - languages: string[], - timeout: number - ) => { - this.openConnection(); - matchSocket.emit(MATCH_REQUEST, { - user: this.user, - complexities: complexities, - categories: categories, - languages: languages, - timeout: timeout, - }); - }; - - acceptMatch = () => { - matchSocket.emit(MATCH_ACCEPTED, this.matchId); - }; - - declineMatch = () => { - matchSocket.emit(MATCH_DECLINED, this.matchId); - }; - - stopMatch = () => { - this.closeConnection(); - }; -} diff --git a/frontend/src/pages/CollabSandbox/index.module.css b/frontend/src/pages/CollabSandbox/index.module.css new file mode 100644 index 0000000000..806d506cc5 --- /dev/null +++ b/frontend/src/pages/CollabSandbox/index.module.css @@ -0,0 +1,10 @@ +.fullheight { + flex: 1; +} + +.center { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx new file mode 100644 index 0000000000..8e786fd7ee --- /dev/null +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -0,0 +1,32 @@ +import AppMargin from "../../components/AppMargin"; +import { Button, Stack, Typography } from "@mui/material"; +import classes from "./index.module.css"; +import { useMatch } from "../../contexts/MatchContext"; +import { USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; + +// TODO: Prevent user from accessing this page via URL +const CollabSandbox: React.FC = () => { + const match = useMatch(); + if (!match) { + throw new Error(USE_MATCH_ERROR_MESSAGE); + } + const { closeConnection } = match; + + return ( + + + Collaborative Sandbox + Coming soon... + + + + ); +}; + +export default CollabSandbox; diff --git a/frontend/src/pages/Home/index.tsx b/frontend/src/pages/Home/index.tsx index 7f9f52b114..1e9405a9f2 100644 --- a/frontend/src/pages/Home/index.tsx +++ b/frontend/src/pages/Home/index.tsx @@ -17,16 +17,15 @@ import { languageList, maxMatchTimeout, minMatchTimeout, + USE_MATCH_ERROR_MESSAGE, } from "../../utils/constants"; import reducer, { getQuestionCategories, - initialState, + initialState as initialState, } from "../../reducers/questionReducer"; import CustomChip from "../../components/CustomChip"; import homepageImage from "/homepage_image.svg"; -import { useOutletContext } from "react-router-dom"; -import { User } from "../../types/types"; -import { MatchHandler } from "../../handlers/matchHandler"; +import { useMatch } from "../../contexts/MatchContext"; const Home: React.FC = () => { const [complexities, setComplexities] = useState([]); @@ -36,12 +35,11 @@ const Home: React.FC = () => { const [state, dispatch] = useReducer(reducer, initialState); - const user = useOutletContext(); - const matchHandler = new MatchHandler({ - id: user.id, - username: user.username, - profile: user.profilePictureUrl, - }); + const match = useMatch(); + if (!match) { + throw new Error(USE_MATCH_ERROR_MESSAGE); + } + const { findMatch } = match; useEffect(() => { getQuestionCategories(dispatch); @@ -272,13 +270,11 @@ const Home: React.FC = () => { // languages.length == 0 // } onClick={() => - matchHandler.findMatch(complexities, categories, languages, timeout) + findMatch(complexities, categories, languages, timeout) } > Find my match! - - ); diff --git a/frontend/src/pages/Matched/index.tsx b/frontend/src/pages/Matched/index.tsx index cb141cd003..a172defa37 100644 --- a/frontend/src/pages/Matched/index.tsx +++ b/frontend/src/pages/Matched/index.tsx @@ -1,10 +1,36 @@ -import { useNavigate } from "react-router-dom"; import AppMargin from "../../components/AppMargin"; import { Avatar, Box, Button, Stack, Typography } from "@mui/material"; import classes from "./index.module.css"; +import { useMatch } from "../../contexts/MatchContext"; +import { USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; +import { useEffect, useState } from "react"; +import { toast } from "react-toastify"; +const acceptanceTimeout = 10; + +// TODO: Prevent user from accessing this page via URL const Matched: React.FC = () => { - const navigate = useNavigate(); + const match = useMatch(); + if (!match) { + throw new Error(USE_MATCH_ERROR_MESSAGE); + } + const { closeConnection, acceptMatch, rematch, matchUser, partner } = match; + + const [timeLeft, setTimeLeft] = useState(acceptanceTimeout); + + useEffect(() => { + const timer = setInterval(() => { + setTimeLeft((prevTime) => (prevTime <= 0 ? 0 : prevTime - 1)); + }, 1000); + return () => clearInterval(timer); + }, []); + + useEffect(() => { + if (timeLeft <= 0) { + toast.error("Match acceptance timeout!"); + closeConnection("/home"); + } + }, [timeLeft]); return ( @@ -18,7 +44,7 @@ const Matched: React.FC = () => { paddingTop={2} paddingBottom={2} > - + ({ @@ -29,21 +55,23 @@ const Matched: React.FC = () => { })} /> - + - Practice with @john? + + Practice with @{partner?.username}? + - diff --git a/frontend/src/pages/Matching/index.tsx b/frontend/src/pages/Matching/index.tsx index 4c6b0ffc93..5c827ed815 100644 --- a/frontend/src/pages/Matching/index.tsx +++ b/frontend/src/pages/Matching/index.tsx @@ -1,13 +1,24 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import AppMargin from "../../components/AppMargin"; import { Stack, Typography } from "@mui/material"; import matching from "../../assets/matching.svg"; import classes from "./index.module.css"; import Timer from "../../components/Timer"; +import { useNavigate } from "react-router-dom"; +import { useMatch } from "../../contexts/MatchContext"; +import { USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; +// TODO: Prevent user from accessing this page via URL const Matching: React.FC = () => { - const totalTime = 30; - const [timeLeft, setTimeLeft] = React.useState(totalTime); + const navigate = useNavigate(); + + const match = useMatch(); + if (!match) { + throw new Error(USE_MATCH_ERROR_MESSAGE); + } + const { closeConnection, matchId, matchCriteria } = match; + + const [timeLeft, setTimeLeft] = useState(matchCriteria.timeout); useEffect(() => { const timer = setInterval(() => { @@ -16,6 +27,18 @@ const Matching: React.FC = () => { return () => clearInterval(timer); }, []); + useEffect(() => { + if (timeLeft <= 0) { + closeConnection("timeout"); + } + }, [timeLeft]); + + useEffect(() => { + if (matchId) { + navigate("matched", { replace: true }); + } + }, [matchId, navigate]); + return ( @@ -24,7 +47,7 @@ const Matching: React.FC = () => { { const navigate = useNavigate(); + const match = useMatch(); + if (!match) { + throw new Error(USE_MATCH_ERROR_MESSAGE); + } + const { retryMatch } = match; + return ( @@ -23,15 +32,11 @@ const Timeout: React.FC = () => { variant="contained" color="secondary" fullWidth - onClick={() => navigate("/questions")} + onClick={() => navigate("/home", { replace: true })} > Exit - diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index d17964509d..30130a83bc 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -7,6 +7,8 @@ export const USE_AUTH_ERROR_MESSAGE = "useAuth() must be used within AuthProvider"; export const USE_PROFILE_ERROR_MESSAGE = "useProfile() must be used within ProfileContextProvider"; +export const USE_MATCH_ERROR_MESSAGE = + "useMatch() must be used within MatchProvider"; /* Name Validation */ export const NAME_REQUIRED_ERROR_MESSAGE = "Name is required"; @@ -81,21 +83,3 @@ export const FIND_MATCH_FORM_PATH = "/find_match_form.png"; export const MATCH_FOUND_PATH = "/match_found.png"; export const QUESTIONS_LIST_PATH = "/questions_list.png"; export const COLLABORATIVE_EDITOR_PATH = "/collaborative_editor.png"; - -/* Websocket */ -// Socket Events -export const SOCKET_DISCONNECT = "disconnect"; -export const SOCKET_CLIENT_DISCONNECT = "io client disconnect"; -export const SOCKET_RECONNECT_SUCCESS = "reconnect"; -export const SOCKET_RECONNECT_FAILED = "reconnect_failed"; - -// Match Events -export const MATCH_REQUEST = "match_request"; -export const MATCH_TIMEOUT = "match_timeout"; -export const MATCH_FOUND = "match_found"; -export const MATCH_IN_PROGRESS = "match_in_progress"; -export const MATCH_RECEIVED = "match_received"; -export const MATCH_ACCEPTED = "match_accepted"; -export const MATCH_DECLINED = "match_declined"; -export const MATCH_SUCCESSFUL = "match_successful"; -export const MATCH_UNSUCCESSFUL = "match_unsuccessful"; From ad4174029aadb2665f994baedf3408f046b66366 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Tue, 15 Oct 2024 19:14:53 +0800 Subject: [PATCH 24/82] Prevent direct url access to match pages --- frontend/src/App.tsx | 13 +++-- frontend/src/components/Navbar/index.tsx | 6 ++- .../components/NoDirectAccessRoutes/index.tsx | 24 ++++++++++ .../src/components/ProtectedRoutes/index.tsx | 2 +- frontend/src/contexts/MatchContext.tsx | 47 +++++++++---------- frontend/src/pages/CollabSandbox/index.tsx | 5 +- frontend/src/pages/Matched/index.tsx | 17 ++++--- frontend/src/pages/Matching/index.tsx | 14 +----- frontend/src/pages/Timeout/index.tsx | 1 - 9 files changed, 75 insertions(+), 54 deletions(-) create mode 100644 frontend/src/components/NoDirectAccessRoutes/index.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 157e9033b1..f0d525b9ea 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -19,6 +19,7 @@ import AuthProvider from "./contexts/AuthContext"; import ProfileContextProvider from "./contexts/ProfileContext"; import MatchProvider from "./contexts/MatchContext"; import CollabSandbox from "./pages/CollabSandbox"; +import NoDirectAccessRoutes from "./components/NoDirectAccessRoutes"; function App() { return ( @@ -47,12 +48,16 @@ function App() { } /> }> - } /> - } /> - } /> + }> + } /> + } /> + } /> + }> - } /> + }> + } /> + } /> diff --git a/frontend/src/components/Navbar/index.tsx b/frontend/src/components/Navbar/index.tsx index 2604959cf8..4861358a0e 100644 --- a/frontend/src/components/Navbar/index.tsx +++ b/frontend/src/components/Navbar/index.tsx @@ -135,7 +135,11 @@ const Navbar: React.FC = (props) => { )} ) : ( - )} diff --git a/frontend/src/components/NoDirectAccessRoutes/index.tsx b/frontend/src/components/NoDirectAccessRoutes/index.tsx new file mode 100644 index 0000000000..f82527cc4b --- /dev/null +++ b/frontend/src/components/NoDirectAccessRoutes/index.tsx @@ -0,0 +1,24 @@ +import { Navigate, Outlet, useLocation, useNavigate } from "react-router-dom"; +import React from "react"; + +export const useAppNavigate = () => { + const navigate = useNavigate(); + + const appNavigate = (path: string) => { + navigate(path, { replace: true, state: { from: "app-navigation" } }); + }; + + return appNavigate; +}; + +const NoDirectAccessRoutes: React.FC = () => { + const location = useLocation(); + + if (location.state?.from !== "app-navigation") { + return ; + } + + return ; +}; + +export default NoDirectAccessRoutes; diff --git a/frontend/src/components/ProtectedRoutes/index.tsx b/frontend/src/components/ProtectedRoutes/index.tsx index 6ecfeb064b..c8d30d9318 100644 --- a/frontend/src/components/ProtectedRoutes/index.tsx +++ b/frontend/src/components/ProtectedRoutes/index.tsx @@ -35,7 +35,7 @@ const ProtectedRoutes: React.FC = ({ ); } - return ; + return ; }; export default ProtectedRoutes; diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 6bf232f2b0..0ba0ba6e4d 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -1,11 +1,11 @@ /* eslint-disable react-refresh/only-export-components */ import { createContext, useContext, useState } from "react"; -import { useNavigate } from "react-router-dom"; import { matchSocket } from "../utils/matchSocket"; import { minMatchTimeout, USE_AUTH_ERROR_MESSAGE } from "../utils/constants"; import { useAuth } from "./AuthContext"; import { toast } from "react-toastify"; +import { useAppNavigate } from "../components/NoDirectAccessRoutes"; type MatchUser = { id: string; @@ -37,7 +37,6 @@ enum MatchEvents { } type MatchContextType = { - closeConnection: (path: string) => void; findMatch: ( complexities: string[], categories: string[], @@ -47,10 +46,9 @@ type MatchContextType = { retryMatch: () => void; acceptMatch: () => void; rematch: () => void; - stopMatch: () => void; + stopMatch: (path: string) => void; matchUser: MatchUser | null; matchCriteria: MatchCriteria; - matchId: string | null; partner: MatchUser | null; }; @@ -58,7 +56,7 @@ const MatchContext = createContext(null); const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const { children } = props; - const navigate = useNavigate(); + const appNavigate = useAppNavigate(); const auth = useAuth(); if (!auth) { @@ -84,18 +82,9 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const [matchId, setMatchId] = useState(null); const [partner, setPartner] = useState(null); - const closeConnection = (path: string) => { + const closeConnection = () => { matchSocket.removeAllListeners(); matchSocket.disconnect(); - setMatchCriteria({ - complexities: [], - categories: [], - languages: [], - timeout: minMatchTimeout, - }); - setMatchId(null); - setPartner(null); - navigate(path, { replace: true }); }; const openConnection = () => { @@ -108,6 +97,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { matchSocket.on(MatchEvents.MATCH_FOUND, ({ matchId, user1, user2 }) => { setMatchId(matchId); matchUser?.id === user1.id ? setPartner(user2) : setPartner(user1); + appNavigate("/matching/matched"); }); } @@ -119,20 +109,20 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { if (!matchSocket.hasListeners(MatchEvents.MATCH_SUCCESSFUL)) { matchSocket.on(MatchEvents.MATCH_SUCCESSFUL, () => { - navigate("/collaboration", { replace: true }); + appNavigate("/collaboration"); }); } if (!matchSocket.hasListeners(MatchEvents.MATCH_UNSUCCESSFUL)) { matchSocket.on(MatchEvents.MATCH_UNSUCCESSFUL, () => { toast.error("Matching unsuccessful!"); - closeConnection("/home"); + stopMatch("/home"); }); } if (!matchSocket.hasListeners(MatchEvents.MATCH_REQUEST_ERROR)) { matchSocket.on(MatchEvents.MATCH_REQUEST_ERROR, () => { - toast.error("Error sending match request! Please try again later."); + toast.error("Failed to send match request! Please try again later."); }); } @@ -153,7 +143,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { if (!matchSocket.io.hasListeners(MatchEvents.SOCKET_RECONNECT_FAILED)) { matchSocket.io.on(MatchEvents.SOCKET_RECONNECT_FAILED, () => { - console.log("Oops, something went wrong! Please try again later."); + toast.error("Failed to reconnect! Please try again later."); }); } }; @@ -182,7 +172,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { languages, timeout, }); - navigate("/matching", { replace: true }); + appNavigate("/matching"); } } ); @@ -213,17 +203,25 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { setMatchId(null); setPartner(null); - navigate("/matching", { replace: true }); + appNavigate("/matching"); }; - const stopMatch = () => { - closeConnection("/home"); + const stopMatch = (path: string) => { + closeConnection(); + setMatchCriteria({ + complexities: [], + categories: [], + languages: [], + timeout: minMatchTimeout, + }); + setMatchId(null); + setPartner(null); + appNavigate(path); }; return ( = (props) => { stopMatch, matchUser, matchCriteria, - matchId, partner, }} > diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 8e786fd7ee..1fce8cb2a0 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -4,13 +4,12 @@ import classes from "./index.module.css"; import { useMatch } from "../../contexts/MatchContext"; import { USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; -// TODO: Prevent user from accessing this page via URL const CollabSandbox: React.FC = () => { const match = useMatch(); if (!match) { throw new Error(USE_MATCH_ERROR_MESSAGE); } - const { closeConnection } = match; + const { stopMatch } = match; return ( @@ -20,7 +19,7 @@ const CollabSandbox: React.FC = () => { diff --git a/frontend/src/pages/Matched/index.tsx b/frontend/src/pages/Matched/index.tsx index a172defa37..ab7042ef05 100644 --- a/frontend/src/pages/Matched/index.tsx +++ b/frontend/src/pages/Matched/index.tsx @@ -5,6 +5,7 @@ import { useMatch } from "../../contexts/MatchContext"; import { USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; import { useEffect, useState } from "react"; import { toast } from "react-toastify"; +import { Navigate } from "react-router-dom"; const acceptanceTimeout = 10; @@ -14,7 +15,7 @@ const Matched: React.FC = () => { if (!match) { throw new Error(USE_MATCH_ERROR_MESSAGE); } - const { closeConnection, acceptMatch, rematch, matchUser, partner } = match; + const { acceptMatch, rematch, stopMatch, matchUser, partner } = match; const [timeLeft, setTimeLeft] = useState(acceptanceTimeout); @@ -28,10 +29,14 @@ const Matched: React.FC = () => { useEffect(() => { if (timeLeft <= 0) { toast.error("Match acceptance timeout!"); - closeConnection("/home"); + stopMatch("/home"); } }, [timeLeft]); + if (!matchUser || !partner) { + return ; + } + return ( @@ -44,7 +49,7 @@ const Matched: React.FC = () => { paddingTop={2} paddingBottom={2} > - + ({ @@ -55,12 +60,10 @@ const Matched: React.FC = () => { })} /> - + - - Practice with @{partner?.username}? - + Practice with @{partner.username}? - From b5e3f9a743c105c90fc36f0a1e58a80889fefc3f Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 16 Oct 2024 18:05:48 +0800 Subject: [PATCH 27/82] Add forget password button --- frontend/src/pages/LogIn/index.tsx | 44 ++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/frontend/src/pages/LogIn/index.tsx b/frontend/src/pages/LogIn/index.tsx index 047c124cf9..202b2f9d59 100644 --- a/frontend/src/pages/LogIn/index.tsx +++ b/frontend/src/pages/LogIn/index.tsx @@ -1,4 +1,12 @@ -import { Box, Button, Stack, TextField, Typography } from "@mui/material"; +import { + Box, + Button, + Stack, + styled, + TextField, + Typography, + TypographyProps, +} from "@mui/material"; import LogInSvg from "../../assets/login.svg?react"; import { useNavigate } from "react-router-dom"; import { useAuth } from "../../contexts/AuthContext"; @@ -12,6 +20,10 @@ import { USE_AUTH_ERROR_MESSAGE, } from "../../utils/constants"; +const StyledTypography = styled((props: TypographyProps) => { + return ; +})({ fontSize: 14 }); + const LogIn: React.FC = () => { const navigate = useNavigate(); const auth = useAuth(); @@ -99,26 +111,36 @@ const LogIn: React.FC = () => { - - Don't have an account? - - navigate("/signup")} > - Sign up - + Forget password + + + Don't have an account?   + navigate("/signup")} + > + Sign up + + From 0ad0bca7337273dcedf5dc7f39c00ced2738be10 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 16 Oct 2024 19:44:28 +0800 Subject: [PATCH 28/82] Add toast message for email verification --- frontend/src/App.tsx | 3 +++ frontend/src/components/Layout/index.tsx | 3 --- frontend/src/contexts/AuthContext.tsx | 11 ++++++++--- frontend/src/pages/Home/index.tsx | 2 +- frontend/src/pages/LogIn/index.tsx | 3 --- frontend/src/pages/SignUp/index.tsx | 3 --- frontend/src/utils/constants.ts | 2 ++ 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2a05ce7053..d168211ec4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,6 +17,8 @@ import Matching from "./pages/Matching"; import Layout from "./components/Layout"; import AuthProvider from "./contexts/AuthContext"; import ProfileContextProvider from "./contexts/ProfileContext"; +import { ToastContainer } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; function App() { return ( @@ -53,6 +55,7 @@ function App() { } /> } /> + ); } diff --git a/frontend/src/components/Layout/index.tsx b/frontend/src/components/Layout/index.tsx index aa267a85a1..d4e932ad5b 100644 --- a/frontend/src/components/Layout/index.tsx +++ b/frontend/src/components/Layout/index.tsx @@ -1,8 +1,6 @@ import { Box } from "@mui/material"; import { Outlet } from "react-router-dom"; import Navbar from "../Navbar"; -import { ToastContainer } from "react-toastify"; -import "react-toastify/dist/ReactToastify.css"; const Layout: React.FC = () => { return ( @@ -17,7 +15,6 @@ const Layout: React.FC = () => { > - ); }; diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 7be6dae74c..de5d78a513 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -5,7 +5,7 @@ import { userClient } from "../utils/api"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import Loader from "../components/Loader"; -import { SUCCESS_LOG_OUT } from "../utils/constants"; +import { SUCCESS_LOG_OUT, SUCCESSFUL_SIGNUP } from "../utils/constants"; type User = { id: string; @@ -75,7 +75,10 @@ const AuthProvider: React.FC<{ children?: React.ReactNode }> = (props) => { email: email, password: password, }) - .then(() => login(email, password)) + .then(() => { + login(email, password); + toast.success(SUCCESSFUL_SIGNUP); + }) .catch((err) => { setUser(null); toast.error(err.response?.data.message || err.message); @@ -112,7 +115,9 @@ const AuthProvider: React.FC<{ children?: React.ReactNode }> = (props) => { } return ( - + {children} ); diff --git a/frontend/src/pages/Home/index.tsx b/frontend/src/pages/Home/index.tsx index 9d4a8e6457..05b66ff00d 100644 --- a/frontend/src/pages/Home/index.tsx +++ b/frontend/src/pages/Home/index.tsx @@ -11,6 +11,7 @@ import { import classes from "./index.module.css"; import AppMargin from "../../components/AppMargin"; + // import { // complexityList, // languageList, @@ -52,7 +53,6 @@ const Home: React.FC = () => { > Start an interactive practice session today! - { > - ); }; diff --git a/frontend/src/pages/SignUp/index.tsx b/frontend/src/pages/SignUp/index.tsx index 0736e57f3f..6b81a6db8e 100644 --- a/frontend/src/pages/SignUp/index.tsx +++ b/frontend/src/pages/SignUp/index.tsx @@ -8,8 +8,6 @@ import { passwordValidator, usernameValidator, } from "../../utils/validators"; -import { ToastContainer } from "react-toastify"; -import "react-toastify/dist/ReactToastify.css"; import { useForm } from "react-hook-form"; import PasswordTextField from "../../components/PasswordTextField"; import { @@ -191,7 +189,6 @@ const SignUp: React.FC = () => { > - ); }; diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index 0a0aa8fea8..fb130cb50d 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -51,6 +51,8 @@ export const PASSWORD_MISMATCH_ERROR_MESSAGE = "Password does not match"; /* Toast Messages */ // Authentication export const SUCCESS_LOG_OUT = "Logged out successfully!"; +export const SUCCESSFUL_SIGNUP = + "User created successfully. Please verify your email address."; // Field Validation export const FILL_ALL_FIELDS = "Please fill in all fields"; From bcaee89c117bef6fd13c7c59a66fa82914f551ac Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Thu, 17 Oct 2024 01:40:21 +0800 Subject: [PATCH 29/82] Set up socket connection for each page --- backend/matching-service/config/rabbitmq.ts | 6 +- .../src/handlers/matchHandler.ts | 127 ++----- .../src/handlers/websocketHandler.ts | 185 +++++++-- .../matching-service/src/utils/constants.ts | 23 +- .../matching-service/src/utils/mq_utils.ts | 20 +- frontend/src/components/Navbar/index.tsx | 2 +- frontend/src/contexts/MatchContext.tsx | 357 ++++++++++++------ frontend/src/pages/CollabSandbox/index.tsx | 13 +- frontend/src/pages/Matched/index.tsx | 20 +- frontend/src/pages/Matching/index.tsx | 32 +- frontend/src/utils/constants.ts | 14 +- frontend/src/utils/matchSocket.ts | 1 + 12 files changed, 505 insertions(+), 295 deletions(-) diff --git a/backend/matching-service/config/rabbitmq.ts b/backend/matching-service/config/rabbitmq.ts index 90c9dc724c..76c77ec53a 100644 --- a/backend/matching-service/config/rabbitmq.ts +++ b/backend/matching-service/config/rabbitmq.ts @@ -1,7 +1,7 @@ import amqplib, { Connection } from "amqplib"; import dotenv from "dotenv"; import { matchUsers } from "../src/utils/mq_utils"; -import { MatchItem } from "../src/handlers/matchHandler"; +import { MatchRequestItem } from "../src/handlers/matchHandler"; dotenv.config(); @@ -26,7 +26,9 @@ export const connectRabbitMq = async () => { } }; -export const sendRabbitMq = async (data: MatchItem): Promise => { +export const sendRabbitMq = async ( + data: MatchRequestItem +): Promise => { try { const senderChannel = await mrConnection.createChannel(); senderChannel.sendToQueue(queue, Buffer.from(JSON.stringify(data))); diff --git a/backend/matching-service/src/handlers/matchHandler.ts b/backend/matching-service/src/handlers/matchHandler.ts index 37b5cc2549..d767bb196f 100644 --- a/backend/matching-service/src/handlers/matchHandler.ts +++ b/backend/matching-service/src/handlers/matchHandler.ts @@ -1,15 +1,6 @@ -import { io } from "../../server"; import { v4 as uuidv4 } from "uuid"; -import { - MATCH_FOUND, - MATCH_IN_PROGRESS, - MATCH_SUCCESSFUL, - MATCH_ENDED, - MATCH_REQUEST_ERROR, - MATCH_NOT_ACCEPTED, -} from "../utils/constants"; -import { Socket } from "socket.io"; import { sendRabbitMq } from "../../config/rabbitmq"; +import { sendMatchFound } from "./websocketHandler"; interface MatchUser { id: string; @@ -31,7 +22,7 @@ export interface MatchRequest { timeout: number; } -export interface MatchItem { +export interface MatchRequestItem { user: MatchUser; complexities: string[]; categories: string[]; @@ -41,23 +32,12 @@ export interface MatchItem { } const matches = new Map(); -const userSockets = new Map(); -export const createMatchItem = async ( - socket: Socket, - matchRequest: MatchRequest, - isRematch?: boolean +export const sendMatchRequest = async ( + matchRequest: MatchRequest ): Promise => { const { user, complexities, categories, languages, timeout } = matchRequest; - - if (!isRematch && userSockets.has(user.id)) { - socket.emit(MATCH_IN_PROGRESS); - return false; - } - - userSockets.set(user.id, socket); - - const matchQueueItem: MatchItem = { + const matchItem: MatchRequestItem = { user: user, complexities: complexities, categories: categories, @@ -66,98 +46,43 @@ export const createMatchItem = async ( ttlInSecs: timeout, }; - const result = await sendRabbitMq(matchQueueItem); - if (!result) { - socket.emit(MATCH_REQUEST_ERROR); - } - return result; + const sent = await sendRabbitMq(matchItem); + return sent; }; -export const createMatch = (matchItem1: MatchItem, matchItem2: MatchItem) => { - const uid1 = matchItem1.user.id; - const uid2 = matchItem2.user.id; - +export const createMatch = ( + requestItem1: MatchRequestItem, + requestItem2: MatchRequestItem +) => { const matchId = uuidv4(); matches.set(matchId, { - uid1: uid1, - uid2: uid2, + uid1: requestItem1.user.id, + uid2: requestItem2.user.id, accepted: false, }); - userSockets.get(uid1)?.join(matchId); - userSockets.get(uid2)?.join(matchId); - io.to(matchId).emit(MATCH_FOUND, { - matchId: matchId, - user1: matchItem1.user, - user2: matchItem2.user, - }); + sendMatchFound(matchId, requestItem1, requestItem2); }; -export const handleMatchAcceptance = (matchId: string) => { +export const handleMatchAccept = (matchId: string): boolean => { const match = matches.get(matchId); if (!match) { - return; - } - - if (match.accepted) { - io.to(matchId).emit(MATCH_SUCCESSFUL); - } else { - matches.set(matchId, { ...match, accepted: true }); - } -}; - -const handleMatchDecline = (socket: Socket, matchId: string) => { - if (matches.delete(matchId)) { - socket.to(matchId).emit(MATCH_NOT_ACCEPTED); + return false; } -}; -export const handleRematch = async ( - socket: Socket, - matchId: string, - rematchRequest: MatchRequest -): Promise => { - handleMatchDecline(socket, matchId); - return await createMatchItem(socket, rematchRequest, true); + const partnerAccepted = match.accepted; + matches.set(matchId, { ...match, accepted: true }); + return partnerAccepted; }; -export const handleMatchStopRequest = ( - socket: Socket, - uid: string | undefined, - matchId: string | null, - matchPending: boolean, - isMutual: boolean -) => { - if (matchId) { - if (matchPending) { - handleMatchDecline(socket, matchId); - return; - } - - const match = matches.get(matchId); - if (match) { - userSockets.delete(match.uid1); - userSockets.delete(match.uid2); - matches.delete(matchId); - } +export const handleMatchDelete = (matchId: string): boolean => + matches.delete(matchId); - if (!isMutual) { - socket.to(matchId).emit(MATCH_ENDED); +export const getMatchIdByUid = (uid: string): string | null => { + for (const [matchId, match] of matches) { + if (match.uid1 === uid || match.uid2 === uid) { + return matchId; } - } else if (uid) { - userSockets.delete(uid); } -}; - -export const handleUserDisconnect = (disconnectedSocket: Socket) => { - for (const [uid, socket] of userSockets) { - if (socket.id === disconnectedSocket.id) { - userSockets.delete(uid); - break; - } - } -}; - -export const hasUserDisconnected = (uid: string) => { - return !userSockets.has(uid); + return null; }; diff --git a/backend/matching-service/src/handlers/websocketHandler.ts b/backend/matching-service/src/handlers/websocketHandler.ts index d69df7ab22..63e8286d5f 100644 --- a/backend/matching-service/src/handlers/websocketHandler.ts +++ b/backend/matching-service/src/handlers/websocketHandler.ts @@ -1,32 +1,113 @@ import { Socket } from "socket.io"; import { - MATCH_ACCEPTED, REMATCH_REQUEST, MATCH_REQUEST, - SOCKET_CLIENT_DISCONNECT, SOCKET_DISCONNECT, - MATCH_STOP_REQUEST, + USER_CONNECTED, + USER_DISCONNECTED, + MATCH_DECLINE_REQUEST, + MATCH_UNSUCCESSFUL, + MATCH_SUCCESSFUL, + MATCH_END_REQUEST, + MATCH_ENDED, + MATCH_ACCEPT_REQUEST, + MATCH_FOUND, + MATCH_REQUEST_EXISTS, + MATCH_REQUEST_ERROR, + CANCEL_MATCH_REQUEST, } from "../utils/constants"; import { - createMatchItem, - handleMatchAcceptance, - handleMatchStopRequest, - handleUserDisconnect, - handleRematch, + sendMatchRequest, + handleMatchAccept, MatchRequest, + handleMatchDelete, + MatchRequestItem, + getMatchIdByUid, } from "./matchHandler"; +import { io } from "../../server"; + +interface UserConnection { + socket: Socket; + connectionTimeout?: NodeJS.Timeout; +} + +// TODO: do a match lost due to poor connection? +const connectionDelay = 3000; // time window to allow for page navigation / refresh +const userConnections = new Map(); export const handleWebsocketMatchEvents = (socket: Socket) => { + socket.removeAllListeners(); + + socket.on(USER_CONNECTED, (uid: string) => { + clearTimeout(userConnections.get(uid)?.connectionTimeout); + userConnections.set(uid, { socket: socket }); + console.log(`socket: ${socket.id}`); + }); + + socket.on(USER_DISCONNECTED, (uid: string, matchId: string | null) => { + if (!userConnections.has(uid)) { + return; + } + + clearTimeout(userConnections.get(uid)?.connectionTimeout); + + const connectionTimeout = setTimeout(() => { + endMatchOnUserDisconnect(socket, uid, matchId); + console.log(`server disconnect connection: ${uid}`); + socket.disconnect(); + }, connectionDelay); // TODO: Clearing up your previous match (in case user immediately sends a new match request) + + userConnections.set(uid, { + socket: socket, + connectionTimeout: connectionTimeout, + }); + }); + socket.on( MATCH_REQUEST, - async (matchRequest: MatchRequest, callback: (result: boolean) => void) => { - const result = await createMatchItem(socket, matchRequest); - callback(result); + async ( + matchRequest: MatchRequest, + callback: (requested: boolean) => void + ) => { + const uid = matchRequest.user.id; + if (isUserConnected(uid)) { + socket.emit(MATCH_REQUEST_EXISTS); + callback(false); + return; + } + + userConnections.set(uid, { socket: socket }); + + const sent = await sendMatchRequest(matchRequest); + if (!sent) { + socket.emit(MATCH_REQUEST_ERROR); + userConnections.delete(uid); + socket.disconnect(); + } + callback(sent); } ); - socket.on(MATCH_ACCEPTED, (matchId: string) => - handleMatchAcceptance(matchId) + socket.on(CANCEL_MATCH_REQUEST, (uid: string) => { + userConnections.delete(uid); + }); + + socket.on(MATCH_ACCEPT_REQUEST, (matchId: string) => { + const partnerAccepted = handleMatchAccept(matchId); + if (partnerAccepted) { + io.to(matchId).emit(MATCH_SUCCESSFUL); + } + }); + + socket.on( + MATCH_DECLINE_REQUEST, + (uid: string, matchId: string, isTimeout: boolean) => { + userConnections.delete(uid); + const matchDeleted = handleMatchDelete(matchId); + if (matchDeleted && !isTimeout) { + socket.to(matchId).emit(MATCH_UNSUCCESSFUL); + } + } ); socket.on( @@ -36,31 +117,71 @@ export const handleWebsocketMatchEvents = (socket: Socket) => { rematchRequest: MatchRequest, callback: (result: boolean) => void ) => { - const result = await handleRematch(socket, matchId, rematchRequest); - callback(result); + const matchDeleted = handleMatchDelete(matchId); + if (matchDeleted) { + socket.to(matchId).emit(MATCH_UNSUCCESSFUL); + } + + const sent = await sendMatchRequest(rematchRequest); + if (!sent) { + socket.emit(MATCH_REQUEST_ERROR); + } + callback(sent); } ); - socket.on( - MATCH_STOP_REQUEST, - ( - uid: string | undefined, - matchId: string | null, - matchPending: boolean, - isMutual: boolean, - callback: () => void - ) => { - handleMatchStopRequest(socket, uid, matchId, matchPending, isMutual); - callback(); + socket.on(MATCH_END_REQUEST, (uid: string, matchId: string) => { + userConnections.delete(uid); + const matchDeleted = handleMatchDelete(matchId); + if (matchDeleted) { + socket.to(matchId).emit(MATCH_ENDED); } - ); + }); // TODO: handle client reconnect failure - socket.on(SOCKET_DISCONNECT, (reason) => { - if (reason === SOCKET_CLIENT_DISCONNECT) { - console.log("Client manually disconnected"); - handleUserDisconnect(socket); + socket.on(SOCKET_DISCONNECT, () => { + for (const [uid, userConnection] of userConnections) { + if (userConnection.socket.id === socket.id) { + if (!userConnections.get(uid)?.connectionTimeout) { + const matchId = getMatchIdByUid(uid); + endMatchOnUserDisconnect(socket, uid, matchId); + console.log(`force delete connection: ${uid}`); + } + break; + } } - console.log("disconnect"); }); }; + +export const sendMatchFound = ( + matchId: string, + requestItem1: MatchRequestItem, + requestItem2: MatchRequestItem +) => { + userConnections.get(requestItem1.user.id)?.socket.join(matchId); + userConnections.get(requestItem2.user.id)?.socket.join(matchId); + io.to(matchId).emit(MATCH_FOUND, { + matchId: matchId, + user1: requestItem1.user, + user2: requestItem2.user, + }); +}; + +export const isUserConnected = (uid: string) => { + return userConnections.has(uid); +}; + +const endMatchOnUserDisconnect = ( + socket: Socket, + uid: string, + matchId: string | null +) => { + userConnections.delete(uid); + if (matchId) { + const matchDeleted = handleMatchDelete(matchId); + if (matchDeleted) { + socket.to(matchId).emit(MATCH_UNSUCCESSFUL); // on matching page + socket.to(matchId).emit(MATCH_ENDED); // on collab page + } + } +}; diff --git a/backend/matching-service/src/utils/constants.ts b/backend/matching-service/src/utils/constants.ts index 49eb05cd3b..6ba8906281 100644 --- a/backend/matching-service/src/utils/constants.ts +++ b/backend/matching-service/src/utils/constants.ts @@ -1,14 +1,21 @@ /* Websocket Match Events */ +// Receive export const MATCH_REQUEST = "match_request"; -export const MATCH_REQUEST_ERROR = "match_request_error"; -export const MATCH_FOUND = "match_found"; -export const MATCH_IN_PROGRESS = "match_in_progress"; -export const MATCH_ACCEPTED = "match_accepted"; -export const MATCH_NOT_ACCEPTED = "match_not_accepted"; +export const CANCEL_MATCH_REQUEST = "cancel_match_request"; +export const MATCH_ACCEPT_REQUEST = "match_accept_request"; +export const MATCH_DECLINE_REQUEST = "match_decline_request"; export const REMATCH_REQUEST = "rematch_request"; -export const MATCH_SUCCESSFUL = "match_successful"; -export const MATCH_ENDED = "match_ended"; -export const MATCH_STOP_REQUEST = "match_stop_request"; +export const MATCH_END_REQUEST = "match_end_request"; +export const USER_CONNECTED = "user_connected"; +export const USER_DISCONNECTED = "user_disconnected"; export const SOCKET_DISCONNECT = "disconnect"; export const SOCKET_CLIENT_DISCONNECT = "client namespace disconnect"; + +// Send +export const MATCH_FOUND = "match_found"; +export const MATCH_SUCCESSFUL = "match_successful"; +export const MATCH_UNSUCCESSFUL = "match_unsuccessful"; +export const MATCH_ENDED = "match_ended"; +export const MATCH_REQUEST_EXISTS = "match_request_exists"; +export const MATCH_REQUEST_ERROR = "match_request_error"; diff --git a/backend/matching-service/src/utils/mq_utils.ts b/backend/matching-service/src/utils/mq_utils.ts index 50ede38377..819d38ce78 100644 --- a/backend/matching-service/src/utils/mq_utils.ts +++ b/backend/matching-service/src/utils/mq_utils.ts @@ -1,42 +1,38 @@ -import { - createMatch, - hasUserDisconnected, - MatchItem, -} from "../handlers/matchHandler"; +import { createMatch, MatchRequestItem } from "../handlers/matchHandler"; +import { isUserConnected } from "../handlers/websocketHandler"; -const matchingRequests = new Map(); +const matchingRequests = new Map(); export const matchUsers = (newRequest: string) => { - const newRequestJson = JSON.parse(newRequest) as MatchItem; + const newRequestJson = JSON.parse(newRequest) as MatchRequestItem; const newRequestUid = newRequestJson.user.id; for (const [uid, pendingRequest] of matchingRequests) { if ( isExpired(pendingRequest) || - hasUserDisconnected(uid) || + !isUserConnected(uid) || uid === newRequestUid ) { matchingRequests.delete(uid); continue; } - if (isExpired(newRequestJson) || hasUserDisconnected(newRequestUid)) { + if (isExpired(newRequestJson) || !isUserConnected(uid)) { return; } if (isMatch(newRequestJson, pendingRequest)) { matchingRequests.delete(uid); createMatch(pendingRequest, newRequestJson); - console.log(`matched ${uid} and ${newRequestUid}`); return; } } matchingRequests.set(newRequestUid, newRequestJson); }; -const isExpired = (data: MatchItem): boolean => { +const isExpired = (data: MatchRequestItem): boolean => { return Date.now() - data.sentTimestamp >= data.ttlInSecs * 1000; }; -const isMatch = (req1: MatchItem, req2: MatchItem): boolean => { +const isMatch = (req1: MatchRequestItem, req2: MatchRequestItem): boolean => { const hasCommonCategory = req1.categories.some((elem) => req1.categories.includes(elem) ); diff --git a/frontend/src/components/Navbar/index.tsx b/frontend/src/components/Navbar/index.tsx index 4861358a0e..68e4d466cb 100644 --- a/frontend/src/components/Navbar/index.tsx +++ b/frontend/src/components/Navbar/index.tsx @@ -138,7 +138,7 @@ const Navbar: React.FC = (props) => { diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index aa33968d24..9b4e88789d 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -1,8 +1,15 @@ /* eslint-disable react-refresh/only-export-components */ -import { createContext, useContext, useState } from "react"; +import { createContext, useContext, useEffect, useState } from "react"; import { matchSocket } from "../utils/matchSocket"; -import { minMatchTimeout, USE_AUTH_ERROR_MESSAGE } from "../utils/constants"; +import { + FAILED_MATCH_REQUEST_MESSAGE, + MATCH_ENDED_MESSAGE, + MATCH_LOGIN_REQUIRED_MESSAGE, + MATCH_REQUEST_EXISTS_MESSAGE, + MATCH_UNSUCCESSFUL_MESSAGE, + USE_AUTH_ERROR_MESSAGE, +} from "../utils/constants"; import { useAuth } from "./AuthContext"; import { toast } from "react-toastify"; import { useAppNavigate } from "../components/NoDirectAccessRoutes"; @@ -21,16 +28,24 @@ type MatchCriteria = { }; enum MatchEvents { + // Send MATCH_REQUEST = "match_request", - MATCH_REQUEST_ERROR = "match_request_error", - MATCH_FOUND = "match_found", - MATCH_IN_PROGRESS = "match_in_progress", - MATCH_ACCEPTED = "match_accepted", - MATCH_NOT_ACCEPTED = "match_not_accepted", + CANCEL_MATCH_REQUEST = "cancel_match_request", + MATCH_ACCEPT_REQUEST = "match_accept_request", + MATCH_DECLINE_REQUEST = "match_decline_request", REMATCH_REQUEST = "rematch_request", + MATCH_END_REQUEST = "match_end_request", + + USER_CONNECTED = "user_connected", + USER_DISCONNECTED = "user_disconnected", + + // Receive + MATCH_FOUND = "match_found", MATCH_SUCCESSFUL = "match_successful", + MATCH_UNSUCCESSFUL = "match_unsuccessful", MATCH_ENDED = "match_ended", - MATCH_STOP_REQUEST = "match_stop_request", + MATCH_REQUEST_EXISTS = "match_request_exists", + MATCH_REQUEST_ERROR = "match_request_error", SOCKET_DISCONNECT = "disconnect", SOCKET_CLIENT_DISCONNECT = "io client disconnect", @@ -38,6 +53,14 @@ enum MatchEvents { SOCKET_RECONNECT_FAILED = "reconnect_failed", } +enum MatchPaths { + HOME = "/home", + TIMEOUT = "/matching/timeout", + MATCHING = "/matching", + MATCHED = "/matching/matched", + COLLAB = "/collaboration", +} + type MatchContextType = { findMatch: ( complexities: string[], @@ -45,14 +68,16 @@ type MatchContextType = { languages: string[], timeout: number ) => void; - retryMatch: () => void; + stopMatch: () => void; acceptMatch: () => void; rematch: () => void; - stopMatch: (path: string, mutual?: boolean) => void; + retryMatch: () => void; + matchingTimeout: () => void; + matchOfferTimeout: () => void; matchUser: MatchUser | null; - matchCriteria: MatchCriteria; - matchId: string | null; + matchCriteria: MatchCriteria | null; partner: MatchUser | null; + matchPending: boolean; loading: boolean; }; @@ -77,101 +102,169 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { : null ); - const [matchCriteria, setMatchCriteria] = useState({ - complexities: [], - categories: [], - languages: [], - timeout: minMatchTimeout, - }); + const [matchCriteria, setMatchCriteria] = useState( + null + ); const [matchId, setMatchId] = useState(null); const [partner, setPartner] = useState(null); const [matchPending, setMatchPending] = useState(false); const [loading, setLoading] = useState(false); - const closeConnection = () => { - matchSocket.removeAllListeners(); - matchSocket.disconnect(); + useEffect(() => { + if ( + !matchUser?.id || + (location.pathname !== MatchPaths.MATCHING && + location.pathname !== MatchPaths.MATCHED && + location.pathname !== MatchPaths.COLLAB) + ) { + resetMatchStates(); + return; + } + + openSocketConnection(); + matchSocket.emit(MatchEvents.USER_CONNECTED, matchUser?.id); + + window.addEventListener("beforeunload", () => closeSocketConnection()); + + return () => { + closeSocketConnection(); + window.removeEventListener("beforeunload", () => closeSocketConnection()); + }; + }, [matchUser?.id, location.pathname]); + + const resetMatchStates = () => { + if (location.pathname !== MatchPaths.TIMEOUT) { + setMatchCriteria(null); + } + setMatchId(null); + setPartner(null); + setMatchPending(false); + setLoading(false); }; - const openConnection = () => { - initSocketListeners(); + const openSocketConnection = () => { + console.log("user request to connect"); matchSocket.connect(); + initListeners(); }; - const initSocketListeners = () => { - if (!matchSocket.hasListeners(MatchEvents.MATCH_FOUND)) { - matchSocket.on(MatchEvents.MATCH_FOUND, ({ matchId, user1, user2 }) => { - setMatchId(matchId); - matchUser?.id === user1.id ? setPartner(user2) : setPartner(user1); - setMatchPending(true); - appNavigate("/matching/matched"); - }); - } + const closeSocketConnection = () => { + console.log("user request to disconnect"); + matchSocket.emit(MatchEvents.USER_DISCONNECTED, matchUser?.id, matchId); + matchSocket.removeAllListeners(); + }; - if (!matchSocket.hasListeners(MatchEvents.MATCH_IN_PROGRESS)) { - matchSocket.on(MatchEvents.MATCH_IN_PROGRESS, () => { - toast.error("You can only have 1 match at a time!"); - }); + const initListeners = () => { + switch (location.pathname) { + case MatchPaths.HOME: + case MatchPaths.TIMEOUT: + initMatchRequestListeners(); + return; + case MatchPaths.MATCHING: + initMatchingListeners(); + return; + case MatchPaths.MATCHED: + initMatchedListeners(); + return; + case MatchPaths.COLLAB: + initCollabListeners(); + return; + default: + return; } + }; - if (!matchSocket.hasListeners(MatchEvents.MATCH_SUCCESSFUL)) { - matchSocket.on(MatchEvents.MATCH_SUCCESSFUL, () => { - setMatchPending(false); - appNavigate("/collaboration"); - }); - } + const initMatchRequestListeners = () => { + matchSocket.on(MatchEvents.MATCH_FOUND, ({ matchId, user1, user2 }) => { + setMatchId(matchId); + matchUser?.id === user1.id ? setPartner(user2) : setPartner(user1); + setMatchPending(true); + appNavigate(MatchPaths.MATCHED); + }); - if (!matchSocket.hasListeners(MatchEvents.MATCH_NOT_ACCEPTED)) { - matchSocket.on(MatchEvents.MATCH_NOT_ACCEPTED, () => { - toast.error("Unfortunately, your partner did not accept the match."); - setMatchId(null); - setMatchPending(false); - }); - } + matchSocket.on(MatchEvents.MATCH_REQUEST_EXISTS, () => { + toast.error(MATCH_REQUEST_EXISTS_MESSAGE); + }); - if (!matchSocket.hasListeners(MatchEvents.MATCH_ENDED)) { - matchSocket.on(MatchEvents.MATCH_ENDED, () => { - toast.error("Your partner has ended the match."); // TODO: stop match during matching - afterMatchCleanUp("/home"); - }); - } + matchSocket.on(MatchEvents.MATCH_REQUEST_ERROR, () => { + toast.error(FAILED_MATCH_REQUEST_MESSAGE); + }); + }; - if (!matchSocket.hasListeners(MatchEvents.MATCH_REQUEST_ERROR)) { - matchSocket.on(MatchEvents.MATCH_REQUEST_ERROR, () => { - toast.error("Failed to send match request! Please try again later."); - }); - } + const initMatchingListeners = () => { + matchSocket.on(MatchEvents.MATCH_FOUND, ({ matchId, user1, user2 }) => { + setMatchId(matchId); + matchUser?.id === user1.id ? setPartner(user2) : setPartner(user1); + setMatchPending(true); + appNavigate(MatchPaths.MATCHED); + }); + }; - if (!matchSocket.hasListeners(MatchEvents.SOCKET_DISCONNECT)) { - matchSocket.on(MatchEvents.SOCKET_DISCONNECT, (reason) => { - if (reason !== MatchEvents.SOCKET_CLIENT_DISCONNECT) { - toast.error("Connection error! Reconnecting..."); - } - }); - } + const initMatchedListeners = () => { + matchSocket.on(MatchEvents.MATCH_SUCCESSFUL, () => { + setMatchPending(false); + appNavigate(MatchPaths.COLLAB); + }); - if (!matchSocket.io.hasListeners(MatchEvents.SOCKET_RECONNECT_SUCCESS)) { - matchSocket.io.on(MatchEvents.SOCKET_RECONNECT_SUCCESS, () => { - toast.success("Reconnected!"); - initSocketListeners(); // TODO: check - }); - } + matchSocket.on(MatchEvents.MATCH_UNSUCCESSFUL, () => { + toast.error(MATCH_UNSUCCESSFUL_MESSAGE); + setMatchPending(false); + }); - if (!matchSocket.io.hasListeners(MatchEvents.SOCKET_RECONNECT_FAILED)) { - matchSocket.io.on(MatchEvents.SOCKET_RECONNECT_FAILED, () => { - toast.error("Failed to reconnect! Please try again later."); - }); - } + matchSocket.on(MatchEvents.MATCH_FOUND, ({ matchId, user1, user2 }) => { + setMatchId(matchId); + matchUser?.id === user1.id ? setPartner(user2) : setPartner(user1); + setMatchPending(true); + appNavigate(MatchPaths.MATCHED); + }); + + matchSocket.on(MatchEvents.MATCH_REQUEST_ERROR, () => { + toast.error(FAILED_MATCH_REQUEST_MESSAGE); + }); + }; + + const initCollabListeners = () => { + matchSocket.on(MatchEvents.MATCH_ENDED, () => { + toast.error(MATCH_ENDED_MESSAGE); + appNavigate(MatchPaths.HOME); + }); }; + // TODO: disconnection / reconnection + // if (!matchSocket.hasListeners(MatchEvents.SOCKET_DISCONNECT)) { + // matchSocket.on(MatchEvents.SOCKET_DISCONNECT, (reason) => { + // if (reason !== MatchEvents.SOCKET_CLIENT_DISCONNECT) { + // toast.error("Connection error! Reconnecting..."); + // } + // }); + // } + + // if (!matchSocket.io.hasListeners(MatchEvents.SOCKET_RECONNECT_SUCCESS)) { + // matchSocket.io.on(MatchEvents.SOCKET_RECONNECT_SUCCESS, () => { + // toast.success("Reconnected!"); + // initSocketListeners(); // TODO: check + // }); + // } + + // if (!matchSocket.io.hasListeners(MatchEvents.SOCKET_RECONNECT_FAILED)) { + // matchSocket.io.on(MatchEvents.SOCKET_RECONNECT_FAILED, () => { + // toast.error("Failed to reconnect! Please try again later."); + // }); + // } + const findMatch = ( complexities: string[], categories: string[], languages: string[], timeout: number ) => { + if (!matchUser) { + toast.error(MATCH_LOGIN_REQUIRED_MESSAGE); + return; + } + setLoading(true); - openConnection(); + openSocketConnection(); matchSocket.emit( MatchEvents.MATCH_REQUEST, { @@ -181,40 +274,63 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { languages: languages, timeout: timeout, }, - (result: boolean) => { + (requested: boolean) => { setTimeout(() => setLoading(false), 500); - if (result) { + if (requested) { setMatchCriteria({ complexities, categories, languages, timeout, }); - appNavigate("/matching"); + appNavigate(MatchPaths.MATCHING); + } else { + matchSocket.removeAllListeners(); } } ); }; - const retryMatch = () => { - findMatch( - matchCriteria.complexities, - matchCriteria.categories, - matchCriteria.languages, - matchCriteria.timeout - ); + const stopMatch = () => { + switch (location.pathname) { + case MatchPaths.TIMEOUT: + appNavigate(MatchPaths.HOME); + return; + case MatchPaths.MATCHING: + matchSocket.emit(MatchEvents.CANCEL_MATCH_REQUEST, matchUser?.id); + appNavigate(MatchPaths.HOME); + return; + case MatchPaths.MATCHED: + matchSocket.emit( + MatchEvents.MATCH_DECLINE_REQUEST, + matchUser?.id, + matchId, + false + ); + appNavigate(MatchPaths.HOME); + return; + case MatchPaths.COLLAB: + matchSocket.emit(MatchEvents.MATCH_END_REQUEST, matchUser?.id, matchId); + appNavigate(MatchPaths.HOME); + return; + default: + return; + } }; const acceptMatch = () => { - matchSocket.emit(MatchEvents.MATCH_ACCEPTED, matchId); + matchSocket.emit(MatchEvents.MATCH_ACCEPT_REQUEST, matchId); }; const rematch = () => { setLoading(true); - setMatchId(null); - setPartner(null); setMatchPending(false); + if (!matchCriteria) { + toast.error(FAILED_MATCH_REQUEST_MESSAGE); + return; + } + const rematchRequest = { user: matchUser, complexities: matchCriteria.complexities, @@ -226,52 +342,59 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { MatchEvents.REMATCH_REQUEST, matchId, rematchRequest, - (result: boolean) => { + (requested: boolean) => { setTimeout(() => setLoading(false), 500); - if (result) { - appNavigate("/matching"); + if (requested) { + appNavigate(MatchPaths.MATCHING); + setPartner(null); } } ); }; - const stopMatch = (path: string, isMutual: boolean = false) => { + const retryMatch = () => { + if (!matchCriteria) { + toast.error(FAILED_MATCH_REQUEST_MESSAGE); + return; + } + + findMatch( + matchCriteria.complexities, + matchCriteria.categories, + matchCriteria.languages, + matchCriteria.timeout + ); + }; + + const matchingTimeout = () => { + matchSocket.emit(MatchEvents.CANCEL_MATCH_REQUEST, matchUser?.id); + appNavigate(MatchPaths.TIMEOUT); + }; + + const matchOfferTimeout = () => { matchSocket.emit( - MatchEvents.MATCH_STOP_REQUEST, + MatchEvents.MATCH_DECLINE_REQUEST, matchUser?.id, matchId, - matchPending, - isMutual, - () => afterMatchCleanUp(path) + true ); - }; - - const afterMatchCleanUp = (path: string) => { - closeConnection(); - setMatchCriteria({ - complexities: [], - categories: [], - languages: [], - timeout: minMatchTimeout, - }); - setMatchId(null); - setPartner(null); - setMatchPending(false); - appNavigate(path); + appNavigate(MatchPaths.HOME); }; return ( diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 1fce8cb2a0..8ff72bb77f 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -3,24 +3,25 @@ import { Button, Stack, Typography } from "@mui/material"; import classes from "./index.module.css"; import { useMatch } from "../../contexts/MatchContext"; import { USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; +import { Navigate } from "react-router-dom"; const CollabSandbox: React.FC = () => { const match = useMatch(); if (!match) { throw new Error(USE_MATCH_ERROR_MESSAGE); } - const { stopMatch } = match; + const { stopMatch, partner } = match; + + if (!partner) { + return ; + } return ( Collaborative Sandbox Coming soon... - diff --git a/frontend/src/pages/Matched/index.tsx b/frontend/src/pages/Matched/index.tsx index ab349ae464..395a34db7f 100644 --- a/frontend/src/pages/Matched/index.tsx +++ b/frontend/src/pages/Matched/index.tsx @@ -9,7 +9,11 @@ import { } from "@mui/material"; import classes from "./index.module.css"; import { useMatch } from "../../contexts/MatchContext"; -import { USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; +import { + MATCH_OFFER_TIMEOUT_MESSAGE, + MATCH_UNSUCCESSFUL_MESSAGE, + USE_MATCH_ERROR_MESSAGE, +} from "../../utils/constants"; import { useEffect, useState } from "react"; import { toast } from "react-toastify"; import { Navigate } from "react-router-dom"; @@ -24,12 +28,12 @@ const Matched: React.FC = () => { throw new Error(USE_MATCH_ERROR_MESSAGE); } const { + matchOfferTimeout, acceptMatch, rematch, - stopMatch, matchUser, - matchId, partner, + matchPending, loading, } = match; @@ -52,9 +56,9 @@ const Matched: React.FC = () => { useEffect(() => { if (timeLeft <= 0) { accepted - ? toast.error("Match unsuccessful! Your partner was not ready.") - : toast.error("Match offer timeout!"); - stopMatch("/home", true); + ? toast.error(MATCH_UNSUCCESSFUL_MESSAGE) + : toast.error(MATCH_OFFER_TIMEOUT_MESSAGE); + matchOfferTimeout(); } }, [timeLeft]); @@ -106,7 +110,7 @@ const Matched: React.FC = () => { variant="contained" color="secondary" fullWidth - disabled={accepted} + disabled={matchPending && accepted} onClick={rematch} > Rematch @@ -114,7 +118,7 @@ const Matched: React.FC = () => { - diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index d7baee6ea6..6431348daf 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -66,8 +66,9 @@ const AuthProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }) .then(() => userClient.post("users/send-verification-email", { email })) .then((res) => { - navigate("/login"); - toast.success(res.data.message); + navigate(`/auth/verifyEmail/${res.data.data.id}`); + // navigate("/login"); + // toast.success(res.data.message); }) .catch((err) => { setUser(null); diff --git a/frontend/src/pages/EmailVerification/index.module.css b/frontend/src/pages/EmailVerification/index.module.css new file mode 100644 index 0000000000..c2a09d7adf --- /dev/null +++ b/frontend/src/pages/EmailVerification/index.module.css @@ -0,0 +1,8 @@ +.fullheight { + display: flex; +} + +.center { + justify-content: center; + align-items: center; +} diff --git a/frontend/src/pages/EmailVerification/index.tsx b/frontend/src/pages/EmailVerification/index.tsx new file mode 100644 index 0000000000..14e5224377 --- /dev/null +++ b/frontend/src/pages/EmailVerification/index.tsx @@ -0,0 +1,97 @@ +import { useNavigate, useParams } from "react-router-dom"; +import AppMargin from "../../components/AppMargin"; +import { Box, Button, Stack, TextField, Typography } from "@mui/material"; +import { useEffect, useState } from "react"; +import { userClient } from "../../utils/api"; +import classes from "./index.module.css"; +import { toast } from "react-toastify"; + +const EmailVerification: React.FC = () => { + const { userId } = useParams<{ userId: string }>(); + const [token, setToken] = useState(""); + const [email, setEmail] = useState(""); + const navigate = useNavigate(); + + useEffect(() => { + userClient + .get(`/users/${userId}/`) + .then((res) => { + setEmail(res.data.data.email); + }) + .catch((err) => console.error(err)); + }, []); + + const handleResend = () => { + userClient + .post(`/users/send-verification-email`, { email }) + .catch((err) => console.error(err)); + }; + + const handleVerify = (e: React.FormEvent) => { + e.preventDefault(); + userClient + .get(`/users/verify-email/${email}/${token}`) + .then((res) => { + navigate("/auth/login"); + toast.success(res.data.message); + }) + .catch((err) => console.error(err)); + }; + + return ( + + + ({ + textAlign: "center", + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.spacing(1), + width: "40vw", + padding: theme.spacing(4), + })} + > + Verify your email address + ({ margin: theme.spacing(2, 0) })}> + An account verification token has been sent to your email. + +
+ setToken(e.target.value)} + /> + ({ marginTop: theme.spacing(4) })} + > + + + + +
+
+
+ ); +}; + +export default EmailVerification; From 7231f62e5a3a49875dcbdc01cad903ee56384fa0 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Fri, 18 Oct 2024 21:53:10 +0800 Subject: [PATCH 44/82] Fix signup and login links --- frontend/src/pages/LogIn/index.tsx | 2 +- frontend/src/pages/SignUp/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/LogIn/index.tsx b/frontend/src/pages/LogIn/index.tsx index b4fb026060..b55983b731 100644 --- a/frontend/src/pages/LogIn/index.tsx +++ b/frontend/src/pages/LogIn/index.tsx @@ -134,7 +134,7 @@ const LogIn: React.FC = () => { textDecoration: "underline", }, }} - onClick={() => navigate("/signup")} + onClick={() => navigate("/auth/signup")} > Sign up diff --git a/frontend/src/pages/SignUp/index.tsx b/frontend/src/pages/SignUp/index.tsx index 6b81a6db8e..f6636ee815 100644 --- a/frontend/src/pages/SignUp/index.tsx +++ b/frontend/src/pages/SignUp/index.tsx @@ -172,7 +172,7 @@ const SignUp: React.FC = () => { textDecoration: "underline", }, }} - onClick={() => navigate("/login")} + onClick={() => navigate("/auth/login")} > Log in From 7e9b2bdd4d661623c8bb467721277b7f3142725f Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Fri, 18 Oct 2024 22:27:42 +0800 Subject: [PATCH 45/82] Remove unused code and fix docker volumes --- backend/user-service/README.md | 8 ++++++++ backend/user-service/tests/setup.ts | 11 ----------- backend/user-service/utils/mailer.ts | 24 ------------------------ docker-compose.yml | 2 +- 4 files changed, 9 insertions(+), 36 deletions(-) diff --git a/backend/user-service/README.md b/backend/user-service/README.md index aa63045bd5..0a1cbdb384 100644 --- a/backend/user-service/README.md +++ b/backend/user-service/README.md @@ -34,6 +34,14 @@ 4. A default admin account (`email: admin@gmail.com` and `password: Admin@123`) wil be created. If you wish to change the default credentials, update them in `.env`. Alternatively, you can also edit your credentials and user profile after you have created the default account. +5. To view the contents stored in Redis, + + 1. Go to [http://localhost:5540](http://localhost:5540). + + 2. Click on "Add Redis Database". + + 3. Enter `host.internal.docker` as the Host. + ## Running User Service without Docker 1. Open Command Line/Terminal and navigate into the `user-service` directory. diff --git a/backend/user-service/tests/setup.ts b/backend/user-service/tests/setup.ts index b686f98848..27e78d75f9 100644 --- a/backend/user-service/tests/setup.ts +++ b/backend/user-service/tests/setup.ts @@ -2,13 +2,6 @@ import mongoose from "mongoose"; import redisClient from "../config/redis"; beforeAll(async () => { - // mongo = await MongoMemoryServer.create(); - // const mongoUri = mongo.getUri(); - - // if (mongoose.connection.readyState !== 0) { - // await mongoose.disconnect(); - // } - const mongoUri = process.env.MONGO_URI_TEST || "mongodb://mongo:mongo@mongo:27017/"; @@ -27,10 +20,6 @@ afterEach(async () => { }); afterAll(async () => { - // if (mongo) { - // await mongo.stop(); - // } - await mongoose.connection.close(); await redisClient.disconnect(); }); diff --git a/backend/user-service/utils/mailer.ts b/backend/user-service/utils/mailer.ts index e9f7e4b0de..9301a8c110 100644 --- a/backend/user-service/utils/mailer.ts +++ b/backend/user-service/utils/mailer.ts @@ -1,17 +1,8 @@ import nodemailer from "nodemailer"; import dotenv from "dotenv"; -// import path from "path"; -// import { fileURLToPath } from "url"; -import { Options } from "nodemailer/lib/mailer"; -// import hbs from "nodemailer-express-handlebars"; import Handlebars from "handlebars"; import { ACCOUNT_VERIFICATION_TEMPLATE } from "./constants"; -// type ExtendedOptions = Options & { -// template: string; -// context: Record; -// }; - dotenv.config(); const SERVICE = process.env.SERVICE; @@ -23,21 +14,6 @@ const transporter = nodemailer.createTransport({ auth: { user: USER, pass: PASS }, }); -// const dirname = fileURLToPath(import.meta.url); - -// transporter.use( -// "compile", -// hbs({ -// viewEngine: { -// extname: ".hbs", -// layoutsDir: path.resolve(path.dirname(dirname), "../templates/"), -// defaultLayout: "", -// }, -// viewPath: path.resolve(path.dirname(dirname), "../templates/"), -// extName: ".hbs", -// }) -// ); - export const sendAccVerificationMail = async ( to: string, subject: string, diff --git a/docker-compose.yml b/docker-compose.yml index 58d288f70d..d7dbe34911 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -135,7 +135,7 @@ services: networks: - peerprep-network volumes: - - redis-insight-data:/db + - redis-insight-data:/data depends_on: redis: condition: service_healthy From 8a2642e5583daf4c1aa5d78878f6a8379cbb0580 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Fri, 18 Oct 2024 22:28:28 +0800 Subject: [PATCH 46/82] Add forget password functionality --- frontend/src/App.tsx | 2 + frontend/src/contexts/AuthContext.tsx | 11 ++- frontend/src/pages/ForgetPassword/index.tsx | 102 ++++++++++++++++++++ frontend/src/pages/LogIn/index.tsx | 1 + 4 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/ForgetPassword/index.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2a05ce7053..df5709cb9d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,6 +10,7 @@ import Landing from "./pages/Landing"; import Home from "./pages/Home"; import SignUp from "./pages/SignUp"; import LogIn from "./pages/LogIn"; +import ForgetPassword from "./pages/ForgetPassword"; import Matched from "./pages/Matched"; import Timeout from "./pages/Timeout"; import ProtectedRoutes from "./components/ProtectedRoutes"; @@ -52,6 +53,7 @@ function App() {
} /> } /> + } /> ); diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 7be6dae74c..83896d6c57 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -29,6 +29,7 @@ type AuthContextType = { ) => void; login: (email: string, password: string) => void; logout: () => void; + resetPassword: (email: string) => void; user: User | null; setUser: React.Dispatch>; loading: boolean; @@ -107,12 +108,20 @@ const AuthProvider: React.FC<{ children?: React.ReactNode }> = (props) => { toast.success(SUCCESS_LOG_OUT); }; + const resetPassword = (email: string) => { + userClient.post("/users/reset-password", { email }).then(() => { + toast.success("Reset link sent to your email"); + }); + }; + if (loading) { return ; } return ( - + {children} ); diff --git a/frontend/src/pages/ForgetPassword/index.tsx b/frontend/src/pages/ForgetPassword/index.tsx new file mode 100644 index 0000000000..1ae70684b2 --- /dev/null +++ b/frontend/src/pages/ForgetPassword/index.tsx @@ -0,0 +1,102 @@ +import { Box, Button, Stack, TextField, Typography } from "@mui/material"; +import LogInSvg from "../../assets/login.svg?react"; +import { useAuth } from "../../contexts/AuthContext"; +import { emailValidator } from "../../utils/validators"; +import { ToastContainer } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; +import { useForm } from "react-hook-form"; +import { USE_AUTH_ERROR_MESSAGE } from "../../utils/constants"; + +const ForgetPassword: React.FC = () => { + const auth = useAuth(); + if (!auth) { + throw new Error(USE_AUTH_ERROR_MESSAGE); + } + const { resetPassword } = auth; + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm<{ email: string }>({ mode: "all" }); + + return ( + + + ({ + backgroundColor: "secondary.main", + padding: theme.spacing(2, 10), + justifyContent: "center", + })} + > + + Reset Password + + ({ + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + })} + onSubmit={handleSubmit((data) => resetPassword(data.email))} + noValidate + > + + Enter your email address and we will send you a password reset + link. + + value.trim(), + validate: { emailValidator }, + })} + error={!!errors.email} + helperText={errors.email?.message} + /> + + + + + + + + + + ); +}; + +export default ForgetPassword; diff --git a/frontend/src/pages/LogIn/index.tsx b/frontend/src/pages/LogIn/index.tsx index 202b2f9d59..18700bd19c 100644 --- a/frontend/src/pages/LogIn/index.tsx +++ b/frontend/src/pages/LogIn/index.tsx @@ -122,6 +122,7 @@ const LogIn: React.FC = () => { textDecoration: "underline", }, }} + onClick={() => navigate("/forget-password")} > Forget password From fce340e7478958ce0db9d042d6e1a19137f58919 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Fri, 18 Oct 2024 23:31:04 +0800 Subject: [PATCH 47/82] Redirect users to email verification page --- .../controller/user-controller.ts | 6 +- frontend/src/App.tsx | 10 ++- frontend/src/contexts/AuthContext.tsx | 5 +- .../src/pages/EmailVerification/index.tsx | 79 +++++++++++-------- 4 files changed, 60 insertions(+), 40 deletions(-) diff --git a/backend/user-service/controller/user-controller.ts b/backend/user-service/controller/user-controller.ts index 5b8d0f57b5..728667bf29 100644 --- a/backend/user-service/controller/user-controller.ts +++ b/backend/user-service/controller/user-controller.ts @@ -155,12 +155,10 @@ export const verifyUser = async ( const updatedUser = await _updateUserVerification(email); if (!updatedUser) { - return res.status(404).json({ message: `User ${email} not verified.` }); + return res.status(404).json({ message: `User not verified.` }); } - return res - .status(200) - .json({ message: `User ${email} verified successfully.` }); + return res.status(200).json({ message: `User verified successfully.` }); } catch (error) { return res .status(500) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f35ae943e8..52d86a6969 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { Routes, Route } from "react-router-dom"; +import { Routes, Route, Navigate } from "react-router-dom"; import NewQuestion from "./pages/NewQuestion"; import QuestionDetail from "./pages/QuestionDetail"; @@ -64,10 +64,14 @@ function App() { } /> - + + } /> } /> } /> - } /> + } + /> diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 6431348daf..a3b6f3479d 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -67,8 +67,6 @@ const AuthProvider: React.FC<{ children?: React.ReactNode }> = (props) => { .then(() => userClient.post("users/send-verification-email", { email })) .then((res) => { navigate(`/auth/verifyEmail/${res.data.data.id}`); - // navigate("/login"); - // toast.success(res.data.message); }) .catch((err) => { setUser(null); @@ -89,6 +87,9 @@ const AuthProvider: React.FC<{ children?: React.ReactNode }> = (props) => { navigate("/home"); }) .catch((err) => { + if (err.response?.data.message === "User not verified.") { + navigate(`/auth/verifyEmail`); + } setUser(null); toast.error(err.response?.data.message || err.message); }); diff --git a/frontend/src/pages/EmailVerification/index.tsx b/frontend/src/pages/EmailVerification/index.tsx index 14e5224377..93174aa0b0 100644 --- a/frontend/src/pages/EmailVerification/index.tsx +++ b/frontend/src/pages/EmailVerification/index.tsx @@ -7,7 +7,7 @@ import classes from "./index.module.css"; import { toast } from "react-toastify"; const EmailVerification: React.FC = () => { - const { userId } = useParams<{ userId: string }>(); + const { userId } = useParams<{ userId?: string }>(); const [token, setToken] = useState(""); const [email, setEmail] = useState(""); const navigate = useNavigate(); @@ -19,16 +19,17 @@ const EmailVerification: React.FC = () => { setEmail(res.data.data.email); }) .catch((err) => console.error(err)); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const handleResend = () => { + const handleSendEmail = () => { userClient .post(`/users/send-verification-email`, { email }) + .then((res) => toast.success(res.data.message)) .catch((err) => console.error(err)); }; - const handleVerify = (e: React.FormEvent) => { - e.preventDefault(); + const handleVerifyAcc = () => { userClient .get(`/users/verify-email/${email}/${token}`) .then((res) => { @@ -59,35 +60,51 @@ const EmailVerification: React.FC = () => { })} > Verify your email address - ({ margin: theme.spacing(2, 0) })}> - An account verification token has been sent to your email. - -
- ({ margin: theme.spacing(2, 0) })}> + An account verification token has been sent to your email. + + ) : ( + ({ margin: theme.spacing(2, 0) })}> + An account verification token will be sent to your email. + + )} + setEmail(e.target.value)} + slotProps={{ + input: { + endAdornment: , + }, + }} + /> + setToken(e.target.value)} + /> + ({ marginTop: theme.spacing(4) })} + > + - - - + Resend + + + From 4357b49fd2c5f2f99d3e0d80591634b120afe466 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sat, 19 Oct 2024 00:57:11 +0800 Subject: [PATCH 48/82] Fix docker compose test --- backend/user-service/config/redis.ts | 5 ++++- backend/user-service/model/repository.ts | 1 + backend/user-service/scripts/seed.ts | 1 + backend/user-service/tests/authRoutes.spec.ts | 4 ++++ docker-compose-test.yml | 14 ++++++++------ 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/backend/user-service/config/redis.ts b/backend/user-service/config/redis.ts index a2bb73e90a..6f6e74a319 100644 --- a/backend/user-service/config/redis.ts +++ b/backend/user-service/config/redis.ts @@ -3,7 +3,10 @@ import dotenv from "dotenv"; dotenv.config(); -const REDIS_URI = process.env.REDIS_URI || "redis://localhost:6379"; +const REDIS_URI = + process.env.NODE_ENV === "test" + ? process.env.REDIS_URI_TEST + : process.env.REDIS_URI || "redis://localhost:6379"; const client = createClient({ url: REDIS_URI }); diff --git a/backend/user-service/model/repository.ts b/backend/user-service/model/repository.ts index a2ab797c7d..6c2ca553e5 100644 --- a/backend/user-service/model/repository.ts +++ b/backend/user-service/model/repository.ts @@ -31,6 +31,7 @@ export async function createUser( email, password, isAdmin, + isVerified, }); return user.save(); } diff --git a/backend/user-service/scripts/seed.ts b/backend/user-service/scripts/seed.ts index af456bb385..db8c17cb51 100644 --- a/backend/user-service/scripts/seed.ts +++ b/backend/user-service/scripts/seed.ts @@ -38,6 +38,7 @@ export async function seedAdminAccount() { adminUsername, adminEmail, hashedPassword, + true, true ); console.log("Admin account created successfully."); diff --git a/backend/user-service/tests/authRoutes.spec.ts b/backend/user-service/tests/authRoutes.spec.ts index 9644103ca3..0951730310 100644 --- a/backend/user-service/tests/authRoutes.spec.ts +++ b/backend/user-service/tests/authRoutes.spec.ts @@ -4,6 +4,8 @@ import supertest from "supertest"; import app from "../app"; import UserModel from "../model/user-model"; +jest.setTimeout(10000); + const request = supertest(app); const AUTH_BASE_URL = "/api/auth"; @@ -25,6 +27,7 @@ const insertAdminUser = async () => { email, password: hashedPassword, isAdmin: true, + isVerified: true, }).save(); return { email, password }; @@ -37,6 +40,7 @@ const insertNonAdminUser = async () => { lastName, email, password: hashedPassword, + isVerified: true, }).save(); return { email, password }; diff --git a/docker-compose-test.yml b/docker-compose-test.yml index 54dc33f6f3..ae304de2a5 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -1,13 +1,15 @@ +name: peerprep-test + services: test-user-service: image: peerprep/user-service build: ./backend/user-service env_file: ./backend/user-service/.env environment: - - MONGO_URI_TEST=mongodb://mongo:mongo@mongo:27017/ + - MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/ depends_on: - - mongo - - redis + - test-mongo + - test-redis networks: - peerprep-network volumes: @@ -16,7 +18,7 @@ services: restart: on-failure command: ["npm", "test"] - mongo: + test-mongo: image: mongo restart: always networks: @@ -24,9 +26,9 @@ services: env_file: - ./backend/.env - redis: + test-redis: image: redis:8.0-M01 - container_name: redis + container_name: test-redis networks: - peerprep-network healthcheck: From 5550ea02943feb13d8dce160c9cd5e2a38522aa9 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sat, 19 Oct 2024 00:57:44 +0800 Subject: [PATCH 49/82] Update sample env --- backend/user-service/.env.sample | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/user-service/.env.sample b/backend/user-service/.env.sample index 8b5eb3e154..14bb3016aa 100644 --- a/backend/user-service/.env.sample +++ b/backend/user-service/.env.sample @@ -29,4 +29,8 @@ USER=EMAIL_ADDRESS PASS=PASSWORD # Redis configuration -REDIS_URI=REDIS_URI \ No newline at end of file +REDIS_URI=REDIS_URI + +# Test +MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/ +REDIS_URI_TEST=redis://test-redis:6379 \ No newline at end of file From 43d20eaf40350d597a62af76fcda887bea66b355 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Sat, 19 Oct 2024 01:30:29 +0800 Subject: [PATCH 50/82] Update forget password functionality --- .../controller/user-controller.ts | 94 +++++++++- backend/user-service/model/repository.ts | 16 ++ backend/user-service/routes/user-routes.ts | 7 + backend/user-service/swagger.yml | 81 +++++++- backend/user-service/utils/constants.ts | 20 ++ backend/user-service/utils/mailer.ts | 6 +- frontend/src/contexts/AuthContext.tsx | 16 +- .../src/pages/ForgetPassword/index.module.css | 8 + frontend/src/pages/ForgetPassword/index.tsx | 173 ++++++++++++++++-- frontend/src/pages/LogIn/index.tsx | 2 +- 10 files changed, 390 insertions(+), 33 deletions(-) create mode 100644 frontend/src/pages/ForgetPassword/index.module.css diff --git a/backend/user-service/controller/user-controller.ts b/backend/user-service/controller/user-controller.ts index 5b8d0f57b5..12f7803e53 100644 --- a/backend/user-service/controller/user-controller.ts +++ b/backend/user-service/controller/user-controller.ts @@ -12,6 +12,7 @@ import { updateUserById as _updateUserById, updateUserPrivilegeById as _updateUserPrivilegeById, updateUserVerification as _updateUserVerification, + updateUserPassword as _updateUserPassword, } from "../model/repository"; import { validateEmail, @@ -25,8 +26,13 @@ import { upload } from "../config/multer"; import { uploadFileToFirebase } from "../utils/utils"; import redisClient from "../config/redis"; import crypto from "crypto"; -import { sendAccVerificationMail } from "../utils/mailer"; -import { ACCOUNT_VERIFICATION_SUBJ } from "../utils/constants"; +import { sendMail } from "../utils/mailer"; +import { + ACCOUNT_VERIFICATION_SUBJ, + ACCOUNT_VERIFICATION_TEMPLATE, + RESET_PASSWORD_SUBJ, + RESET_PASSWORD_TEMPLATE, +} from "../utils/constants"; export async function createUser( req: Request, @@ -114,10 +120,11 @@ export const sendVerificationMail = async ( const emailToken = crypto.randomBytes(16).toString("hex"); await redisClient.set(email, emailToken, { EX: 60 * 5 }); // expire in 5 minutes - await sendAccVerificationMail( + await sendMail( email, ACCOUNT_VERIFICATION_SUBJ, user.username, + ACCOUNT_VERIFICATION_TEMPLATE, emailToken ); @@ -334,6 +341,87 @@ export async function updateUser( } } +export const sendResetPasswordMail = async ( + req: Request, + res: Response +): Promise => { + try { + const { email } = req.body; + const user = await _findUserByEmail(email); + + if (!user) { + return res.status(404).json({ message: `User not found` }); + } + + const emailToken = crypto.randomBytes(16).toString("hex"); + await redisClient.set(email, emailToken, { EX: 60 * 5 }); // expire in 5 minutes + await sendMail( + email, + RESET_PASSWORD_SUBJ, + user.username, + RESET_PASSWORD_TEMPLATE, + emailToken + ); + + return res.status(200).json({ + message: "Reset password email sent. Please check your inbox.", + data: { email, id: user.id }, + }); + } catch (error) { + return res.status(500).json({ + message: "Unknown error when sending reset password email!", + error, + }); + } +}; + +export const resetPassword = async ( + req: Request, + res: Response +): Promise => { + try { + const { email, token, password } = req.body; + + const user = await _findUserByEmail(email); + if (!user) { + return res.status(404).json({ message: `User not found` }); + } + + const expectedToken = await redisClient.get(email); + + if (expectedToken !== token) { + return res + .status(400) + .json({ message: "Invalid token. Please request for a new one." }); + } + + const { isValid: isValidPassword, message: passwordMessage } = + validatePassword(password); + if (!isValidPassword) { + return res.status(400).json({ message: passwordMessage }); + } + + const salt = bcrypt.genSaltSync(10); + const hashedPassword = bcrypt.hashSync(password, salt); + + const updatedUser = await _updateUserPassword(email, hashedPassword); + + if (!updatedUser) { + return res + .status(404) + .json({ message: `User's password not reset.` }); + } + + return res + .status(200) + .json({ message: `User's password successfully reset.` }); + } catch (error) { + return res + .status(500) + .json({ message: "Unknown error when resetting user password!", error }); + } +}; + export async function updateUserPrivilege( req: Request, res: Response diff --git a/backend/user-service/model/repository.ts b/backend/user-service/model/repository.ts index a2ab797c7d..30b7b625c8 100644 --- a/backend/user-service/model/repository.ts +++ b/backend/user-service/model/repository.ts @@ -31,6 +31,7 @@ export async function createUser( email, password, isAdmin, + isVerified, }); return user.save(); } @@ -114,6 +115,21 @@ export async function updateUserVerification( ); } +export async function updateUserPassword( + email: string, + password: string +): Promise { + return UserModel.findOneAndUpdate( + { email }, + { + $set: { + password, + }, + }, + { new: true } // return the updated user + ); +} + export async function deleteUserById(userId: string): Promise { return UserModel.findByIdAndDelete(userId); } diff --git a/backend/user-service/routes/user-routes.ts b/backend/user-service/routes/user-routes.ts index c91c350427..c971dc70cf 100644 --- a/backend/user-service/routes/user-routes.ts +++ b/backend/user-service/routes/user-routes.ts @@ -10,12 +10,15 @@ import { updateUser, updateUserPrivilege, verifyUser, + sendResetPasswordMail, + resetPassword, } from "../controller/user-controller"; import { verifyAccessToken, verifyIsAdmin, verifyIsOwnerOrAdmin, } from "../middleware/basic-access-control"; +import { send } from "process"; const router = express.Router(); @@ -34,6 +37,10 @@ router.post("/images", createImageLink); router.post("/send-verification-email", sendVerificationMail); +router.post("/send-reset-password-email", sendResetPasswordMail); + +router.post("/reset-password", resetPassword); + router.get("/:id", getUser); router.get("/verify-email/:email/:token", verifyUser); diff --git a/backend/user-service/swagger.yml b/backend/user-service/swagger.yml index 37345ee0b9..e48c9c1ade 100644 --- a/backend/user-service/swagger.yml +++ b/backend/user-service/swagger.yml @@ -71,11 +71,22 @@ components: password: type: string required: true - EmailVerification: + Email: properties: email: type: string required: true + ResetPassword: + properties: + email: + type: string + required: true + token: + type: string + required: true + password: + type: string + required: true UserResponse: properties: message: @@ -347,7 +358,73 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/EmailVerification" + $ref: "#/components/schemas/Email" + responses: + 200: + description: Successful Response + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: Message + 404: + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + 500: + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/ServerErrorResponse" + /api/users/send-reset-password-email: + post: + summary: Send reset password email + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Email" + responses: + 200: + description: Successful Response + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: Message + 404: + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + 500: + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/ServerErrorResponse" + /api/users/reset-password: + post: + summary: Reset password + tags: + - users + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ResetPassword" responses: 200: description: Successful Response diff --git a/backend/user-service/utils/constants.ts b/backend/user-service/utils/constants.ts index d7847ff492..94d4bd0713 100644 --- a/backend/user-service/utils/constants.ts +++ b/backend/user-service/utils/constants.ts @@ -17,3 +17,23 @@ export const ACCOUNT_VERIFICATION_TEMPLATE = ` `; + +export const RESET_PASSWORD_SUBJ = "Password Reset Link"; + +export const RESET_PASSWORD_TEMPLATE = ` + + + Password Reset + + + +

Hello {{username}}!

+

+ You have requested to reset your password. Please use this token: {{token}} to reset your password. +

+

If you did not request for a password reset, please ignore this email.

+

Regards,

+

Peerprep G28

+ + +`; diff --git a/backend/user-service/utils/mailer.ts b/backend/user-service/utils/mailer.ts index 9301a8c110..eec92c297e 100644 --- a/backend/user-service/utils/mailer.ts +++ b/backend/user-service/utils/mailer.ts @@ -1,7 +1,6 @@ import nodemailer from "nodemailer"; import dotenv from "dotenv"; import Handlebars from "handlebars"; -import { ACCOUNT_VERIFICATION_TEMPLATE } from "./constants"; dotenv.config(); @@ -14,13 +13,14 @@ const transporter = nodemailer.createTransport({ auth: { user: USER, pass: PASS }, }); -export const sendAccVerificationMail = async ( +export const sendMail = async ( to: string, subject: string, username: string, + htmlTemplate: string, token: string ) => { - const template = Handlebars.compile(ACCOUNT_VERIFICATION_TEMPLATE); + const template = Handlebars.compile(htmlTemplate); const replacement = { username, token }; const html = template(replacement); const options = { diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index dc639daa11..5393108e03 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -18,7 +18,6 @@ type AuthContextType = { ) => void; login: (email: string, password: string) => void; logout: () => void; - resetPassword: (email: string) => void; user: User | null; setUser: React.Dispatch>; loading: boolean; @@ -102,19 +101,20 @@ const AuthProvider: React.FC<{ children?: React.ReactNode }> = (props) => { toast.success(SUCCESS_LOG_OUT); }; - const resetPassword = (email: string) => { - userClient.post("/users/reset-password", { email }).then(() => { - toast.success("Reset link sent to your email"); - }); - }; - if (loading) { return ; } return ( {children} diff --git a/frontend/src/pages/ForgetPassword/index.module.css b/frontend/src/pages/ForgetPassword/index.module.css new file mode 100644 index 0000000000..c2a09d7adf --- /dev/null +++ b/frontend/src/pages/ForgetPassword/index.module.css @@ -0,0 +1,8 @@ +.fullheight { + display: flex; +} + +.center { + justify-content: center; + align-items: center; +} diff --git a/frontend/src/pages/ForgetPassword/index.tsx b/frontend/src/pages/ForgetPassword/index.tsx index 1ae70684b2..c350491a14 100644 --- a/frontend/src/pages/ForgetPassword/index.tsx +++ b/frontend/src/pages/ForgetPassword/index.tsx @@ -1,25 +1,167 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import AppMargin from "../../components/AppMargin"; import { Box, Button, Stack, TextField, Typography } from "@mui/material"; import LogInSvg from "../../assets/login.svg?react"; -import { useAuth } from "../../contexts/AuthContext"; import { emailValidator } from "../../utils/validators"; -import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; +import PasswordTextField from "../../components/PasswordTextField"; import { useForm } from "react-hook-form"; -import { USE_AUTH_ERROR_MESSAGE } from "../../utils/constants"; +import { userClient } from "../../utils/api"; +import { passwordValidator } from "../../utils/validators"; +import classes from "./index.module.css"; +import { toast } from "react-toastify"; +import { + PASSWORD_REQUIRED_ERROR_MESSAGE, + PASSWORD_MISMATCH_ERROR_MESSAGE, +} from "../../utils/constants"; const ForgetPassword: React.FC = () => { - const auth = useAuth(); - if (!auth) { - throw new Error(USE_AUTH_ERROR_MESSAGE); - } - const { resetPassword } = auth; + const navigate = useNavigate(); + + const [email, setEmail] = useState(""); + const [hasSentEmail, setHasSentEmail] = useState(false); const { - register, - handleSubmit, - formState: { errors }, + register: registerEmail, + handleSubmit: handleSubmitEmail, + formState: { errors: errorsEmail }, } = useForm<{ email: string }>({ mode: "all" }); + const { + register: registerPassword, + watch: watchPassword, + handleSubmit: handleSubmitPassword, + formState: { errors: errorsPassword }, + } = useForm<{ token: string; password: string; confirmPassword: string }>({ + mode: "all", + }); + + const handleSendEmail = async (email: string) => { + userClient + .post("/users/send-reset-password-email", { email }) + .then((res) => { + setEmail(res.data.data.email); + toast.success(res.data.message); + setHasSentEmail(true); + }) + .catch((err) => { + toast.error(err.response?.data.message || err.message); + }); + }; + + const handleResetPassword = async (password: string, token: string) => { + userClient + .post(`/users/reset-password`, { email, token, password }) + .then((res) => { + navigate("/auth/login"); + toast.success(res.data.message); + }) + .catch((err) => { + toast.error(err.response?.data.message || err.message); + }); + }; + + if (hasSentEmail) { + return ( + + + ({ + textAlign: "center", + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.spacing(1), + width: "40vw", + padding: theme.spacing(4), + })} + > + Change your password + ({ margin: theme.spacing(2, 0) })}> + An account verification token has been sent to your email. + +
+ handleResetPassword(data.password, data.token) + )} + > + ({ marginTop: theme.spacing(1) })} + {...registerPassword("token", { + setValueAs: (value: string) => value.trim(), + })} + error={!!errorsPassword.token} + helperText={errorsPassword.token?.message} + /> + value.trim(), + required: PASSWORD_REQUIRED_ERROR_MESSAGE, + validate: { passwordValidator }, + })} + error={!!errorsPassword.password} + helperText={errorsPassword.password?.message} + /> + ({ marginTop: theme.spacing(1) })} + {...registerPassword("confirmPassword", { + setValueAs: (value: string) => value.trim(), + validate: { + matchPassword: (value) => + watchPassword("password") === value || + PASSWORD_MISMATCH_ERROR_MESSAGE, + }, + })} + error={!!errorsPassword.confirmPassword} + helperText={errorsPassword.confirmPassword?.message} + /> + ({ marginTop: theme.spacing(4) })} + > + + + + +
+
+
+ ); + } + return ( { marginTop: theme.spacing(2), marginBottom: theme.spacing(2), })} - onSubmit={handleSubmit((data) => resetPassword(data.email))} + onSubmit={handleSubmitEmail((data) => handleSendEmail(data.email))} noValidate > @@ -67,12 +209,12 @@ const ForgetPassword: React.FC = () => { fullWidth margin="normal" type="email" - {...register("email", { + {...registerEmail("email", { setValueAs: (value: string) => value.trim(), validate: { emailValidator }, })} - error={!!errors.email} - helperText={errors.email?.message} + error={!!errorsEmail.email} + helperText={errorsEmail.email?.message} /> diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index 6c228a8ce7..c3b26604f5 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -91,6 +91,8 @@ export const MATCH_LOGIN_REQUIRED_MESSAGE = export const MATCH_OFFER_TIMEOUT_MESSAGE = "Match offer timeout!"; export const MATCH_CONNECTION_ERROR = "Connection error! Please try again later."; +export const QUESTION_DOES_NOT_EXIST_ERROR = + "There are no questions with the specified complexity and category. Please try another combination."; /* Image paths */ export const FIND_MATCH_FORM_PATH = "/find_match_form.png"; From 5b1419f8064eedc4abdedbaac053973c317fd60e Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Sat, 19 Oct 2024 10:23:03 +0800 Subject: [PATCH 52/82] Refine forget password functionality --- frontend/src/pages/ForgetPassword/index.tsx | 20 +++++++++++++++++++- frontend/src/utils/constants.ts | 3 +++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/ForgetPassword/index.tsx b/frontend/src/pages/ForgetPassword/index.tsx index c350491a14..11683e3483 100644 --- a/frontend/src/pages/ForgetPassword/index.tsx +++ b/frontend/src/pages/ForgetPassword/index.tsx @@ -14,6 +14,7 @@ import { toast } from "react-toastify"; import { PASSWORD_REQUIRED_ERROR_MESSAGE, PASSWORD_MISMATCH_ERROR_MESSAGE, + TOKEN_REQUIRED_ERROR_MESSAGE, } from "../../utils/constants"; const ForgetPassword: React.FC = () => { @@ -21,6 +22,7 @@ const ForgetPassword: React.FC = () => { const [email, setEmail] = useState(""); const [hasSentEmail, setHasSentEmail] = useState(false); + const [isLoading, setisLoading] = useState(false); const { register: registerEmail, @@ -38,6 +40,7 @@ const ForgetPassword: React.FC = () => { }); const handleSendEmail = async (email: string) => { + setisLoading(true); userClient .post("/users/send-reset-password-email", { email }) .then((res) => { @@ -47,10 +50,14 @@ const ForgetPassword: React.FC = () => { }) .catch((err) => { toast.error(err.response?.data.message || err.message); + }) + .finally(() => { + setisLoading(false); }); }; const handleResetPassword = async (password: string, token: string) => { + setisLoading(true); userClient .post(`/users/reset-password`, { email, token, password }) .then((res) => { @@ -59,6 +66,9 @@ const ForgetPassword: React.FC = () => { }) .catch((err) => { toast.error(err.response?.data.message || err.message); + }) + .finally(() => { + setisLoading(false); }); }; @@ -99,6 +109,7 @@ const ForgetPassword: React.FC = () => { sx={(theme) => ({ marginTop: theme.spacing(1) })} {...registerPassword("token", { setValueAs: (value: string) => value.trim(), + required: TOKEN_REQUIRED_ERROR_MESSAGE, })} error={!!errorsPassword.token} helperText={errorsPassword.token?.message} @@ -148,10 +159,16 @@ const ForgetPassword: React.FC = () => { onClick={() => { handleSendEmail(email); }} + disabled={isLoading} > Resend - @@ -220,6 +237,7 @@ const ForgetPassword: React.FC = () => { type="submit" variant="contained" sx={(theme) => ({ margin: theme.spacing(2, 0) })} + disabled={isLoading} > Send Reset Link diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index 3358474b89..eb0d344ecc 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -50,6 +50,9 @@ export const PASSWORD_SPECIAL_CHAR_ERROR_MESSAGE = export const PASSWORD_WEAK_ERROR_MESSAGE = "Password is weak"; export const PASSWORD_MISMATCH_ERROR_MESSAGE = "Password does not match"; +/* Token Validation */ +export const TOKEN_REQUIRED_ERROR_MESSAGE = "Token is required"; + /* Toast Messages */ // Authentication export const SUCCESS_LOG_OUT = "Logged out successfully!"; From a802b6c9e0d48aa7be80d6b6507e613e93975ea1 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sat, 19 Oct 2024 10:57:27 +0800 Subject: [PATCH 53/82] Redirect users with /auth routes are invalid --- frontend/src/App.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 52d86a6969..036eda57e5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -72,6 +72,7 @@ function App() { path="verifyEmail/:userId?" element={} /> + } /> From 1c4dd3ff5188fe0057aa8b73327a6c13852176fc Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sat, 19 Oct 2024 11:08:15 +0800 Subject: [PATCH 54/82] Remove unused env --- docker-compose-test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker-compose-test.yml b/docker-compose-test.yml index ae304de2a5..fadc631824 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -5,8 +5,6 @@ services: image: peerprep/user-service build: ./backend/user-service env_file: ./backend/user-service/.env - environment: - - MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/ depends_on: - test-mongo - test-redis From 1acd6630c9ef26b7ea0c696a3aa3a030628bf56b Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sat, 19 Oct 2024 11:13:47 +0800 Subject: [PATCH 55/82] Update test mongo uri --- backend/user-service/tests/setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/user-service/tests/setup.ts b/backend/user-service/tests/setup.ts index 27e78d75f9..a85ae2e02f 100644 --- a/backend/user-service/tests/setup.ts +++ b/backend/user-service/tests/setup.ts @@ -3,7 +3,7 @@ import redisClient from "../config/redis"; beforeAll(async () => { const mongoUri = - process.env.MONGO_URI_TEST || "mongodb://mongo:mongo@mongo:27017/"; + process.env.MONGO_URI_TEST || "mongodb://mongo:mongo@test-mongo:27017/"; await mongoose.connect(mongoUri, {}); await redisClient.connect(); From 5e1ae7ab3745cc19adf1cfc317d5784a79402f78 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Sat, 19 Oct 2024 14:13:46 +0800 Subject: [PATCH 56/82] Fix bugs --- .../controller/user-controller.ts | 22 +++++++++++++------ backend/user-service/routes/user-routes.ts | 1 - frontend/src/pages/ForgetPassword/index.tsx | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/backend/user-service/controller/user-controller.ts b/backend/user-service/controller/user-controller.ts index 12f7803e53..7610d958a1 100644 --- a/backend/user-service/controller/user-controller.ts +++ b/backend/user-service/controller/user-controller.ts @@ -119,7 +119,9 @@ export const sendVerificationMail = async ( } const emailToken = crypto.randomBytes(16).toString("hex"); - await redisClient.set(email, emailToken, { EX: 60 * 5 }); // expire in 5 minutes + await redisClient.set(`email_verification:${email}`, emailToken, { + EX: 60 * 5, + }); // expire in 5 minutes await sendMail( email, ACCOUNT_VERIFICATION_SUBJ, @@ -152,7 +154,7 @@ export const verifyUser = async ( return res.status(404).json({ message: `User ${email} not found` }); } - const expectedToken = await redisClient.get(email); + const expectedToken = await redisClient.get(`email_verification:${email}`); if (expectedToken !== token) { return res @@ -353,8 +355,16 @@ export const sendResetPasswordMail = async ( return res.status(404).json({ message: `User not found` }); } + if (!user.isVerified) { + return res.status(403).json({ + message: "User is not verified. Please verify your account first.", + }); + } + const emailToken = crypto.randomBytes(16).toString("hex"); - await redisClient.set(email, emailToken, { EX: 60 * 5 }); // expire in 5 minutes + await redisClient.set(`password_reset:${email}`, emailToken, { + EX: 60 * 5, + }); // expire in 5 minutes await sendMail( email, RESET_PASSWORD_SUBJ, @@ -387,7 +397,7 @@ export const resetPassword = async ( return res.status(404).json({ message: `User not found` }); } - const expectedToken = await redisClient.get(email); + const expectedToken = await redisClient.get(`password_reset:${email}`); if (expectedToken !== token) { return res @@ -407,9 +417,7 @@ export const resetPassword = async ( const updatedUser = await _updateUserPassword(email, hashedPassword); if (!updatedUser) { - return res - .status(404) - .json({ message: `User's password not reset.` }); + return res.status(404).json({ message: `User's password not reset.` }); } return res diff --git a/backend/user-service/routes/user-routes.ts b/backend/user-service/routes/user-routes.ts index c971dc70cf..07f06030d7 100644 --- a/backend/user-service/routes/user-routes.ts +++ b/backend/user-service/routes/user-routes.ts @@ -18,7 +18,6 @@ import { verifyIsAdmin, verifyIsOwnerOrAdmin, } from "../middleware/basic-access-control"; -import { send } from "process"; const router = express.Router(); diff --git a/frontend/src/pages/ForgetPassword/index.tsx b/frontend/src/pages/ForgetPassword/index.tsx index 11683e3483..b82a632dfc 100644 --- a/frontend/src/pages/ForgetPassword/index.tsx +++ b/frontend/src/pages/ForgetPassword/index.tsx @@ -105,7 +105,7 @@ const ForgetPassword: React.FC = () => { ({ marginTop: theme.spacing(1) })} {...registerPassword("token", { setValueAs: (value: string) => value.trim(), From 918eb10a191b2f07a64d6062c300bf92191570b6 Mon Sep 17 00:00:00 2001 From: jolynloh Date: Sat, 19 Oct 2024 14:50:02 +0800 Subject: [PATCH 57/82] Show confirmation popup message before user leaves (unloads) matching pages --- frontend/src/contexts/MatchContext.tsx | 16 ++++++++++++++-- frontend/src/pages/NewQuestion/index.tsx | 7 ++----- frontend/src/pages/QuestionEdit/index.tsx | 7 ++----- frontend/src/utils/constants.ts | 9 +++++++++ 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 1e552eef8b..215a2c743f 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -3,6 +3,7 @@ import { createContext, useContext, useEffect, useState } from "react"; import { matchSocket } from "../utils/matchSocket"; import { + ABORT_MATCH_PROCESS_CONFIRMATION_MESSAGE, FAILED_MATCH_REQUEST_MESSAGE, MATCH_CONNECTION_ERROR, MATCH_ENDED_MESSAGE, @@ -130,11 +131,22 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { openSocketConnection(); matchSocket.emit(MatchEvents.USER_CONNECTED, matchUser?.id); - window.addEventListener("beforeunload", () => closeSocketConnection()); + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + e.preventDefault(); + e.returnValue = ABORT_MATCH_PROCESS_CONFIRMATION_MESSAGE; // for legacy support, does not actually display message + }; + + const handleUnload = () => { + closeSocketConnection(); + }; + + window.addEventListener("beforeunload", handleBeforeUnload); + window.addEventListener("unload", handleUnload); return () => { closeSocketConnection(); - window.removeEventListener("beforeunload", () => closeSocketConnection()); + window.removeEventListener("beforeunload", handleBeforeUnload); + window.removeEventListener("unload", handleUnload); }; }, [matchUser?.id, location.pathname]); diff --git a/frontend/src/pages/NewQuestion/index.tsx b/frontend/src/pages/NewQuestion/index.tsx index d6731f433d..29c3a894f1 100644 --- a/frontend/src/pages/NewQuestion/index.tsx +++ b/frontend/src/pages/NewQuestion/index.tsx @@ -15,6 +15,7 @@ import reducer, { import { toast } from "react-toastify"; import { + ABORT_CREATE_OR_EDIT_QUESTION_CONFIRMATION_MESSAGE, complexityList, FAILED_QUESTION_CREATE, FILL_ALL_FIELDS, @@ -47,11 +48,7 @@ const NewQuestion = () => { selectedComplexity || selectedCategories.length > 0 ) { - if ( - !confirm( - "Are you sure you want to leave this page? All process will be lost." - ) - ) { + if (!confirm(ABORT_CREATE_OR_EDIT_QUESTION_CONFIRMATION_MESSAGE)) { return; } } diff --git a/frontend/src/pages/QuestionEdit/index.tsx b/frontend/src/pages/QuestionEdit/index.tsx index c3eb79e048..b522c5144f 100644 --- a/frontend/src/pages/QuestionEdit/index.tsx +++ b/frontend/src/pages/QuestionEdit/index.tsx @@ -11,6 +11,7 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import { toast } from "react-toastify"; import { + ABORT_CREATE_OR_EDIT_QUESTION_CONFIRMATION_MESSAGE, complexityList, FAILED_QUESTION_UPDATE, FILL_ALL_FIELDS, @@ -61,11 +62,7 @@ const QuestionEdit = () => { }, [state.selectedQuestion]); const handleBack = () => { - if ( - !confirm( - "Are you sure you want to leave this page? All process will be lost." - ) - ) { + if (!confirm(ABORT_CREATE_OR_EDIT_QUESTION_CONFIRMATION_MESSAGE)) { return; } navigate("/questions"); diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index c3b26604f5..640c591395 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -94,6 +94,15 @@ export const MATCH_CONNECTION_ERROR = export const QUESTION_DOES_NOT_EXIST_ERROR = "There are no questions with the specified complexity and category. Please try another combination."; +/* Alerts & Dialog Boxes */ +// Questions +export const ABORT_CREATE_OR_EDIT_QUESTION_CONFIRMATION_MESSAGE = + "Are you sure you want to leave this page? All process will be lost."; + +// Match +export const ABORT_MATCH_PROCESS_CONFIRMATION_MESSAGE = + "Are you sure you want to leave the matching process?"; + /* Image paths */ export const FIND_MATCH_FORM_PATH = "/find_match_form.png"; export const MATCH_FOUND_PATH = "/match_found.png"; From 58ec9f66cc4084244079f1c495d5edfa4880f6ba Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sun, 20 Oct 2024 00:10:34 +0800 Subject: [PATCH 58/82] Fix signup and login tests --- frontend/src/pages/LogIn/LogIn.test.tsx | 2 +- frontend/src/pages/SignUp/SignUp.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/LogIn/LogIn.test.tsx b/frontend/src/pages/LogIn/LogIn.test.tsx index ccfad9df65..5f50b4b102 100644 --- a/frontend/src/pages/LogIn/LogIn.test.tsx +++ b/frontend/src/pages/LogIn/LogIn.test.tsx @@ -59,7 +59,7 @@ describe("Log In Components", () => { expect(signUpButton).toBeInTheDocument(); fireEvent.click(signUpButton); - expect(mockUseNavigate).toHaveBeenCalledWith("/signup"); + expect(mockUseNavigate).toHaveBeenCalledWith("/auth/signup"); }); }); diff --git a/frontend/src/pages/SignUp/SignUp.test.tsx b/frontend/src/pages/SignUp/SignUp.test.tsx index 8eec5f8da7..231e3d9843 100644 --- a/frontend/src/pages/SignUp/SignUp.test.tsx +++ b/frontend/src/pages/SignUp/SignUp.test.tsx @@ -74,7 +74,7 @@ describe("Sign Up Components", () => { expect(logInButton).toBeInTheDocument(); fireEvent.click(logInButton); - expect(mockUseNavigate).toHaveBeenCalledWith("/login"); + expect(mockUseNavigate).toHaveBeenCalledWith("/auth/login"); }); }); From 0ac07cb9bb94457b6ba70a61d8ea316807b39087 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sun, 20 Oct 2024 12:28:38 +0800 Subject: [PATCH 59/82] Update ci --- .github/workflows/ci.yml | 52 ++++++++++++++----------- backend/question-service/.env.sample | 2 + backend/question-service/tests/setup.ts | 11 +----- backend/user-service/.env.sample | 2 +- docker-compose-test.yml | 50 +++++++++++++++++++++++- 5 files changed, 84 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5dde70ed67..505efd528d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,21 +7,34 @@ on: env: NODE_VERSION: 20 - FIREBASE_PROJECT_ID: ${{ secrets.FIREBASE_PROJECT_ID }} - FIREBASE_PRIVATE_KEY: ${{ secrets.FIREBASE_PRIVATE_KEY }} - FIREBASE_CLIENT_EMAIL: ${{ secrets.FIREBASE_CLIENT_EMAIL }} - FIREBASE_STORAGE_BUCKET: ${{ secrets.FIREBASE_STORAGE_BUCKET }} - JWT_SECRET: ${{ secrets.JWT_SECRET }} permissions: contents: read jobs: - ci: + frontend-ci: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Setting node version + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Install dependencies + working-directory: frontend + run: npm install + - name: Linting + working-directory: frontend + run: npm run lint + - name: Frontend tests + working-directory: frontend + run: docker compose -f docker-compose-test.yml run --rm test-frontend + backend-ci: runs-on: ubuntu-latest strategy: matrix: - service: [frontend, backend/question-service, backend/user-service] + service: [question-service, user-service] steps: - name: Checkout code uses: actions/checkout@v4 @@ -30,21 +43,16 @@ jobs: with: node-version: ${{ env.NODE_VERSION }} - name: Install dependencies - working-directory: ${{ matrix.service }} + working-directory: backend/${{ matrix.service }} run: npm install - name: Linting - working-directory: ${{ matrix.service }} + working-directory: backend/${{ matrix.service }} run: npm run lint - - name: Set .env variables - working-directory: ${{ matrix.service }} - run: | - touch .env - echo "FIREBASE_PROJECT_ID=${{ env.FIREBASE_PROJECT_ID }}" >> .env - echo "FIREBASE_PRIVATE_KEY=${{ env.FIREBASE_PRIVATE_KEY }}" >> .env - echo "FIREBASE_CLIENT_EMAIL=${{ env.FIREBASE_CLIENT_EMAIL }}" >> .env - echo "FIREBASE_CLIENT_EMAIL=${{ env.FIREBASE_CLIENT_EMAIL }}" >> .env - echo "FIREBASE_STORAGE_BUCKET=${{ env.FIREBASE_STORAGE_BUCKET }}" >> .env - echo "JWT_SECRET=${{ env.JWT_SECRET }}" >> .env - - name: Tests - working-directory: ${{ matrix.service }} - run: npm test + - name: Backend tests + env: + FIREBASE_PROJECT_ID: ${{ secrets.FIREBASE_PROJECT_ID }} + FIREBASE_PRIVATE_KEY: ${{ secrets.FIREBASE_PRIVATE_KEY }} + FIREBASE_CLIENT_EMAIL: ${{ secrets.FIREBASE_CLIENT_EMAIL }} + FIREBASE_STORAGE_BUCKET: ${{ secrets.FIREBASE_STORAGE_BUCKET }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} + run: docker compose -f docker-compose-test.yml run --rm test-${{ matrix.service }} diff --git a/backend/question-service/.env.sample b/backend/question-service/.env.sample index 8c6b2bfb82..2804758eb4 100644 --- a/backend/question-service/.env.sample +++ b/backend/question-service/.env.sample @@ -12,3 +12,5 @@ FIREBASE_STORAGE_BUCKET=>FIREBASE_STORAGE_BUCKET> ORIGINS=http://localhost:5173,http://127.0.0.1:5173 USER_SERVICE_URL=http://user-service:3001/api + +MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/ diff --git a/backend/question-service/tests/setup.ts b/backend/question-service/tests/setup.ts index 455506c382..37d7edf1fa 100644 --- a/backend/question-service/tests/setup.ts +++ b/backend/question-service/tests/setup.ts @@ -1,11 +1,8 @@ import mongoose from "mongoose"; -import { MongoMemoryServer } from "mongodb-memory-server"; - -let mongo: MongoMemoryServer; beforeAll(async () => { - mongo = await MongoMemoryServer.create(); - const mongoUri = mongo.getUri(); + const mongoUri = + process.env.MONGO_URI_TEST || "mongodb://mongo:mongo@test-mongo:27017/"; if (mongoose.connection.readyState !== 0) { await mongoose.disconnect(); @@ -24,9 +21,5 @@ afterEach(async () => { }); afterAll(async () => { - if (mongo) { - await mongo.stop(); - } - await mongoose.connection.close(); }); diff --git a/backend/user-service/.env.sample b/backend/user-service/.env.sample index 14bb3016aa..3cac024c32 100644 --- a/backend/user-service/.env.sample +++ b/backend/user-service/.env.sample @@ -29,7 +29,7 @@ USER=EMAIL_ADDRESS PASS=PASSWORD # Redis configuration -REDIS_URI=REDIS_URI +REDIS_URI=redis://redis:6379 # Test MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/ diff --git a/docker-compose-test.yml b/docker-compose-test.yml index fadc631824..049b131a7d 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -4,7 +4,22 @@ services: test-user-service: image: peerprep/user-service build: ./backend/user-service - env_file: ./backend/user-service/.env + # env_file: ./backend/user-service/.env + environment: + - NODE_ENV=test + - PORT=3001 + - JWT_SECRET + - ADMIN_FIRST_NAME=Admin + - ADMIN_LAST_NAME=User + - ADMIN_USERNAME=administrator + - ADMIN_EMAIL=admin@gmail.com + - ADMIN_PASSWORD=Admin@123 + - FIREBASE_PROJECT_ID + - FIREBASE_PRIVATE_KEY + - FIREBASE_CLIENT_EMAIL + - FIREBASE_STORAGE_BUCKET + - MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/ + - REDIS_URI_TEST=redis://test-redis:6379 depends_on: - test-mongo - test-redis @@ -16,6 +31,39 @@ services: restart: on-failure command: ["npm", "test"] + test-question-service: + image: peerprep/question-service + build: ./backend/question-service + # env_file: ./backend/question-service/.env + environment: + - NODE_ENV=test + - PORT=3000 + - FIREBASE_PROJECT_ID + - FIREBASE_PRIVATE_KEY + - FIREBASE_CLIENT_EMAIL + - FIREBASE_STORAGE_BUCKET + - MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/ + depends_on: + - test-mongo + networks: + - peerprep-network + volumes: + - ./backend/question-service:/question-service + - /question-service/node_modules + restart: on-failure + command: ["npm", "test"] + + test-frontend: + image: peerprep/frontend + build: ./frontend + networks: + - peerprep-network + volumes: + - ./frontend:/frontend + - /frontend/node_modules + restart: on-failure + command: ["npm", "test"] + test-mongo: image: mongo restart: always From 367cf1046325f19f2a247958a9a918e09c54fd44 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sun, 20 Oct 2024 14:28:30 +0800 Subject: [PATCH 60/82] Increase rabbitmq healthcheck interval --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index d7dbe34911..b99ed4083b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -107,7 +107,7 @@ services: env_file: ./backend/matching-service/.env healthcheck: test: rabbitmq-diagnostics check_port_connectivity - interval: 10s + interval: 20s timeout: 10s retries: 10 From 50747b0ab276659d4026ea5c767c210ceee6e6a9 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sun, 20 Oct 2024 15:24:18 +0800 Subject: [PATCH 61/82] Add test mongo credentials --- docker-compose-test.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docker-compose-test.yml b/docker-compose-test.yml index 049b131a7d..37302b7c30 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -69,8 +69,11 @@ services: restart: always networks: - peerprep-network - env_file: - - ./backend/.env + # env_file: + # - ./backend/.env + environment: + - MONGO_INITDB_ROOT_USERNAME=mongo + - MONGO_INITDB_ROOT_PASSWORD=mongo test-redis: image: redis:8.0-M01 From 9ae828a7d60cdc2d28e18df6c7fc6f57df35bb30 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Sun, 20 Oct 2024 15:37:57 +0800 Subject: [PATCH 62/82] Fix frontend lint --- frontend/src/contexts/MatchContext.tsx | 22 ++++++++++++++++++---- frontend/src/pages/CollabSandbox/index.tsx | 1 + frontend/src/pages/Matched/index.tsx | 9 ++++++--- frontend/src/pages/Matching/index.tsx | 2 ++ 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 1e552eef8b..3883cebfac 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -98,7 +98,8 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { throw new Error(USE_AUTH_ERROR_MESSAGE); } const { user } = auth; - const [matchUser, _setMatchUser] = useState( + console.log(user); + const [matchUser] = useState( user ? { id: user.id, @@ -136,6 +137,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { closeSocketConnection(); window.removeEventListener("beforeunload", () => closeSocketConnection()); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [matchUser?.id, location.pathname]); const resetMatchStates = () => { @@ -220,7 +222,11 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const initMatchRequestListeners = () => { matchSocket.on(MatchEvents.MATCH_FOUND, ({ matchId, user1, user2 }) => { setMatchId(matchId); - matchUser?.id === user1.id ? setPartner(user2) : setPartner(user1); + if (matchUser?.id === user1.id) { + setPartner(user2); + } else { + setPartner(user1); + } setMatchPending(true); appNavigate(MatchPaths.MATCHED); }); @@ -237,7 +243,11 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const initMatchingListeners = () => { matchSocket.on(MatchEvents.MATCH_FOUND, ({ matchId, user1, user2 }) => { setMatchId(matchId); - matchUser?.id === user1.id ? setPartner(user2) : setPartner(user1); + if (matchUser?.id === user1.id) { + setPartner(user2); + } else { + setPartner(user1); + } setMatchPending(true); appNavigate(MatchPaths.MATCHED); }); @@ -256,7 +266,11 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { matchSocket.on(MatchEvents.MATCH_FOUND, ({ matchId, user1, user2 }) => { setMatchId(matchId); - matchUser?.id === user1.id ? setPartner(user2) : setPartner(user1); + if (matchUser?.id === user1.id) { + setPartner(user2); + } else { + setPartner(user1); + } setMatchPending(true); appNavigate(MatchPaths.MATCHED); }); diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index d017371646..969a9e89bb 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -16,6 +16,7 @@ const CollabSandbox: React.FC = () => { useEffect(() => { verifyMatchStatus(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); if (loading) { diff --git a/frontend/src/pages/Matched/index.tsx b/frontend/src/pages/Matched/index.tsx index 5acb29e9f4..c545ac2f84 100644 --- a/frontend/src/pages/Matched/index.tsx +++ b/frontend/src/pages/Matched/index.tsx @@ -55,11 +55,14 @@ const Matched: React.FC = () => { useEffect(() => { if (timeLeft <= 0) { - accepted - ? toast.error(MATCH_UNSUCCESSFUL_MESSAGE) - : toast.error(MATCH_OFFER_TIMEOUT_MESSAGE); + if (accepted) { + toast.error(MATCH_UNSUCCESSFUL_MESSAGE); + } else { + toast.error(MATCH_OFFER_TIMEOUT_MESSAGE); + } matchOfferTimeout(); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [timeLeft]); if (!matchUser || !partner) { diff --git a/frontend/src/pages/Matching/index.tsx b/frontend/src/pages/Matching/index.tsx index 987bf8959c..03530bd56d 100644 --- a/frontend/src/pages/Matching/index.tsx +++ b/frontend/src/pages/Matching/index.tsx @@ -35,12 +35,14 @@ const Matching: React.FC = () => { }); return () => clearInterval(timer); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { if (timeLeft <= 0) { matchingTimeout(); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [timeLeft]); if (!matchCriteria) { From 24a8427d78baa44de6cada4465bf1d686de18024 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Sun, 20 Oct 2024 15:48:08 +0800 Subject: [PATCH 63/82] Fix warnings --- .../components/NoDirectAccessRoutes/index.tsx | 16 +--------------- frontend/src/components/UseAppNavigate/index.tsx | 16 ++++++++++++++++ frontend/src/contexts/MatchContext.tsx | 2 +- 3 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 frontend/src/components/UseAppNavigate/index.tsx diff --git a/frontend/src/components/NoDirectAccessRoutes/index.tsx b/frontend/src/components/NoDirectAccessRoutes/index.tsx index 85c8ec9eb7..0af10ae54c 100644 --- a/frontend/src/components/NoDirectAccessRoutes/index.tsx +++ b/frontend/src/components/NoDirectAccessRoutes/index.tsx @@ -1,18 +1,4 @@ -import { Navigate, Outlet, useLocation, useNavigate } from "react-router-dom"; -import React from "react"; - -export const useAppNavigate = () => { - const navigate = useNavigate(); - - const appNavigate = (path: string) => { - navigate(path, { - replace: location.pathname !== "/home", - state: { from: "app-navigation" }, - }); - }; - - return appNavigate; -}; +import { Navigate, Outlet, useLocation } from "react-router-dom"; const NoDirectAccessRoutes: React.FC = () => { const location = useLocation(); diff --git a/frontend/src/components/UseAppNavigate/index.tsx b/frontend/src/components/UseAppNavigate/index.tsx new file mode 100644 index 0000000000..d68826bf65 --- /dev/null +++ b/frontend/src/components/UseAppNavigate/index.tsx @@ -0,0 +1,16 @@ +import { useNavigate } from "react-router-dom"; + +export const useAppNavigate = () => { + const navigate = useNavigate(); + + const appNavigate = (path: string) => { + navigate(path, { + replace: location.pathname !== "/home", + state: { from: "app-navigation" }, + }); + }; + + return appNavigate; +}; + +export default useAppNavigate; diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 3883cebfac..83524bab33 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -13,7 +13,7 @@ import { } from "../utils/constants"; import { useAuth } from "./AuthContext"; import { toast } from "react-toastify"; -import { useAppNavigate } from "../components/NoDirectAccessRoutes"; +import useAppNavigate from "../components/UseAppNavigate"; type MatchUser = { id: string; From 34f8c7df704030a440c025c09c769094b37c0deb Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Sun, 20 Oct 2024 15:52:22 +0800 Subject: [PATCH 64/82] remove log --- frontend/src/contexts/MatchContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 83524bab33..77670f2c21 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -98,7 +98,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { throw new Error(USE_AUTH_ERROR_MESSAGE); } const { user } = auth; - console.log(user); + const [matchUser] = useState( user ? { From 831418b49a9b90b542b6c73c6dcc2337cb072221 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Sun, 20 Oct 2024 15:59:45 +0800 Subject: [PATCH 65/82] Fix ci.yml --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 505efd528d..9a9028ae98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,6 @@ jobs: working-directory: frontend run: npm run lint - name: Frontend tests - working-directory: frontend run: docker compose -f docker-compose-test.yml run --rm test-frontend backend-ci: runs-on: ubuntu-latest From 40af7b97a9daf6c6f50de8f5d0c8e6b147003a95 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Sun, 20 Oct 2024 19:58:41 +0800 Subject: [PATCH 66/82] Fix match bug on account change --- frontend/src/contexts/MatchContext.tsx | 65 ++++++++++++-------------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index b6af67aec3..2554034159 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -100,16 +100,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { } const { user } = auth; - const [matchUser] = useState( - user - ? { - id: user.id, - username: user.username, - profile: user.profilePictureUrl, - } - : null - ); - + const [matchUser, setMatchUser] = useState(null); const [matchCriteria, setMatchCriteria] = useState( null ); @@ -118,6 +109,18 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const [matchPending, setMatchPending] = useState(false); const [loading, setLoading] = useState(true); + useEffect(() => { + if (user) { + setMatchUser({ + id: user.id, + username: user.username, + profile: user.profilePictureUrl, + }); + } else { + setMatchUser(null); + } + }, [user]); + useEffect(() => { if ( !matchUser?.id || @@ -233,14 +236,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const initMatchRequestListeners = () => { matchSocket.on(MatchEvents.MATCH_FOUND, ({ matchId, user1, user2 }) => { - setMatchId(matchId); - if (matchUser?.id === user1.id) { - setPartner(user2); - } else { - setPartner(user1); - } - setMatchPending(true); - appNavigate(MatchPaths.MATCHED); + handleMatchFound(matchId, user1, user2); }); matchSocket.on(MatchEvents.MATCH_REQUEST_EXISTS, () => { @@ -254,14 +250,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const initMatchingListeners = () => { matchSocket.on(MatchEvents.MATCH_FOUND, ({ matchId, user1, user2 }) => { - setMatchId(matchId); - if (matchUser?.id === user1.id) { - setPartner(user2); - } else { - setPartner(user1); - } - setMatchPending(true); - appNavigate(MatchPaths.MATCHED); + handleMatchFound(matchId, user1, user2); }); }; @@ -277,14 +266,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }); matchSocket.on(MatchEvents.MATCH_FOUND, ({ matchId, user1, user2 }) => { - setMatchId(matchId); - if (matchUser?.id === user1.id) { - setPartner(user2); - } else { - setPartner(user1); - } - setMatchPending(true); - appNavigate(MatchPaths.MATCHED); + handleMatchFound(matchId, user1, user2); }); matchSocket.on(MatchEvents.MATCH_REQUEST_ERROR, () => { @@ -299,6 +281,21 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }); }; + const handleMatchFound = ( + matchId: string, + user1: MatchUser, + user2: MatchUser + ) => { + setMatchId(matchId); + if (matchUser?.id === user1.id) { + setPartner(user2); + } else { + setPartner(user1); + } + setMatchPending(true); + appNavigate(MatchPaths.MATCHED); + }; + const findMatch = ( complexities: string[], categories: string[], From 590065201004d974cd9fe136251dc96871bbf677 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sun, 20 Oct 2024 20:21:15 +0800 Subject: [PATCH 67/82] Add instructions to run redis --- backend/user-service/.env.sample | 3 ++- backend/user-service/README.md | 14 +++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/backend/user-service/.env.sample b/backend/user-service/.env.sample index 14bb3016aa..fc13daa4bf 100644 --- a/backend/user-service/.env.sample +++ b/backend/user-service/.env.sample @@ -29,7 +29,8 @@ USER=EMAIL_ADDRESS PASS=PASSWORD # Redis configuration -REDIS_URI=REDIS_URI +REDIS_URI=redis://redis:6379 # Uncomment if you're running the user service with docker +# REDIS_URI=redis://localhost:6379 # Uncomment if you're running the user service individually without docker # Test MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/ diff --git a/backend/user-service/README.md b/backend/user-service/README.md index 0a1cbdb384..6618903821 100644 --- a/backend/user-service/README.md +++ b/backend/user-service/README.md @@ -44,11 +44,19 @@ ## Running User Service without Docker -1. Open Command Line/Terminal and navigate into the `user-service` directory. +1. Set up and run Redis using `docker compose run --rm --name redis -p 6379:6379 redis`. -2. Run the command: `npm install`. This will install all the necessary dependencies. +2. Open Command Line/Terminal and navigate into the `user-service` directory. -3. Run the command `npm start` to start the User Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. +3. Run the command: `npm install`. This will install all the necessary dependencies. + +4. Run the command `npm start` to start the User Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. + +## Running User Service Individually with Docker + +1. Open the command line/terminal. + +2. Run the command `docker compose run user-service` to start up the user service and its dependencies. ## After running From 94fa627695c7f043cf2b47a3bf719150b865254e Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Sun, 20 Oct 2024 20:26:34 +0800 Subject: [PATCH 68/82] separate mongodb instances for each service --- backend/.env.sample | 16 ----------- backend/README.md | 16 +++-------- backend/matching-service/.env.sample | 2 +- backend/matching-service/server.ts | 2 +- backend/question-service/.env.sample | 24 +++++++++++++--- backend/question-service/README.md | 6 +++- backend/question-service/server.ts | 2 +- backend/user-service/.env.sample | 26 +++++++++++++---- backend/user-service/README.md | 12 +++++--- backend/user-service/server.ts | 2 +- docker-compose-test.yml | 4 +-- docker-compose.yml | 42 ++++++++++++++++++++++------ 12 files changed, 97 insertions(+), 57 deletions(-) delete mode 100644 backend/.env.sample diff --git a/backend/.env.sample b/backend/.env.sample deleted file mode 100644 index a0fef16d39..0000000000 --- a/backend/.env.sample +++ /dev/null @@ -1,16 +0,0 @@ -# Credentials for MongoDB and Mongo Express. -# Create a copy of this file and name it `.env`. Change the values accordingly. - -# MongoDB credentials -MONGO_INITDB_ROOT_USERNAME=root -MONGO_INITDB_ROOT_PASSWORD=example - -# Mongo Express credentials -ME_CONFIG_BASICAUTH_USERNAME=admin -ME_CONFIG_BASICAUTH_PASSWORD=password - -# Do not change anything below this line -ME_CONFIG_MONGODB_ADMINUSERNAME=${MONGO_INITDB_ROOT_USERNAME} -ME_CONFIG_MONGODB_ADMINPASSWORD=${MONGO_INITDB_ROOT_PASSWORD} - -ME_CONFIG_MONGODB_URL=mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017/ diff --git a/backend/README.md b/backend/README.md index a7633268b2..bb3dc6ba1c 100644 --- a/backend/README.md +++ b/backend/README.md @@ -2,22 +2,12 @@ > Before proceeding to each microservice for more instructions: -1. Set-up either a local or cloud MongoDB. +1. Set up cloud MongoDB if not using docker. We recommend this if you are just testing out each microservice separately to avoid needing to manually set up multiple instances of local MongoDB. Else, if you are using docker-compose.yml to run PeerPrep, check out the READMEs in the different backend microservices to set up the env for the local MongoDB instances. -2. Set-up Firebase. +2. Set up Firebase. 3. Follow the instructions [here](https://nodejs.org/en/download/package-manager) to set up Node v20. -## Setting-up local MongoDB (only if you are using Docker) - -1. In the `backend` directory, create a copy of the `.env.sample` file and name it `.env`. - -2. To set up credentials for the MongoDB database, update `MONGO_INITDB_ROOT_USERNAME`, `MONGO_INITDB_ROOT_PASSWORD` of the `.env` file. - -3. Your local Mongo URI will be `mongodb://:@mongo:27017/`. Take note of it as we will be using in the `.env` files in the various microservices later on. - -4. You can view the MongoDB collections locally using Mongo Express. To set up Mongo Express, update `ME_CONFIG_BASICAUTH_USERNAME` and `ME_CONFIG_BASICAUTH_PASSWORD`. The username and password will be the login credentials when you access Mongo Express at http://localhost:8081. - ## Setting-up cloud MongoDB (in production) > This guide references the [user-service README in the PeerPrep-UserService repository](https://github.com/CS3219-AY2425S1/PeerPrep-UserService/blob/main/user-service/README.md) @@ -97,6 +87,8 @@ ## Setting-up Firebase +> For ease of testing, you can set up just one firebase to use across all the microservices that need it. + 1. Go to https://console.firebase.google.com/u/0/. 2. Create a project and choose a project name. Navigate to `Storage` and click on it to activate it. diff --git a/backend/matching-service/.env.sample b/backend/matching-service/.env.sample index 3ee6b5d853..4307b9473f 100644 --- a/backend/matching-service/.env.sample +++ b/backend/matching-service/.env.sample @@ -1,5 +1,5 @@ NODE_ENV=development -PORT=3002 +SERVICE_PORT=3002 ORIGINS=http://localhost:5173,http://127.0.0.1:5173 diff --git a/backend/matching-service/server.ts b/backend/matching-service/server.ts index 7bc95a8f00..a48b459c8d 100644 --- a/backend/matching-service/server.ts +++ b/backend/matching-service/server.ts @@ -17,7 +17,7 @@ io.on("connection", (socket) => { handleWebsocketMatchEvents(socket); }); -const PORT = process.env.PORT || 3002; +const PORT = process.env.SERVICE_PORT || 3002; if (process.env.NODE_ENV !== "test") { connectRabbitMq() diff --git a/backend/question-service/.env.sample b/backend/question-service/.env.sample index 2804758eb4..adcfe2bbcf 100644 --- a/backend/question-service/.env.sample +++ b/backend/question-service/.env.sample @@ -1,8 +1,5 @@ NODE_ENV=development -PORT=3000 - -MONGO_CLOUD_URI= -MONGO_LOCAL_URI= +SERVICE_PORT=3000 FIREBASE_PROJECT_ID= FIREBASE_PRIVATE_KEY= @@ -14,3 +11,22 @@ ORIGINS=http://localhost:5173,http://127.0.0.1:5173 USER_SERVICE_URL=http://user-service:3001/api MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/ + +# if using cloud MongoDB, replace with actual URI (run service separately) +MONGO_CLOUD_URI= + +# if using local MongoDB (run service with docker-compose) +## MongoDB credentials +MONGO_INITDB_ROOT_USERNAME=root +MONGO_INITDB_ROOT_PASSWORD=example + +## Mongo Express credentials +ME_CONFIG_BASICAUTH_USERNAME=admin +ME_CONFIG_BASICAUTH_PASSWORD=password + +## Do not change anything below this line +ME_CONFIG_MONGODB_ADMINUSERNAME=${MONGO_INITDB_ROOT_USERNAME} +ME_CONFIG_MONGODB_ADMINPASSWORD=${MONGO_INITDB_ROOT_PASSWORD} +ME_CONFIG_MONGODB_URL=mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@question-service-mongo:27017/ + +MONGO_LOCAL_URI=mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@question-service-mongo:27017/ diff --git a/backend/question-service/README.md b/backend/question-service/README.md index 2f0c8a702e..384013eedc 100644 --- a/backend/question-service/README.md +++ b/backend/question-service/README.md @@ -8,10 +8,14 @@ 2. To connect to your cloud MongoDB instead of your local MongoDB, set the `NODE_ENV` to `production` instead of `development`. -3. Update `MONGO_CLOUD_URI`, `MONGO_LOCAL_URI`, `FIREBASE_PROJECT_ID`, `FIREBASE_PRIVATE_KEY`, `FIREBASE_CLIENT_EMAIL`, `FIREBASE_STORAGE_BUCKET`. +3. Update `FIREBASE_PROJECT_ID`, `FIREBASE_PRIVATE_KEY`, `FIREBASE_CLIENT_EMAIL`, `FIREBASE_STORAGE_BUCKET`, `MONGO_CLOUD_URI` with the env variables obtained from following the instructions in the backend README. Then update `MONGO_INITDB_ROOT_USERNAME`, `MONGO_INITDB_ROOT_PASSWORD` to change your MongoDB credentials if necessary. + +4. You can view the MongoDB collections locally using Mongo Express. To set up Mongo Express, update `ME_CONFIG_BASICAUTH_USERNAME` and `ME_CONFIG_BASICAUTH_PASSWORD`. The username and password will be the login credentials when you access Mongo Express at http://localhost:8081. ## Running Question Service without Docker +> Make sure you have the cloud MongoDB URI in your .env file and set NODE_ENV to production already. + 1. Open Command Line/Terminal and navigate into the `question-service` directory. 2. Run the command: `npm install`. This will install all the necessary dependencies. diff --git a/backend/question-service/server.ts b/backend/question-service/server.ts index 6e2700160a..513945f03e 100644 --- a/backend/question-service/server.ts +++ b/backend/question-service/server.ts @@ -1,7 +1,7 @@ import app from "./app.ts"; import connectDB from "./config/db.ts"; -const PORT = process.env.PORT || 3000; +const PORT = process.env.SERVICE_PORT || 3000; if (process.env.NODE_ENV !== "test") { connectDB() diff --git a/backend/user-service/.env.sample b/backend/user-service/.env.sample index 3cac024c32..34c14ef94d 100644 --- a/backend/user-service/.env.sample +++ b/backend/user-service/.env.sample @@ -1,8 +1,5 @@ NODE_ENV=development -PORT=3001 - -MONGO_CLOUD_URI= -MONGO_LOCAL_URI= +SERVICE_PORT=3001 # Secret for creating JWT signature JWT_SECRET= @@ -33,4 +30,23 @@ REDIS_URI=redis://redis:6379 # Test MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/ -REDIS_URI_TEST=redis://test-redis:6379 \ No newline at end of file +REDIS_URI_TEST=redis://test-redis:6379 + +# if using cloud MongoDB, replace with actual URI (run service separately) +MONGO_CLOUD_URI= + +# if using local MongoDB (run service with docker-compose) +## MongoDB credentials +MONGO_INITDB_ROOT_USERNAME=root +MONGO_INITDB_ROOT_PASSWORD=example + +## Mongo Express credentials +ME_CONFIG_BASICAUTH_USERNAME=admin +ME_CONFIG_BASICAUTH_PASSWORD=password + +## Do not change anything below this line +ME_CONFIG_MONGODB_ADMINUSERNAME=${MONGO_INITDB_ROOT_USERNAME} +ME_CONFIG_MONGODB_ADMINPASSWORD=${MONGO_INITDB_ROOT_PASSWORD} +ME_CONFIG_MONGODB_URL=mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@user-service-mongo:27017/ + +MONGO_LOCAL_URI=mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@user-service-mongo:27017/ diff --git a/backend/user-service/README.md b/backend/user-service/README.md index 0a1cbdb384..7701cab40e 100644 --- a/backend/user-service/README.md +++ b/backend/user-service/README.md @@ -12,8 +12,6 @@ - `MONGO_CLOUD_URI` - - `MONGO_LOCAL_URI` - - `FIREBASE_PROJECT_ID` - `FIREBASE_PRIVATE_KEY` @@ -32,9 +30,13 @@ - `REDIS_URI` -4. A default admin account (`email: admin@gmail.com` and `password: Admin@123`) wil be created. If you wish to change the default credentials, update them in `.env`. Alternatively, you can also edit your credentials and user profile after you have created the default account. + You can also update `MONGO_INITDB_ROOT_USERNAME`, `MONGO_INITDB_ROOT_PASSWORD` to change your MongoDB credentials if necessary. + +4. You can view the MongoDB collections locally using Mongo Express. To set up Mongo Express, update `ME_CONFIG_BASICAUTH_USERNAME` and `ME_CONFIG_BASICAUTH_PASSWORD`. The username and password will be the login credentials when you access Mongo Express at http://localhost:8082. -5. To view the contents stored in Redis, +5. A default admin account (`email: admin@gmail.com` and `password: Admin@123`) wil be created. If you wish to change the default credentials, update them in `.env`. Alternatively, you can also edit your credentials and user profile after you have created the default account. + +6. To view the contents stored in Redis, 1. Go to [http://localhost:5540](http://localhost:5540). @@ -44,6 +46,8 @@ ## Running User Service without Docker +> Make sure you have the cloud MongoDB URI in your .env file and set NODE_ENV to production already. + 1. Open Command Line/Terminal and navigate into the `user-service` directory. 2. Run the command: `npm install`. This will install all the necessary dependencies. diff --git a/backend/user-service/server.ts b/backend/user-service/server.ts index 3bfa70c31a..78b033c119 100644 --- a/backend/user-service/server.ts +++ b/backend/user-service/server.ts @@ -7,7 +7,7 @@ import { connectRedis } from "./config/redis.ts"; dotenv.config(); -const port = process.env.PORT || 3001; +const port = process.env.SERVICE_PORT || 3001; const server = http.createServer(index); diff --git a/docker-compose-test.yml b/docker-compose-test.yml index 37302b7c30..16cff61140 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -7,7 +7,7 @@ services: # env_file: ./backend/user-service/.env environment: - NODE_ENV=test - - PORT=3001 + - SERVICE_PORT=3001 - JWT_SECRET - ADMIN_FIRST_NAME=Admin - ADMIN_LAST_NAME=User @@ -37,7 +37,7 @@ services: # env_file: ./backend/question-service/.env environment: - NODE_ENV=test - - PORT=3000 + - SERVICE_PORT=3000 - FIREBASE_PROJECT_ID - FIREBASE_PRIVATE_KEY - FIREBASE_CLIENT_EMAIL diff --git a/docker-compose.yml b/docker-compose.yml index b99ed4083b..003a03c395 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: ports: - 3001:3001 depends_on: - - mongo + - user-service-mongo - redis networks: - peerprep-network @@ -26,7 +26,7 @@ services: ports: - 3000:3000 depends_on: - - mongo + - question-service-mongo - user-service networks: - peerprep-network @@ -72,7 +72,7 @@ services: - /frontend/node_modules restart: on-failure - mongo: + question-service-mongo: image: mongo restart: always ports: @@ -80,11 +80,11 @@ services: networks: - peerprep-network volumes: - - mongo-data:/data/db + - question-service-mongo-data:/data/db env_file: - - ./backend/.env + - ./backend/question-service/.env - mongo-express: + question-service-mongo-express: image: mongo-express restart: always ports: @@ -92,8 +92,31 @@ services: networks: - peerprep-network depends_on: - - mongo - env_file: ./backend/.env + - question-service-mongo + env_file: ./backend/question-service/.env + + user-service-mongo: + image: mongo + restart: always + ports: + - 27018:27017 + networks: + - peerprep-network + volumes: + - user-service-mongo-data:/data/db + env_file: + - ./backend/user-service/.env + + user-service-mongo-express: + image: mongo-express + restart: always + ports: + - 8082:8081 + networks: + - peerprep-network + depends_on: + - user-service-mongo + env_file: ./backend/user-service/.env rabbitmq: image: rabbitmq:4.0-management @@ -142,7 +165,8 @@ services: restart: true volumes: - mongo-data: + question-service-mongo-data: + user-service-mongo-data: redis-data: redis-insight-data: From 0608776c6033d1287dc74afadecd817d74a8352c Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sun, 20 Oct 2024 21:19:07 +0800 Subject: [PATCH 69/82] Update according to comments --- backend/user-service/.env.sample | 2 +- backend/user-service/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/user-service/.env.sample b/backend/user-service/.env.sample index fc13daa4bf..d5f06fada2 100644 --- a/backend/user-service/.env.sample +++ b/backend/user-service/.env.sample @@ -29,7 +29,7 @@ USER=EMAIL_ADDRESS PASS=PASSWORD # Redis configuration -REDIS_URI=redis://redis:6379 # Uncomment if you're running the user service with docker +REDIS_URI=redis://redis:6379 # Uncomment if you're running the user service using docker compose # REDIS_URI=redis://localhost:6379 # Uncomment if you're running the user service individually without docker # Test diff --git a/backend/user-service/README.md b/backend/user-service/README.md index 6618903821..24fbddefd5 100644 --- a/backend/user-service/README.md +++ b/backend/user-service/README.md @@ -42,7 +42,7 @@ 3. Enter `host.internal.docker` as the Host. -## Running User Service without Docker +## Running User Service Individually 1. Set up and run Redis using `docker compose run --rm --name redis -p 6379:6379 redis`. From 06d4ae4b57fa042812c4b08d8c21f963d3ab4730 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sun, 20 Oct 2024 21:24:22 +0800 Subject: [PATCH 70/82] Add matching service to ci --- .github/workflows/ci.yml | 6 +++--- .../tests/webSocketHandler.spec.ts | 5 +++++ docker-compose-test.yml | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 backend/matching-service/tests/webSocketHandler.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a9028ae98..e7dceb180d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,13 +27,13 @@ jobs: - name: Linting working-directory: frontend run: npm run lint - - name: Frontend tests + - name: Test run: docker compose -f docker-compose-test.yml run --rm test-frontend backend-ci: runs-on: ubuntu-latest strategy: matrix: - service: [question-service, user-service] + service: [question-service, user-service, matching-service] steps: - name: Checkout code uses: actions/checkout@v4 @@ -47,7 +47,7 @@ jobs: - name: Linting working-directory: backend/${{ matrix.service }} run: npm run lint - - name: Backend tests + - name: Test env: FIREBASE_PROJECT_ID: ${{ secrets.FIREBASE_PROJECT_ID }} FIREBASE_PRIVATE_KEY: ${{ secrets.FIREBASE_PRIVATE_KEY }} diff --git a/backend/matching-service/tests/webSocketHandler.spec.ts b/backend/matching-service/tests/webSocketHandler.spec.ts new file mode 100644 index 0000000000..1b286ac176 --- /dev/null +++ b/backend/matching-service/tests/webSocketHandler.spec.ts @@ -0,0 +1,5 @@ +describe("Test web socket", () => { + it("Test", () => { + expect(true); + }); +}); diff --git a/docker-compose-test.yml b/docker-compose-test.yml index 37302b7c30..d0fb46f074 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -53,6 +53,24 @@ services: restart: on-failure command: ["npm", "test"] + test-matching-service: + image: peerprep/matching-service + build: ./backend/matching-service + # env_file: ./backend/matching-service/.env + environment: + - NODE_ENV=test + - PORT=3002 + - RABBITMQ_DEFAULT_USER=admin + - RABBITMQ_DEFAULT_PASS=password + - RABBITMQ_ADDR=amqp://admin:password@rabbitmq:5672 + networks: + - peerprep-network + volumes: + - ./backend/matching-service:/matching-service + - /matching-service/node_modules + restart: on-failure + command: ["npm", "test"] + test-frontend: image: peerprep/frontend build: ./frontend From 6a69919bbd8c904e4ea9eba30ee5d7d53b9052b4 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sun, 20 Oct 2024 21:42:20 +0800 Subject: [PATCH 71/82] Change collab sandbox text --- frontend/src/pages/CollabSandbox/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 969a9e89bb..e46465f10d 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -35,7 +35,7 @@ const CollabSandbox: React.FC = () => { return ( - Collaborative Sandbox + Successfully matched! Coming soon... From c16b325c392ac6ab18b43d17d4d6188b17d226da Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sun, 20 Oct 2024 23:25:01 +0800 Subject: [PATCH 73/82] Remove multiselect and use a fix set of categories --- .../src/controllers/questionController.ts | 27 +++++++++++++---- .../QuestionCategoryAutoComplete/index.tsx | 30 +++++++++---------- frontend/src/pages/Home/index.tsx | 14 +++------ 3 files changed, 40 insertions(+), 31 deletions(-) diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index fcc2115b9b..90661179ca 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -251,12 +251,27 @@ export const readCategories = async ( res: Response, ): Promise => { try { - const uniqueCats = await Question.distinct("category"); - - res.status(200).json({ - message: CATEGORIES_RETRIEVED_MESSAGE, - categories: sortAlphabetically(uniqueCats), - }); + // const uniqueCats = await Question.distinct("category"); + + // res.status(200).json({ + // message: CATEGORIES_RETRIEVED_MESSAGE, + // categories: sortAlphabetically(uniqueCats), + // }); + res + .status(200) + .json({ + message: CATEGORIES_RETRIEVED_MESSAGE, + categories: [ + "Strings", + "Algorithms", + "Data Structures", + "Bit Manipulation", + "Recursion", + "Databases", + "Arrays", + "Brainteaser", + ], + }); } catch (error) { res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); } diff --git a/frontend/src/components/QuestionCategoryAutoComplete/index.tsx b/frontend/src/components/QuestionCategoryAutoComplete/index.tsx index 49074de936..93181180b7 100644 --- a/frontend/src/components/QuestionCategoryAutoComplete/index.tsx +++ b/frontend/src/components/QuestionCategoryAutoComplete/index.tsx @@ -24,31 +24,31 @@ const QuestionCategoryAutoComplete: React.FC< return ( { - const newValue = - newCategoriesSelected[newCategoriesSelected.length - 1]; - if (typeof newValue === "string" && newValue.startsWith(`Add: "`)) { - const newCategory = newValue.slice(6, -1); - state.questionCategories.push(newCategory); - setSelectedCategories((prev) => [...prev, newCategory]); - } else { - setSelectedCategories(newCategoriesSelected); - } + // const newValue = + // newCategoriesSelected[newCategoriesSelected.length - 1]; + // if (typeof newValue === "string" && newValue.startsWith(`Add: "`)) { + // const newCategory = newValue.slice(6, -1); + // state.questionCategories.push(newCategory); + // setSelectedCategories((prev) => [...prev, newCategory]); + // } else { + // setSelectedCategories(newCategoriesSelected); + // } + setSelectedCategories(newCategoriesSelected); }} filterOptions={(options, params) => { const filtered = filter(options, params); - const { inputValue } = params; + // const { inputValue } = params; - const isExisting = options.some((option) => inputValue === option); + // const isExisting = options.some((option) => inputValue === option); - if (inputValue !== "" && !isExisting) { - filtered.push(`Add: "${inputValue}"`); - } + // if (inputValue !== "" && !isExisting) { + // filtered.push(`Add: "${inputValue}"`); + // } return filtered; }} diff --git a/frontend/src/pages/Home/index.tsx b/frontend/src/pages/Home/index.tsx index a48b0a0675..02045fadff 100644 --- a/frontend/src/pages/Home/index.tsx +++ b/frontend/src/pages/Home/index.tsx @@ -49,7 +49,7 @@ const Home: React.FC = () => { } const { findMatch, loading } = match; - const isSmallerThan1100px = useMediaQuery('(max-width:1100px)'); + const isSmallerThan1100px = useMediaQuery("(max-width:1100px)"); useEffect(() => { getQuestionCategories(dispatch); @@ -138,11 +138,9 @@ const Home: React.FC = () => { sx={{ backgroundColor: "white" }} > { - setComplexities(selectedOptions); + setComplexities(selectedOptions ? [selectedOptions] : []); }} renderInput={(params) => } renderTags={(tagValue, getTagProps) => @@ -178,11 +176,9 @@ const Home: React.FC = () => { sx={{ backgroundColor: "white" }} > { - setCategories(selectedOptions); + setCategories(selectedOptions ? [selectedOptions] : []); }} renderInput={(params) => } renderTags={(tagValue, getTagProps) => @@ -218,11 +214,9 @@ const Home: React.FC = () => { sx={{ backgroundColor: "white" }} > { - setLanguages(selectedOptions); + setLanguages(selectedOptions ? [selectedOptions] : []); }} renderInput={(params) => } renderTags={(tagValue, getTagProps) => From 4251e8247b781de6759f2f94d8600eb93ad5a8af Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sun, 20 Oct 2024 23:31:48 +0800 Subject: [PATCH 74/82] Add rabbitmq logs --- backend/matching-service/config/rabbitmq.ts | 1 + backend/matching-service/src/utils/mq_utils.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/backend/matching-service/config/rabbitmq.ts b/backend/matching-service/config/rabbitmq.ts index d87f50dcf5..db313f704b 100644 --- a/backend/matching-service/config/rabbitmq.ts +++ b/backend/matching-service/config/rabbitmq.ts @@ -32,6 +32,7 @@ export const sendRabbitMq = async ( try { const senderChannel = await mrConnection.createChannel(); senderChannel.sendToQueue(queue, Buffer.from(JSON.stringify(data))); + console.log("Sent to queue:", JSON.stringify(data)); return true; } catch (error) { console.log(error); diff --git a/backend/matching-service/src/utils/mq_utils.ts b/backend/matching-service/src/utils/mq_utils.ts index 9745df914d..2c4d3d92f8 100644 --- a/backend/matching-service/src/utils/mq_utils.ts +++ b/backend/matching-service/src/utils/mq_utils.ts @@ -34,6 +34,11 @@ export const matchUsers = (newRequest: string) => { if (isMatch(newRequestJson, pendingRequest)) { matchingRequests.delete(uid); createMatch(pendingRequest, newRequestJson); + console.log( + "Matched users:", + JSON.stringify(newRequestJson), + JSON.stringify(pendingRequest) + ); return; } } From fc2dec4ae25bfc83538c3f0de1154d5c19e39f1a Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sun, 20 Oct 2024 23:46:24 +0800 Subject: [PATCH 75/82] Update question controller and seed file --- .../src/controllers/questionController.ts | 28 +++++++------- backend/question-service/src/scripts/seed.ts | 38 ++++++------------- 2 files changed, 25 insertions(+), 41 deletions(-) diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index 90661179ca..a28fe2d75f 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -257,21 +257,19 @@ export const readCategories = async ( // message: CATEGORIES_RETRIEVED_MESSAGE, // categories: sortAlphabetically(uniqueCats), // }); - res - .status(200) - .json({ - message: CATEGORIES_RETRIEVED_MESSAGE, - categories: [ - "Strings", - "Algorithms", - "Data Structures", - "Bit Manipulation", - "Recursion", - "Databases", - "Arrays", - "Brainteaser", - ], - }); + res.status(200).json({ + message: CATEGORIES_RETRIEVED_MESSAGE, + categories: sortAlphabetically([ + "Strings", + "Algorithms", + "Data Structures", + "Bit Manipulation", + "Recursion", + "Databases", + "Arrays", + "Brainteaser", + ]), + }); } catch (error) { res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); } diff --git a/backend/question-service/src/scripts/seed.ts b/backend/question-service/src/scripts/seed.ts index d8d56dd1d2..16929ab792 100644 --- a/backend/question-service/src/scripts/seed.ts +++ b/backend/question-service/src/scripts/seed.ts @@ -11,98 +11,84 @@ export async function seedQuestions() { description: "Design an algorithm to serialize and deserialize a binary tree. There is no restriction on how your serialization/deserialization algorithm should work. You just need to ensure that a binary tree can be serialized to a string and this string can be deserialized to the original tree structure. \n\n![image](https://firebasestorage.googleapis.com/v0/b/peerprep-c3bd1.appspot.com/o/07148757-21b2-4c20-93e0-d8bef1b3560d?alt=media)", complexity: "Hard", - category: ["Tree", "Design"], + category: ["Tree"], }, { title: "Two Sum", description: "Given an array of integers `nums` and an integer `target`, return indices of the two numbers such that they add up to `target`. You may assume that each input would have **exactly one solution**, and you may not use the same element twice. You can return the answer in any order.", complexity: "Easy", - category: ["Array", "Hash Table"], - }, - { - title: "Add Two Numbers", - description: - "You are given two non-empty linked lists representing two non-negative integers. The digits are stored in **reverse order**, and each of their nodes contains a single digit. Add the two numbers and return the sum as a linked list. You may assume the two numbers do not contain any leading zero, except the number 0 itself.", - complexity: "Medium", - category: ["Linked List", "Math"], + category: ["Arrays"], }, { title: "Longest Substring Without Repeating Characters", description: "Given a string `s`, find the length of the **longest substring** without repeating characters.", complexity: "Medium", - category: ["Hash Table", "Two Pointers", "String", "Sliding Window"], + category: ["Strings"], }, { title: "Median of Two Sorted Arrays", description: "Given two sorted arrays `nums1` and `nums2` of size `m` and `n` respectively, return the median of the two sorted arrays.", complexity: "Hard", - category: ["Array", "Binary Search", "Divide and Conquer"], + category: ["Arrays"], }, { title: "Longest Palindromic Substring", description: "Given a string `s`, return the **longest palindromic substring** in `s`.", complexity: "Medium", - category: ["String", "Dynamic Programming"], + category: ["Strings", "Dynamic Programming"], }, { title: "ZigZag Conversion", description: "The string `PAYPALISHIRING` is written in a zigzag pattern on a given number of rows like this: (you may want to display this pattern in a fixed font for better legibility) P A H N A P L S I I G Y I R And then read line by line: `PAHNAPLSIIGYIR` Write the code that will take a string and make this conversion given a number of rows.", complexity: "Medium", - category: ["String"], + category: ["Strings"], }, { title: "Reverse Integer", description: "Given a signed 32-bit integer `x`, return `x` with its digits reversed. If reversing `x` causes the value to go outside the signed 32-bit integer range `[-2^31, 2^31 - 1]`, then return 0.", complexity: "Easy", - category: ["Math"], + category: ["Strings"], }, { title: "String to Integer (atoi)", description: "Implement the `myAtoi(string s)` function, which converts a string to a 32-bit signed integer (similar to C/C++'s `atoi` function).", complexity: "Medium", - category: ["Math", "String"], - }, - { - title: "Palindrome Number", - description: - "Given an integer `x`, return `true` if `x` is a palindrome integer. An integer is a palindrome when it reads the same backward as forward. For example, `121` is palindrome while `123` is not.", - complexity: "Easy", - category: ["Math"], + category: ["Strings"], }, { title: "Regular Expression Matching", description: "Given an input string `s` and a pattern `p`, implement regular expression matching with support for `'.'` and `'*'` where: - `'.'` Matches any single character.​​​​ - `'*'` Matches zero or more of the preceding element.", complexity: "Hard", - category: ["String", "Dynamic Programming", "Backtracking"], + category: ["Strings", "Dynamic Programming"], }, { title: "Container With Most Water", description: "Given `n` non-negative integers `a1, a2, ..., an`, where each represents a point at coordinate `(i, ai)`. `n` vertical lines are drawn such that the two endpoints of the line `i` is at `(i, ai)` and `(i, 0)`. Find two lines, which, together with the x-axis forms a container, such that the container contains the most water.", complexity: "Medium", - category: ["Array", "Two Pointers"], + category: ["Arrays"], }, { title: "Integer to Roman", description: "Roman numerals are represented by seven different symbols: `I`, `V`, `X`, `L`, `C`, `D` and `M`. Given an integer, convert it to a roman numeral.", complexity: "Medium", - category: ["Math", "String"], + category: ["Strings"], }, { title: "Roman to Integer", description: "Roman numerals are represented by seven different symbols: `I`, `V`, `X`, `L`, `C`, `D` and `M`. Given a roman numeral, convert it to an integer.", complexity: "Easy", - category: ["Math", "String"], + category: ["Strings"], }, ]; From f921c05730cff309eb753f872e894b38400e388a Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sun, 20 Oct 2024 23:47:35 +0800 Subject: [PATCH 76/82] Comment out test case --- .../QuestionCategoryAutoComplete.test.tsx | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/QuestionCategoryAutoComplete/QuestionCategoryAutoComplete.test.tsx b/frontend/src/components/QuestionCategoryAutoComplete/QuestionCategoryAutoComplete.test.tsx index ad1fb95732..5579d7e9eb 100644 --- a/frontend/src/components/QuestionCategoryAutoComplete/QuestionCategoryAutoComplete.test.tsx +++ b/frontend/src/components/QuestionCategoryAutoComplete/QuestionCategoryAutoComplete.test.tsx @@ -38,33 +38,33 @@ describe("Question Category Auto Complete", () => { await waitFor(() => expect(screen.getByText("DFS")).toBeInTheDocument()); }); - it("Adding a new category not from the category list", async () => { - const { rerender } = render( - - ); + // it("Adding a new category not from the category list", async () => { + // const { rerender } = render( + // + // ); - const input = screen.getByLabelText("Category"); - fireEvent.change(input, { target: { value: "New Category" } }); + // const input = screen.getByLabelText("Category"); + // fireEvent.change(input, { target: { value: "New Category" } }); - const valueAdded = 'Add: "New Category"'; - expect(await screen.findByText(valueAdded)).toBeInTheDocument(); + // const valueAdded = 'Add: "New Category"'; + // expect(await screen.findByText(valueAdded)).toBeInTheDocument(); - fireEvent.click(screen.getByText(valueAdded)); + // fireEvent.click(screen.getByText(valueAdded)); - const updatedCategories = [...selectedCategories, "New Category"]; + // const updatedCategories = [...selectedCategories, "New Category"]; - rerender( - - ); + // rerender( + // + // ); - expect(screen.getByText("New Category")).toBeInTheDocument(); - }); + // expect(screen.getByText("New Category")).toBeInTheDocument(); + // }); it("Remove a category from selected categories", async () => { render( From 9c2eb703947cdc6cb5e72972d6adbbe1a0930357 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Mon, 21 Oct 2024 00:08:36 +0800 Subject: [PATCH 77/82] Change question categories --- .../question-service/src/controllers/questionController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index a28fe2d75f..1d01443cbc 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -265,9 +265,9 @@ export const readCategories = async ( "Data Structures", "Bit Manipulation", "Recursion", - "Databases", + "Dynamic Programming", "Arrays", - "Brainteaser", + "Tree", ]), }); } catch (error) { From 315b0cd48a36f3ebda1d7319501b130c8b27f54b Mon Sep 17 00:00:00 2001 From: jolynloh Date: Mon, 21 Oct 2024 00:43:37 +0800 Subject: [PATCH 78/82] Add back navigation confirmation popup on matching pages --- frontend/package-lock.json | 46 ++++++++++++-------------- frontend/package.json | 3 +- frontend/src/contexts/MatchContext.tsx | 14 ++++++++ frontend/src/pages/Home/index.tsx | 2 +- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 006d964fc1..5833bd7fd3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,11 +18,12 @@ "@mui/material": "^6.1.0", "@uiw/react-md-editor": "^4.0.4", "axios": "^1.7.7", + "history": "^5.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", "react-material-ui-carousel": "^3.4.2", - "react-router-dom": "^6.26.2", + "react-router-dom": "^6.3.0", "react-toastify": "^10.0.5", "socket.io-client": "^4.8.0", "vite-plugin-svgr": "^4.2.0" @@ -3719,14 +3720,6 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@remix-run/router": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz", - "integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@rollup/pluginutils": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", @@ -7293,6 +7286,15 @@ "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==", "license": "MIT" }, + "node_modules/history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.6" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -11503,29 +11505,25 @@ } }, "node_modules/react-router": { - "version": "6.26.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz", - "integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", + "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.19.2" - }, - "engines": { - "node": ">=14.0.0" + "history": "^5.2.0" }, "peerDependencies": { "react": ">=16.8" } }, "node_modules/react-router-dom": { - "version": "6.26.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz", - "integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz", + "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.19.2", - "react-router": "6.26.2" - }, - "engines": { - "node": ">=14.0.0" + "history": "^5.2.0", + "react-router": "6.3.0" }, "peerDependencies": { "react": ">=16.8", diff --git a/frontend/package.json b/frontend/package.json index f6cfbc02fa..7f0aba9259 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,11 +22,12 @@ "@mui/material": "^6.1.0", "@uiw/react-md-editor": "^4.0.4", "axios": "^1.7.7", + "history": "^5.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", "react-material-ui-carousel": "^3.4.2", - "react-router-dom": "^6.26.2", + "react-router-dom": "^6.3.0", "react-toastify": "^10.0.5", "socket.io-client": "^4.8.0", "vite-plugin-svgr": "^4.2.0" diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 2554034159..1f4646fccf 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -15,6 +15,8 @@ import { import { useAuth } from "./AuthContext"; import { toast } from "react-toastify"; import useAppNavigate from "../components/UseAppNavigate"; +import { UNSAFE_NavigationContext } from "react-router-dom"; +import { Action, type History, type Transition } from "history"; type MatchUser = { id: string; @@ -109,6 +111,8 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const [matchPending, setMatchPending] = useState(false); const [loading, setLoading] = useState(true); + const navigator = useContext(UNSAFE_NavigationContext).navigator as History; + useEffect(() => { if (user) { setMatchUser({ @@ -135,6 +139,16 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { openSocketConnection(); matchSocket.emit(MatchEvents.USER_CONNECTED, matchUser?.id); + const unblock = navigator.block((transition: Transition) => { + if ( + transition.action === Action.Replace || + confirm(ABORT_MATCH_PROCESS_CONFIRMATION_MESSAGE) + ) { + unblock(); + appNavigate(transition.location.pathname); + } + }); + const handleBeforeUnload = (e: BeforeUnloadEvent) => { e.preventDefault(); e.returnValue = ABORT_MATCH_PROCESS_CONFIRMATION_MESSAGE; // for legacy support, does not actually display message diff --git a/frontend/src/pages/Home/index.tsx b/frontend/src/pages/Home/index.tsx index a48b0a0675..fb5d94108e 100644 --- a/frontend/src/pages/Home/index.tsx +++ b/frontend/src/pages/Home/index.tsx @@ -296,7 +296,7 @@ const Home: React.FC = () => { } onClick={() => { setIsQueryingQnDB(true); - getQuestionList(1, 10, "", complexities, categories, dispatch); + getQuestionList(1, 1, "", complexities, categories, dispatch); }} > {isQueryingQnDB ? : "Find my match!"} From 450ac4e1850ad1926c9b83ed955af57d39e3340a Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Mon, 21 Oct 2024 00:56:42 +0800 Subject: [PATCH 79/82] Set up queue per match criteria --- backend/matching-service/config/rabbitmq.ts | 84 ++++++++++++++++++- backend/matching-service/server.ts | 5 +- .../src/handlers/matchHandler.ts | 4 +- .../matching-service/src/utils/mq_utils.ts | 41 +++++++++ 4 files changed, 129 insertions(+), 5 deletions(-) diff --git a/backend/matching-service/config/rabbitmq.ts b/backend/matching-service/config/rabbitmq.ts index d87f50dcf5..3c3eed185e 100644 --- a/backend/matching-service/config/rabbitmq.ts +++ b/backend/matching-service/config/rabbitmq.ts @@ -1,6 +1,6 @@ import amqplib, { Connection } from "amqplib"; import dotenv from "dotenv"; -import { matchUsers } from "../src/utils/mq_utils"; +import { matchUsers, matchUsersInQueue } from "../src/utils/mq_utils"; import { MatchRequestItem } from "../src/handlers/matchHandler"; dotenv.config(); @@ -8,6 +8,88 @@ dotenv.config(); let mrConnection: Connection; const queue = "match_requests"; +let mrConnectionNew: Connection; +const queues: string[] = []; + +enum Complexities { + EASY = "Easy", + MEDIUM = "Medium", + HARD = "Hard", +} + +enum Categories { + STRINGS = "Strings", + ALGORITHMS = "Algorithms", + DATA_STRUCTURES = "Data Structures", + BIT_MANIPULATION = "Bit Manipulation", + RECURSION = "Recursion", + DYNAMIC_PROGRAMMING = "Dynamic Programming", + ARRAYS = "Arrays", + TREE = "Tree", +} + +enum LANGUAGES { + PYTHON = "Python", + JAVA = "Java", + C = "C", +} + +export const pendingRequestsPerQueue = new Map< + string, + Map +>(); + +const initQueueNames = () => { + for (const complexity of Object.values(Complexities)) { + for (const category of Object.values(Categories)) { + for (const language of Object.values(LANGUAGES)) { + queues.push(`${complexity}_${category}_${language}`); + } + } + } +}; + +export const connectToRabbitMq = async () => { + try { + initQueueNames(); + mrConnectionNew = await amqplib.connect(`${process.env.RABBITMQ_ADDR}`); + for (const queue of queues) { + await setUpQueue(queue); + pendingRequestsPerQueue.set(queue, new Map()); + } + } catch (error) { + console.error(error); + process.exit(1); + } +}; + +const setUpQueue = async (queueName: string) => { + const consumerChannel = await mrConnectionNew.createChannel(); + await consumerChannel.assertQueue(queueName); + + consumerChannel.consume(queueName, (msg) => { + console.log(`consume from queue: ${queueName}`); + if (msg !== null) { + matchUsersInQueue(queueName, msg.content.toString()); + consumerChannel.ack(msg); + } + }); +}; + +export const sendToQueue = async (data: MatchRequestItem) => { + try { + const queueName = `${data.complexities[0]}_${data.categories[0]}_${data.languages[0]}`; + const senderChannel = await mrConnectionNew.createChannel(); + senderChannel.sendToQueue(queueName, Buffer.from(JSON.stringify(data))); + return true; + } catch (error) { + console.log(error); + return false; + } +}; + +// ---------------- + export const connectRabbitMq = async () => { try { mrConnection = await amqplib.connect(`${process.env.RABBITMQ_ADDR}`); diff --git a/backend/matching-service/server.ts b/backend/matching-service/server.ts index 7bc95a8f00..3c0862dae3 100644 --- a/backend/matching-service/server.ts +++ b/backend/matching-service/server.ts @@ -2,7 +2,7 @@ import http from "http"; import app, { allowedOrigins } from "./app.ts"; import { handleWebsocketMatchEvents } from "./src/handlers/websocketHandler.ts"; import { Server } from "socket.io"; -import { connectRabbitMq } from "./config/rabbitmq.ts"; +import { connectRabbitMq, connectToRabbitMq } from "./config/rabbitmq.ts"; const server = http.createServer(app); export const io = new Server(server, { @@ -20,7 +20,8 @@ io.on("connection", (socket) => { const PORT = process.env.PORT || 3002; if (process.env.NODE_ENV !== "test") { - connectRabbitMq() + connectToRabbitMq() + // connectRabbitMq() .then(() => { console.log("RabbitMq connected!"); diff --git a/backend/matching-service/src/handlers/matchHandler.ts b/backend/matching-service/src/handlers/matchHandler.ts index 45bbbd1e54..d867814d82 100644 --- a/backend/matching-service/src/handlers/matchHandler.ts +++ b/backend/matching-service/src/handlers/matchHandler.ts @@ -1,5 +1,5 @@ import { v4 as uuidv4 } from "uuid"; -import { sendRabbitMq } from "../../config/rabbitmq"; +import { sendToQueue } from "../../config/rabbitmq"; import { sendMatchFound } from "./websocketHandler"; interface Match { @@ -53,7 +53,7 @@ export const sendMatchRequest = async ( rejectedPartnerId: rejectedPartnerId, }; - const sent = await sendRabbitMq(matchItem); + const sent = await sendToQueue(matchItem); return sent; }; diff --git a/backend/matching-service/src/utils/mq_utils.ts b/backend/matching-service/src/utils/mq_utils.ts index 9745df914d..a4cd6b4fce 100644 --- a/backend/matching-service/src/utils/mq_utils.ts +++ b/backend/matching-service/src/utils/mq_utils.ts @@ -1,6 +1,47 @@ +import { pendingRequestsPerQueue } from "../../config/rabbitmq"; import { createMatch, MatchRequestItem } from "../handlers/matchHandler"; import { isActiveRequest, isUserConnected } from "../handlers/websocketHandler"; +export const matchUsersInQueue = (queueName: string, newRequest: string) => { + const pendingRequests = pendingRequestsPerQueue.get(queueName)!; + const newRequestJson = JSON.parse(newRequest) as MatchRequestItem; + const newRequestUid = newRequestJson.user.id; + + for (const [uid, pendingRequest] of pendingRequests) { + if ( + isExpired(pendingRequest) || + !isUserConnected(uid) || + !isActiveRequest(uid, pendingRequest.id) || + uid === newRequestUid + ) { + pendingRequests.delete(uid); + continue; + } + + if ( + isExpired(newRequestJson) || + !isUserConnected(newRequestUid) || + !isActiveRequest(newRequestUid, newRequestJson.id) + ) { + return; + } + + if ( + uid === newRequestJson.rejectedPartnerId || + newRequestUid === pendingRequest.rejectedPartnerId + ) { + continue; + } + + pendingRequests.delete(uid); + createMatch(pendingRequest, newRequestJson); + return; + } + pendingRequests.set(newRequestUid, newRequestJson); +}; + +// ---------------- + const matchingRequests = new Map(); export const matchUsers = (newRequest: string) => { From 46c7bd6428f9dcebe3c446f5c44ae84d4c41d931 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Mon, 21 Oct 2024 01:18:34 +0800 Subject: [PATCH 80/82] Convert to multiple match queues --- backend/matching-service/config/rabbitmq.ts | 102 +++++------------- .../src/handlers/matchHandler.ts | 13 ++- .../matching-service/src/utils/constants.ts | 22 ++++ .../matching-service/src/utils/mq_utils.ts | 61 +---------- frontend/src/utils/constants.ts | 2 +- 5 files changed, 57 insertions(+), 143 deletions(-) create mode 100644 backend/matching-service/src/utils/constants.ts diff --git a/backend/matching-service/config/rabbitmq.ts b/backend/matching-service/config/rabbitmq.ts index 3c3eed185e..14b9a5f532 100644 --- a/backend/matching-service/config/rabbitmq.ts +++ b/backend/matching-service/config/rabbitmq.ts @@ -1,122 +1,70 @@ import amqplib, { Connection } from "amqplib"; import dotenv from "dotenv"; -import { matchUsers, matchUsersInQueue } from "../src/utils/mq_utils"; +import { matchUsers } from "../src/utils/mq_utils"; import { MatchRequestItem } from "../src/handlers/matchHandler"; +import { Complexities, Categories, Languages } from "../src/utils/constants"; dotenv.config(); let mrConnection: Connection; -const queue = "match_requests"; - -let mrConnectionNew: Connection; const queues: string[] = []; - -enum Complexities { - EASY = "Easy", - MEDIUM = "Medium", - HARD = "Hard", -} - -enum Categories { - STRINGS = "Strings", - ALGORITHMS = "Algorithms", - DATA_STRUCTURES = "Data Structures", - BIT_MANIPULATION = "Bit Manipulation", - RECURSION = "Recursion", - DYNAMIC_PROGRAMMING = "Dynamic Programming", - ARRAYS = "Arrays", - TREE = "Tree", -} - -enum LANGUAGES { - PYTHON = "Python", - JAVA = "Java", - C = "C", -} - -export const pendingRequestsPerQueue = new Map< - string, - Map ->(); +const pendingQueueRequests = new Map>(); const initQueueNames = () => { for (const complexity of Object.values(Complexities)) { for (const category of Object.values(Categories)) { - for (const language of Object.values(LANGUAGES)) { + for (const language of Object.values(Languages)) { queues.push(`${complexity}_${category}_${language}`); } } } }; -export const connectToRabbitMq = async () => { - try { - initQueueNames(); - mrConnectionNew = await amqplib.connect(`${process.env.RABBITMQ_ADDR}`); - for (const queue of queues) { - await setUpQueue(queue); - pendingRequestsPerQueue.set(queue, new Map()); - } - } catch (error) { - console.error(error); - process.exit(1); - } -}; - const setUpQueue = async (queueName: string) => { - const consumerChannel = await mrConnectionNew.createChannel(); + const consumerChannel = await mrConnection.createChannel(); await consumerChannel.assertQueue(queueName); consumerChannel.consume(queueName, (msg) => { - console.log(`consume from queue: ${queueName}`); if (msg !== null) { - matchUsersInQueue(queueName, msg.content.toString()); + matchUsers(queueName, msg.content.toString()); consumerChannel.ack(msg); } }); }; -export const sendToQueue = async (data: MatchRequestItem) => { - try { - const queueName = `${data.complexities[0]}_${data.categories[0]}_${data.languages[0]}`; - const senderChannel = await mrConnectionNew.createChannel(); - senderChannel.sendToQueue(queueName, Buffer.from(JSON.stringify(data))); - return true; - } catch (error) { - console.log(error); - return false; - } -}; - -// ---------------- - -export const connectRabbitMq = async () => { +export const connectToRabbitMq = async () => { try { + initQueueNames(); mrConnection = await amqplib.connect(`${process.env.RABBITMQ_ADDR}`); - const consumerChannel = await mrConnection.createChannel(); - await consumerChannel.assertQueue(queue); - - consumerChannel.consume(queue, (msg) => { - if (msg !== null) { - matchUsers(msg.content.toString()); - consumerChannel.ack(msg); - } - }); + for (const queue of queues) { + await setUpQueue(queue); + pendingQueueRequests.set(queue, new Map()); + } } catch (error) { console.error(error); process.exit(1); } }; -export const sendRabbitMq = async ( +export const sendToQueue = async ( + complexity: string, + category: string, + language: string, data: MatchRequestItem -): Promise => { +) => { try { + const queueName = `${complexity}_${category}_${language}`; const senderChannel = await mrConnection.createChannel(); - senderChannel.sendToQueue(queue, Buffer.from(JSON.stringify(data))); + senderChannel.sendToQueue(queueName, Buffer.from(JSON.stringify(data))); return true; } catch (error) { console.log(error); return false; } }; + +export const getPendingRequests = ( + queueName: string +): Map => { + return pendingQueueRequests.get(queueName)!; +}; diff --git a/backend/matching-service/src/handlers/matchHandler.ts b/backend/matching-service/src/handlers/matchHandler.ts index d867814d82..a7bbbbab62 100644 --- a/backend/matching-service/src/handlers/matchHandler.ts +++ b/backend/matching-service/src/handlers/matchHandler.ts @@ -25,9 +25,6 @@ export interface MatchRequest { export interface MatchRequestItem { id: string; user: MatchUser; - complexities: string[]; - categories: string[]; - languages: string[]; sentTimestamp: number; ttlInSecs: number; rejectedPartnerId?: string; @@ -45,15 +42,17 @@ export const sendMatchRequest = async ( const matchItem: MatchRequestItem = { id: requestId, user: user, - complexities: complexities, - categories: categories, - languages: languages, sentTimestamp: Date.now(), ttlInSecs: timeout, rejectedPartnerId: rejectedPartnerId, }; - const sent = await sendToQueue(matchItem); + const sent = await sendToQueue( + complexities[0], + categories[0], + languages[0], + matchItem + ); return sent; }; diff --git a/backend/matching-service/src/utils/constants.ts b/backend/matching-service/src/utils/constants.ts new file mode 100644 index 0000000000..77806e74ae --- /dev/null +++ b/backend/matching-service/src/utils/constants.ts @@ -0,0 +1,22 @@ +export enum Complexities { + EASY = "Easy", + MEDIUM = "Medium", + HARD = "Hard", +} + +export enum Categories { + STRINGS = "Strings", + ALGORITHMS = "Algorithms", + DATA_STRUCTURES = "Data Structures", + BIT_MANIPULATION = "Bit Manipulation", + RECURSION = "Recursion", + DYNAMIC_PROGRAMMING = "Dynamic Programming", + ARRAYS = "Arrays", + TREE = "Tree", +} + +export enum Languages { + PYTHON = "Python", + JAVA = "Java", + C = "C", +} diff --git a/backend/matching-service/src/utils/mq_utils.ts b/backend/matching-service/src/utils/mq_utils.ts index a4cd6b4fce..f1082a935e 100644 --- a/backend/matching-service/src/utils/mq_utils.ts +++ b/backend/matching-service/src/utils/mq_utils.ts @@ -1,9 +1,9 @@ -import { pendingRequestsPerQueue } from "../../config/rabbitmq"; +import { getPendingRequests } from "../../config/rabbitmq"; import { createMatch, MatchRequestItem } from "../handlers/matchHandler"; import { isActiveRequest, isUserConnected } from "../handlers/websocketHandler"; -export const matchUsersInQueue = (queueName: string, newRequest: string) => { - const pendingRequests = pendingRequestsPerQueue.get(queueName)!; +export const matchUsers = (queueName: string, newRequest: string) => { + const pendingRequests = getPendingRequests(queueName); const newRequestJson = JSON.parse(newRequest) as MatchRequestItem; const newRequestUid = newRequestJson.user.id; @@ -40,61 +40,6 @@ export const matchUsersInQueue = (queueName: string, newRequest: string) => { pendingRequests.set(newRequestUid, newRequestJson); }; -// ---------------- - -const matchingRequests = new Map(); - -export const matchUsers = (newRequest: string) => { - const newRequestJson = JSON.parse(newRequest) as MatchRequestItem; - const newRequestUid = newRequestJson.user.id; - for (const [uid, pendingRequest] of matchingRequests) { - if ( - isExpired(pendingRequest) || - !isUserConnected(uid) || - !isActiveRequest(uid, pendingRequest.id) || - uid === newRequestUid - ) { - matchingRequests.delete(uid); - continue; - } - if ( - isExpired(newRequestJson) || - !isUserConnected(newRequestUid) || - !isActiveRequest(newRequestUid, newRequestJson.id) - ) { - return; - } - - if ( - uid === newRequestJson.rejectedPartnerId || - newRequestUid === pendingRequest.rejectedPartnerId - ) { - continue; - } - - if (isMatch(newRequestJson, pendingRequest)) { - matchingRequests.delete(uid); - createMatch(pendingRequest, newRequestJson); - return; - } - } - matchingRequests.set(newRequestUid, newRequestJson); -}; - const isExpired = (data: MatchRequestItem): boolean => { return Date.now() - data.sentTimestamp >= data.ttlInSecs * 1000; }; - -const isMatch = (req1: MatchRequestItem, req2: MatchRequestItem): boolean => { - const hasCommonComplexity = req1.complexities.some((elem) => - req2.complexities.includes(elem) - ); - const hasCommonCategory = req1.categories.some((elem) => - req2.categories.includes(elem) - ); - const hasCommonLanguage = req1.languages.some((elem) => - req2.languages.includes(elem) - ); - - return hasCommonComplexity && hasCommonCategory && hasCommonLanguage; -}; diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index b647346b11..dad0b7a81e 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -1,6 +1,6 @@ /* Dropdowns */ export const complexityList: string[] = ["Easy", "Medium", "Hard"]; -export const languageList = ["Python", "Java"]; +export const languageList = ["Python", "Java", "C"]; /* Context Provider Errors */ export const USE_AUTH_ERROR_MESSAGE = From 1222d175cfbd80fd745c6758b63e40b352a3d6e7 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Mon, 21 Oct 2024 01:24:47 +0800 Subject: [PATCH 81/82] Remove unused code --- backend/matching-service/config/rabbitmq.ts | 2 +- backend/matching-service/server.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/matching-service/config/rabbitmq.ts b/backend/matching-service/config/rabbitmq.ts index 14b9a5f532..03a84d8f15 100644 --- a/backend/matching-service/config/rabbitmq.ts +++ b/backend/matching-service/config/rabbitmq.ts @@ -51,7 +51,7 @@ export const sendToQueue = async ( category: string, language: string, data: MatchRequestItem -) => { +): Promise => { try { const queueName = `${complexity}_${category}_${language}`; const senderChannel = await mrConnection.createChannel(); diff --git a/backend/matching-service/server.ts b/backend/matching-service/server.ts index 1f14ce9531..fdc1933659 100644 --- a/backend/matching-service/server.ts +++ b/backend/matching-service/server.ts @@ -2,7 +2,7 @@ import http from "http"; import app, { allowedOrigins } from "./app.ts"; import { handleWebsocketMatchEvents } from "./src/handlers/websocketHandler.ts"; import { Server } from "socket.io"; -import { connectRabbitMq, connectToRabbitMq } from "./config/rabbitmq.ts"; +import { connectToRabbitMq } from "./config/rabbitmq.ts"; const server = http.createServer(app); export const io = new Server(server, { @@ -21,7 +21,6 @@ const PORT = process.env.SERVICE_PORT || 3002; if (process.env.NODE_ENV !== "test") { connectToRabbitMq() - // connectRabbitMq() .then(() => { console.log("RabbitMq connected!"); From ba33ed6167d751aa18416b933db1aa9f951d1f09 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Mon, 21 Oct 2024 01:44:30 +0800 Subject: [PATCH 82/82] Change match criteria from multiselect to single select --- .../src/handlers/matchHandler.ts | 15 +++---- frontend/src/contexts/MatchContext.tsx | 42 +++++++++---------- frontend/src/pages/Home/index.tsx | 28 ++++++------- 3 files changed, 40 insertions(+), 45 deletions(-) diff --git a/backend/matching-service/src/handlers/matchHandler.ts b/backend/matching-service/src/handlers/matchHandler.ts index a7bbbbab62..e80b1748fb 100644 --- a/backend/matching-service/src/handlers/matchHandler.ts +++ b/backend/matching-service/src/handlers/matchHandler.ts @@ -16,9 +16,9 @@ export interface MatchUser { export interface MatchRequest { user: MatchUser; - complexities: string[]; - categories: string[]; - languages: string[]; + complexity: string; + category: string; + language: string; timeout: number; } @@ -37,7 +37,7 @@ export const sendMatchRequest = async ( requestId: string, rejectedPartnerId?: string ): Promise => { - const { user, complexities, categories, languages, timeout } = matchRequest; + const { user, complexity, category, language, timeout } = matchRequest; const matchItem: MatchRequestItem = { id: requestId, @@ -47,12 +47,7 @@ export const sendMatchRequest = async ( rejectedPartnerId: rejectedPartnerId, }; - const sent = await sendToQueue( - complexities[0], - categories[0], - languages[0], - matchItem - ); + const sent = await sendToQueue(complexity, category, language, matchItem); return sent; }; diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 2554034159..056f77463e 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -23,9 +23,9 @@ type MatchUser = { }; type MatchCriteria = { - complexities: string[]; - categories: string[]; - languages: string[]; + complexity: string; + category: string; + language: string; timeout: number; }; @@ -67,9 +67,9 @@ enum MatchPaths { type MatchContextType = { findMatch: ( - complexities: string[], - categories: string[], - languages: string[], + complexity: string, + category: string, + language: string, timeout: number ) => void; stopMatch: () => void; @@ -297,9 +297,9 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }; const findMatch = ( - complexities: string[], - categories: string[], - languages: string[], + complexity: string, + category: string, + language: string, timeout: number ) => { if (!matchUser) { @@ -318,9 +318,9 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { MatchEvents.MATCH_REQUEST, { user: matchUser, - complexities: complexities, - categories: categories, - languages: languages, + complexity: complexity, + category: category, + language: language, timeout: timeout, }, (requested: boolean) => { @@ -328,9 +328,9 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { setTimeout(() => setLoading(false), 500); if (requested) { setMatchCriteria({ - complexities, - categories, - languages, + complexity: complexity, + category: category, + language: language, timeout, }); appNavigate(MatchPaths.MATCHING); @@ -388,9 +388,9 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const rematchRequest = { user: matchUser, - complexities: matchCriteria.complexities, - categories: matchCriteria.categories, - languages: matchCriteria.languages, + complexities: matchCriteria.complexity, + categories: matchCriteria.category, + languages: matchCriteria.language, timeout: matchCriteria.timeout, }; matchSocket.emit( @@ -416,9 +416,9 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { } findMatch( - matchCriteria.complexities, - matchCriteria.categories, - matchCriteria.languages, + matchCriteria.complexity, + matchCriteria.category, + matchCriteria.language, matchCriteria.timeout ); }; diff --git a/frontend/src/pages/Home/index.tsx b/frontend/src/pages/Home/index.tsx index 02045fadff..5103fe566f 100644 --- a/frontend/src/pages/Home/index.tsx +++ b/frontend/src/pages/Home/index.tsx @@ -34,9 +34,9 @@ import Loader from "../../components/Loader"; import { toast } from "react-toastify"; const Home: React.FC = () => { - const [complexities, setComplexities] = useState([]); - const [categories, setCategories] = useState([]); - const [languages, setLanguages] = useState([]); + const [complexity, setComplexity] = useState(""); + const [category, setCategory] = useState(""); + const [language, setLanguage] = useState(""); const [timeout, setTimeout] = useState(30); const [isQueryingQnDB, setIsQueryingQnDB] = useState(false); @@ -58,7 +58,7 @@ const Home: React.FC = () => { useEffect(() => { if (isQueryingQnDB) { if (state.questions.length > 0) { - findMatch(complexities, categories, languages, timeout!); + findMatch(complexity, category, language, timeout!); } else { toast.error(QUESTION_DOES_NOT_EXIST_ERROR); } @@ -139,8 +139,8 @@ const Home: React.FC = () => { > { - setComplexities(selectedOptions ? [selectedOptions] : []); + onChange={(_, selectedOption) => { + setComplexity(selectedOption || ""); }} renderInput={(params) => } renderTags={(tagValue, getTagProps) => @@ -177,8 +177,8 @@ const Home: React.FC = () => { > { - setCategories(selectedOptions ? [selectedOptions] : []); + onChange={(_, selectedOption) => { + setCategory(selectedOption || ""); }} renderInput={(params) => } renderTags={(tagValue, getTagProps) => @@ -215,8 +215,8 @@ const Home: React.FC = () => { > { - setLanguages(selectedOptions ? [selectedOptions] : []); + onChange={(_, selectedOption) => { + setLanguage(selectedOption || ""); }} renderInput={(params) => } renderTags={(tagValue, getTagProps) => @@ -284,13 +284,13 @@ const Home: React.FC = () => { !timeout || timeout < minMatchTimeout || timeout > maxMatchTimeout || - complexities.length == 0 || - categories.length == 0 || - languages.length == 0 + !complexity || + !category || + !language } onClick={() => { setIsQueryingQnDB(true); - getQuestionList(1, 10, "", complexities, categories, dispatch); + getQuestionList(1, 10, "", [complexity], [category], dispatch); }} > {isQueryingQnDB ? : "Find my match!"}