diff --git a/.env.sample b/.env.sample index 86160f9a44..43b9bcf6b6 100644 --- a/.env.sample +++ b/.env.sample @@ -1,6 +1,3 @@ -# This is a sample environment configuration file. -# Copy this file to .env and replace the placeholder values with your own. - # Question Service QUESTION_DB_CLOUD_URI= QUESTION_DB_LOCAL_URI=mongodb://question-db:27017/question @@ -19,7 +16,22 @@ MATCH_DB_LOCAL_URI=mongodb://match-db:27017/match MATCH_DB_USERNAME=user MATCH_DB_PASSWORD=password +# Room Service +COLLAB_DB_CLOUD_URI=mongodb+srv://:@cluster0.h5ukw.mongodb.net/collaboration-service?retryWrites=true&w=majority&appName=Cluster0 +COLLAB_DB_LOCAL_URI=mongodb://collaboration-db:27017/collaboration-service + +# Collaboration Service (Yjs Documents) +YJS_DB_CLOUD_URI=mongodb+srv://:@cluster0.h5ukw.mongodb.net/yjs-documents?retryWrites=true&w=majority&appName=Cluster0 +YJS_DB_LOCAL_URI=mongodb://collaboration-db:27017/yjs-documents + +# Will use cloud MongoDB Atlas database +ENV=PROD + +# Broker +BROKER_URL=amqp://broker:5672 + # Secret for creating JWT signature JWT_SECRET=you-can-replace-this-with-your-own-secret -NODE_ENV=development +# Node environment +NODE_ENV=development \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5089e29eb6..8c1bdde0cb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: strategy: fail-fast: false matrix: - service: [frontend, services/question, services/user, services/match] + service: [frontend, services/question, services/user, services/match, services/collaboration] steps: - uses: actions/checkout@v4 - name: Use Node.js diff --git a/README.md b/README.md index c277c61c8d..7fa83d1a3f 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,23 @@ [![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/bzPrOe11) + # CS3219 Project (PeerPrep) - AY2425S1 + ## Group: G03 -### Note: -- You can choose to develop individual microservices within separate folders within this repository **OR** use individual repositories (all public) for each microservice. -- In the latter scenario, you should enable sub-modules on this GitHub classroom repository to manage the development/deployment **AND** add your mentor to the individual repositories as a collaborator. -- The teaching team should be given access to the repositories as we may require viewing the history of the repository in case of any disputes or disagreements. +### Note: + +- You can choose to develop individual microservices within separate folders within this repository **OR** use + individual repositories (all public) for each microservice. +- In the latter scenario, you should enable sub-modules on this GitHub classroom repository to manage the + development/deployment **AND** add your mentor to the individual repositories as a collaborator. +- The teaching team should be given access to the repositories as we may require viewing the history of the repository + in case of any disputes or disagreements. ## Pre-requisites 1. Install [Docker Desktop](https://www.docker.com/products/docker-desktop/) 2. Clone the GitHub repository + ``` git clone https://github.com/CS3219-AY2425S1/cs3219-ay2425s1-project-g03.git ``` @@ -19,7 +26,7 @@ git clone https://github.com/CS3219-AY2425S1/cs3219-ay2425s1-project-g03.git **Step 1: Copy Environment Configuration File** -To get started, copy the contents of `.env.sample` into a new `.env` file located at the root level of the project. +To get started, copy the contents of `.env.sample` into a new `.env` file located at the root level of the project. **Step 2: Build the Docker containers** @@ -37,25 +44,26 @@ Once the build is complete, you can start the Docker containers. docker compose -f compose.yml up -d ``` -After spinning up the services, you may access the frontend client at `127.0.0.1:4200`. Specifically, you can navigate to the Question SPA at `127.0.0.1:4200/questions` and the login page at `127.0.0.1/account`. +After spinning up the services, you may access the frontend client at `127.0.0.1:4200`. Specifically, you can navigate +to the Question SPA at `127.0.0.1:4200/questions` and the login page at `127.0.0.1/account`. -If you would like to spin up the services in development mode, you may use the following command. This enables hot reloading and exposes the ports for all microservices. +If you would like to spin up the services in development mode, you may use the following command. This enables hot +reloading and exposes the ports for all microservices. ```bash docker compose -f compose.yml -f compose.dev.yml up -d ``` -| Service | Port | -|-----------------------|------| -| Frontend | 4200 | -| API Gateway | 8080 | -| Question Service | 8081 | -| User Service | 8082 | -| Match Service | 8083 | -| Collaboration Service | 8084 | -| Chat Service | 8085 | -| History Service | 8086 | - +| Service | Port | +|------------------------------|------| +| Frontend | 4200 | +| API Gateway | 8080 | +| Question Service | 8081 | +| User Service | 8082 | +| Match Service | 8083 | +| Collaboration & Room Service | 8084 | +| Chat Service | 8085 | +| History Service | 8086 | **Step 4: Stop the Docker containers** @@ -65,4 +73,6 @@ Once you are done, stop and remove the containers using: docker compose down -v ``` -Note that this will clear any data stored in volumes associated with the containers. If you would like to keep your data, you can run the command without the `-v` flag, which will remove the containers but retain the data in the volumes for future use. \ No newline at end of file +Note that this will clear any data stored in volumes associated with the containers. If you would like to keep your +data, you can run the command without the `-v` flag, which will remove the containers but retain the data in the volumes +for future use. \ No newline at end of file diff --git a/compose.dev.yml b/compose.dev.yml index b0ab310edc..d0215809ac 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -40,6 +40,22 @@ services: ports: - 27019:27017 - match-broker: + collaboration: + command: npm run dev + ports: + - 8084:8084 + volumes: + - /app/node_modules + - ./services/collaboration:/app + + broker: ports: - 5672:5672 + + dozzle: + image: amir20/dozzle:latest + volumes: + - /var/run/docker.sock:/var/run/docker.sock + ports: + - 8000:8080 + restart: always diff --git a/compose.yml b/compose.yml index 92626e918b..a3dc85a22a 100644 --- a/compose.yml +++ b/compose.yml @@ -8,7 +8,7 @@ services: ports: - 4200:4200 restart: always - + gateway: container_name: gateway image: nginx:1.27 @@ -20,9 +20,11 @@ services: - question - user - match + - collaboration networks: - gateway-network - + restart: always + question: container_name: question image: question @@ -34,6 +36,11 @@ services: DB_LOCAL_URI: ${QUESTION_DB_LOCAL_URI} DB_USERNAME: ${QUESTION_DB_USERNAME} DB_PASSWORD: ${QUESTION_DB_PASSWORD} + BROKER_URL: ${BROKER_URL} + JWT_SECRET: ${JWT_SECRET} + depends_on: + broker: + condition: service_healthy networks: - gateway-network - question-db-network @@ -69,7 +76,7 @@ services: - gateway-network - user-db-network restart: always - + user-db: container_name: user-db image: mongo:7.0.14 @@ -82,7 +89,7 @@ services: - user-db-network command: --quiet restart: always - + match: container_name: match image: match @@ -94,8 +101,10 @@ services: DB_LOCAL_URI: ${MATCH_DB_LOCAL_URI} DB_USERNAME: ${MATCH_DB_USERNAME} DB_PASSWORD: ${MATCH_DB_PASSWORD} + JWT_SECRET: ${JWT_SECRET} + BROKER_URL: ${BROKER_URL} depends_on: - match-broker: + broker: condition: service_healthy networks: - gateway-network @@ -113,25 +122,57 @@ services: networks: - match-db-network restart: always - - match-broker: - container_name: match-broker - hostname: match-broker + + broker: + container_name: broker + hostname: broker image: rabbitmq:4.0.2 user: rabbitmq networks: - - match-db-network + - gateway-network healthcheck: test: rabbitmq-diagnostics check_port_connectivity interval: 30s timeout: 30s retries: 10 start_period: 30s + restart: always + + collaboration: + container_name: collaboration + image: collaboration + build: + context: services/collaboration + dockerfile: Dockerfile + environment: + COLLAB_DB_CLOUD_URI: ${COLLAB_DB_CLOUD_URI} + COLLAB_DB_LOCAL_URI: ${COLLAB_DB_LOCAL_URI} + YJS_DB_CLOUD_URI: ${YJS_DB_CLOUD_URI} + YJS_DB_LOCAL_URI: ${YJS_DB_LOCAL_URI} + BROKER_URL: ${BROKER_URL} + JWT_SECRET: ${JWT_SECRET} + depends_on: + broker: + condition: service_healthy + networks: + - gateway-network + - collaboration-db-network + restart: always + + collaboration-db: + container_name: collaboration-db + image: mongo:7.0.14 + volumes: + - collaboration-db:/data/db + networks: + - collaboration-db-network + restart: always volumes: question-db: user-db: match-db: + collaboration-db: networks: gateway-network: @@ -142,3 +183,5 @@ networks: driver: bridge match-db-network: driver: bridge + collaboration-db-network: + driver: bridge \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b2e734daa6..becb841779 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,12 +16,18 @@ "@angular/platform-browser": "^18.2.0", "@angular/platform-browser-dynamic": "^18.2.0", "@angular/router": "^18.2.0", + "@codemirror/lang-java": "^6.0.1", + "@codemirror/theme-one-dark": "^6.1.0", + "codemirror": "^6.0.1", "primeflex": "^3.3.1", "primeicons": "^7.0.0", "primeng": "^17.18.11", "rxjs": "~7.8.0", "tslib": "^2.3.0", "typeface-poppins": "^1.1.13", + "y-codemirror.next": "^0.3.5", + "y-websocket": "^2.0.4", + "yjs": "^13.6.19", "zone.js": "~0.14.10" }, "devDependencies": { @@ -39,8 +45,9 @@ "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", - "prettier": "^3.3.3", + "prettier": "3.3.3", "prettier-eslint": "^16.3.0", + "prettier-plugin-java": "^2.6.0", "typescript": "~5.5.2", "typescript-eslint": "8.2.0" } @@ -2597,6 +2604,155 @@ "node": ">=6.9.0" } }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.1.tgz", + "integrity": "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.2.tgz", + "integrity": "sha512-Fq7eWOl1Rcbrfn6jD8FPCj9Auaxdm5nIK5RYOeW7ughnd/rY5AmPg6b+CfsG39ZHdwiwe8lde3q8uR7CF5S0yQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-java": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.1.tgz", + "integrity": "sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/java": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.3.tgz", + "integrity": "sha512-kDqEU5sCP55Oabl6E7m5N+vZRoc0iWqgDVhEKifcHzPzjqCegcO4amfrYVL9PmPZpl4G0yjkpTpUO/Ui8CzO8A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.2.tgz", + "integrity": "sha512-PDFG5DjHxSEjOXk9TQYYVjZDqlZTFaDBfhQixHnQOEVDDNHUbEh/hstAjcQJaA6FQdZTD1hquXTK0rVBLADR1g==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.6", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz", + "integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==", + "license": "MIT" + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz", + "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.34.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.34.1.tgz", + "integrity": "sha512-t1zK/l9UiRqwUNPm+pdIT0qzJlzuVckbTEMVNFhfWkGiBQClstzg+78vedCvLSX0xJEZ6lwZbPpnljL7L6iwMQ==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -3782,6 +3938,41 @@ "dev": true, "license": "MIT" }, + "node_modules/@lezer/common": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.2.tgz", + "integrity": "sha512-Z+R3hN6kXbgBWAuejUNPihylAL1Z5CaFqnIe0nTX8Ej+XlIy3EGtXxn6WtLMO+os2hRkQvm2yvaGMYliUzlJaw==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/java": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.3.tgz", + "integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, "node_modules/@listr2/prompt-adapter-inquirer": { "version": "2.0.15", "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.15.tgz", @@ -5752,6 +5943,23 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/abstract-leveldown": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz", + "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "immediate": "^3.2.3", + "level-concat-iterator": "~2.0.0", + "level-supports": "~1.0.0", + "xtend": "~4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -6045,6 +6253,13 @@ "node": ">=8" } }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "license": "MIT", + "optional": true + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -6174,7 +6389,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -6363,7 +6578,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -6542,6 +6757,34 @@ "dev": true, "license": "MIT" }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -6779,6 +7022,21 @@ "node": ">=6" } }, + "node_modules/codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -7103,6 +7361,12 @@ } } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/critters": { "version": "0.0.24", "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.24.tgz", @@ -7387,6 +7651,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deferred-leveldown": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz", + "integrity": "sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==", + "license": "MIT", + "optional": true, + "dependencies": { + "abstract-leveldown": "~6.2.1", + "inherits": "^2.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -7660,6 +7938,22 @@ "iconv-lite": "^0.6.2" } }, + "node_modules/encoding-down": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/encoding-down/-/encoding-down-6.3.0.tgz", + "integrity": "sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "abstract-leveldown": "^6.2.1", + "inherits": "^2.0.3", + "level-codec": "^9.0.0", + "level-errors": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/encoding/node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -7780,7 +8074,6 @@ "version": "0.1.8", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -9397,7 +9690,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -9451,6 +9744,13 @@ "node": ">=0.10.0" } }, + "node_modules/immediate": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", + "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==", + "license": "MIT", + "optional": true + }, "node_modules/immutable": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", @@ -9511,7 +9811,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/ini": { @@ -9817,6 +10117,16 @@ "node": ">=0.10.0" } }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "license": "MIT", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -9944,6 +10254,18 @@ "dev": true, "license": "MIT" }, + "node_modules/java-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/java-parser/-/java-parser-2.3.0.tgz", + "integrity": "sha512-P6Ma4LU1w/e0Lr4SVM/0PtqCGoL2/i/KP9ZoiyLa824oBqhF0yGTgHDyZkLgp9GTzqR43wm5wabE56FF5X7cqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "chevrotain": "11.0.3", + "chevrotain-allstar": "0.3.1", + "lodash": "4.17.21" + } + }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -10589,6 +10911,161 @@ "node": ">=0.10.0" } }, + "node_modules/level": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/level/-/level-6.0.1.tgz", + "integrity": "sha512-psRSqJZCsC/irNhfHzrVZbmPYXDcEYhA5TVNwr+V92jF44rbf86hqGp8fiT702FyiArScYIlPSBTDUASCVNSpw==", + "license": "MIT", + "optional": true, + "dependencies": { + "level-js": "^5.0.0", + "level-packager": "^5.1.0", + "leveldown": "^5.4.0" + }, + "engines": { + "node": ">=8.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/level" + } + }, + "node_modules/level-codec": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-9.0.2.tgz", + "integrity": "sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-concat-iterator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz", + "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-2.0.1.tgz", + "integrity": "sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "errno": "~0.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-iterator-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz", + "integrity": "sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^3.4.0", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-js": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/level-js/-/level-js-5.0.2.tgz", + "integrity": "sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg==", + "license": "MIT", + "optional": true, + "dependencies": { + "abstract-leveldown": "~6.2.3", + "buffer": "^5.5.0", + "inherits": "^2.0.3", + "ltgt": "^2.1.2" + } + }, + "node_modules/level-packager": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/level-packager/-/level-packager-5.1.1.tgz", + "integrity": "sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "encoding-down": "^6.3.0", + "levelup": "^4.3.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-supports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz", + "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==", + "license": "MIT", + "optional": true, + "dependencies": { + "xtend": "^4.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/leveldown": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-5.6.0.tgz", + "integrity": "sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "abstract-leveldown": "~6.2.1", + "napi-macros": "~2.0.0", + "node-gyp-build": "~4.1.0" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/leveldown/node_modules/node-gyp-build": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.1.1.tgz", + "integrity": "sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==", + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/levelup": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/levelup/-/levelup-4.4.0.tgz", + "integrity": "sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "deferred-leveldown": "~5.3.0", + "level-errors": "~2.0.0", + "level-iterator-stream": "~4.0.0", + "level-supports": "~1.0.0", + "xtend": "~4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -10603,6 +11080,27 @@ "node": ">= 0.8.0" } }, + "node_modules/lib0": { + "version": "0.2.98", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.98.tgz", + "integrity": "sha512-XteTiNO0qEXqqweWx+b21p/fBnNHUA1NwAtJNJek1oPrewEZs2uiT4gWivHKr9GqCjDPAhchz0UQO8NwU3bBNA==", + "license": "MIT", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/license-webpack-plugin": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", @@ -10782,11 +11280,17 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, "license": "MIT" }, "node_modules/lodash.merge": { @@ -11130,6 +11634,13 @@ "yallist": "^3.0.2" } }, + "node_modules/ltgt": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", + "integrity": "sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==", + "license": "MIT", + "optional": true + }, "node_modules/magic-string": { "version": "0.30.11", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", @@ -11667,6 +12178,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-macros": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", + "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==", + "license": "MIT", + "optional": true + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -13511,6 +14029,34 @@ "node": ">=6.0.0" } }, + "node_modules/prettier-plugin-java": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-java/-/prettier-plugin-java-2.6.0.tgz", + "integrity": "sha512-mHZ3Ub3WAyYSUe1mMbiGH85xYV+NtzJgNsrfLNYDKvL7NfvoKBuJiEW4Xa2MFG668f9uRdj38WEuPKmRu+nv/g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "java-parser": "2.3.0", + "lodash": "4.17.21", + "prettier": "3.2.5" + } + }, + "node_modules/prettier-plugin-java/node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -13633,7 +14179,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", - "dev": true, "license": "MIT", "optional": true }, @@ -13738,7 +14283,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -14125,7 +14670,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -14918,7 +15463,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -15054,6 +15599,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -15848,7 +16399,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/utils-merge": { @@ -16491,6 +17042,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/vue-eslint-parser/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, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/watchpack": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", @@ -17119,6 +17686,112 @@ } } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y-codemirror.next": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/y-codemirror.next/-/y-codemirror.next-0.3.5.tgz", + "integrity": "sha512-VluNu3e5HfEXybnypnsGwKAj+fKLd4iAnR7JuX1Sfyydmn1jCBS5wwEL/uS04Ch2ib0DnMAOF6ZRR/8kK3wyGw==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.42" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "yjs": "^13.5.6" + } + }, + "node_modules/y-leveldb": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/y-leveldb/-/y-leveldb-0.1.2.tgz", + "integrity": "sha512-6ulEn5AXfXJYi89rXPEg2mMHAyyw8+ZfeMMdOtBbV8FJpQ1NOrcgi6DTAcXof0dap84NjHPT2+9d0rb6cFsjEg==", + "license": "MIT", + "optional": true, + "dependencies": { + "level": "^6.0.1", + "lib0": "^0.2.31" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, + "node_modules/y-protocols": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", + "integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.85" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, + "node_modules/y-websocket": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/y-websocket/-/y-websocket-2.0.4.tgz", + "integrity": "sha512-UbrkOU4GPNFFTDlJYAxAmzZhia8EPxHkngZ6qjrxgIYCN3gI2l+zzLzA9p4LQJ0IswzpioeIgmzekWe7HoBBjg==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.52", + "lodash.debounce": "^4.0.8", + "y-protocols": "^1.0.5" + }, + "bin": { + "y-websocket": "bin/server.cjs", + "y-websocket-server": "bin/server.cjs" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "optionalDependencies": { + "ws": "^6.2.1", + "y-leveldb": "^0.1.0" + }, + "peerDependencies": { + "yjs": "^13.5.6" + } + }, + "node_modules/y-websocket/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "license": "MIT", + "optional": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -17197,6 +17870,23 @@ "node": ">=8" } }, + "node_modules/yjs": { + "version": "13.6.19", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.19.tgz", + "integrity": "sha512-GNKw4mEUn5yWU2QPHRx8jppxmCm9KzbBhB4qJLUJFiiYD0g/tDVgXQ7aPkyh01YO28kbs2J/BEbWBagjuWyejw==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.86" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index f5f5952366..5d310e53ca 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,12 +20,18 @@ "@angular/platform-browser": "^18.2.0", "@angular/platform-browser-dynamic": "^18.2.0", "@angular/router": "^18.2.0", + "@codemirror/lang-java": "^6.0.1", + "@codemirror/theme-one-dark": "^6.1.0", + "codemirror": "^6.0.1", "primeflex": "^3.3.1", "primeicons": "^7.0.0", "primeng": "^17.18.11", "rxjs": "~7.8.0", "tslib": "^2.3.0", "typeface-poppins": "^1.1.13", + "y-codemirror.next": "^0.3.5", + "y-websocket": "^2.0.4", + "yjs": "^13.6.19", "zone.js": "~0.14.10" }, "devDependencies": { @@ -43,8 +49,9 @@ "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", - "prettier": "^3.3.3", + "prettier": "3.3.3", "prettier-eslint": "^16.3.0", + "prettier-plugin-java": "^2.6.0", "typescript": "~5.5.2", "typescript-eslint": "8.2.0" } diff --git a/frontend/src/_services/authentication.service.ts b/frontend/src/_services/authentication.service.ts index 5a120d45d9..303303b39e 100644 --- a/frontend/src/_services/authentication.service.ts +++ b/frontend/src/_services/authentication.service.ts @@ -48,6 +48,8 @@ export class AuthenticationService extends ApiService { if (response.body) { const { id, username, email, accessToken, isAdmin, createdAt } = response.body.data; user = { id, username, email, accessToken, isAdmin, createdAt }; + + // console.log('JWT Token:', accessToken); } localStorage.setItem('user', JSON.stringify(user)); this.userSubject.next(user); diff --git a/frontend/src/_services/collab.guard.service.ts b/frontend/src/_services/collab.guard.service.ts new file mode 100644 index 0000000000..a4a9b0690b --- /dev/null +++ b/frontend/src/_services/collab.guard.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@angular/core'; +import { CanActivate, ActivatedRouteSnapshot, Router } from '@angular/router'; +import { Observable, of, combineLatest } from 'rxjs'; +import { catchError, map, switchMap } from 'rxjs/operators'; +import { CollabService } from './collab.service'; +import { AuthenticationService } from './authentication.service'; + +@Injectable({ + providedIn: 'root', +}) +export class CollabGuardService implements CanActivate { + constructor( + private collabService: CollabService, + private router: Router, + private authService: AuthenticationService, + ) {} + + canActivate(route: ActivatedRouteSnapshot): Observable { + const roomId$ = of(route.queryParamMap.get('roomId') || ''); + const user$ = this.authService.user$; + + return combineLatest([roomId$, user$]).pipe( + switchMap(([roomId, user]) => { + if (!roomId || !user) { + this.router.navigate(['/matching']); + return of(false); + } + + return this.collabService.getRoomDetails(roomId).pipe( + map(response => { + const isFound = response.data.users.some(roomUser => roomUser?.id === user.id); + const isOpen = response.data.room_status; + const isForfeit = response.data.users.find(roomUser => roomUser?.id === user.id)?.isForfeit; + + if (!isFound || !isOpen || isForfeit) { + this.router.navigate(['/matching']); + return false; + } + return true; + }), + catchError(() => { + this.router.navigate(['/matching']); + return of(false); + }), + ); + }), + ); + } +} diff --git a/frontend/src/_services/collab.service.ts b/frontend/src/_services/collab.service.ts new file mode 100644 index 0000000000..ec8c961cd5 --- /dev/null +++ b/frontend/src/_services/collab.service.ts @@ -0,0 +1,56 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { ApiService } from './api.service'; +import { RoomResponse, CloseRoomResponse, RoomsResponse } from '../app/collaboration/collab.model'; + +@Injectable({ + providedIn: 'root', +}) +export class CollabService extends ApiService { + protected apiPath = 'collaboration/room'; + + private httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + }), + }; + + constructor(private http: HttpClient) { + super(); + } + + /** + * Retrieves all room IDs for a given user, but only if the room is still + * active (room_status is true). One user can have multiple rooms, + * and each room is identified by a unique room_id. + */ + getRooms() { + return this.http.get(this.apiUrl + 'user/rooms'); + } + + /** + * Retrieves the details of a room by its room ID. + */ + getRoomDetails(roomId: string) { + return this.http.get(this.apiUrl + '/' + roomId); + } + + /** + * Allows a user to close a room (change room_status to false) and delete the associated Yjs document. + */ + closeRoom(roomId: string) { + return this.http.patch(this.apiUrl + '/' + roomId + '/close', {}, this.httpOptions); + } + + /** + * updates the isForfeit status of a specified user in a particular room. Each user in a room has a + * isForfeit field that tracks whether the user has left the room through forfeiting or is still active. + */ + forfeit(roomId: string) { + return this.http.patch( + this.apiUrl + '/' + roomId + '/user/isForfeit', + { isForfeit: true }, + this.httpOptions, + ); + } +} diff --git a/frontend/src/_services/match.service.ts b/frontend/src/_services/match.service.ts index 47530fa3c8..d1c1b4025d 100644 --- a/frontend/src/_services/match.service.ts +++ b/frontend/src/_services/match.service.ts @@ -34,13 +34,6 @@ export class MatchService extends ApiService { return this.http.get(this.apiUrl + '/' + id); } - /** - * Refreshes the match request, effectively resetting its validity to one minute. - */ - updateMatchRequest(id: string) { - return this.http.put(this.apiUrl + '/' + id, {}, this.httpOptions); - } - /** * Deletes the match request */ diff --git a/frontend/src/_services/question.service.ts b/frontend/src/_services/question.service.ts index 533a88eee4..e13688c788 100644 --- a/frontend/src/_services/question.service.ts +++ b/frontend/src/_services/question.service.ts @@ -51,8 +51,8 @@ export class QuestionService extends ApiService { return this.http.get(this.apiUrl, { params }); } - getQuestionByID(id: number): Observable { - return this.http.get(this.apiUrl + '/' + id); + getQuestionByID(id: number): Observable { + return this.http.get(this.apiUrl + '/' + id); } getQuestionByParam(topics: string[], difficulty: string, limit?: number): Observable { diff --git a/frontend/src/app/account/_validators/invalid-password.validator.ts b/frontend/src/app/account/_validators/invalid-password.validator.ts index dc2a686f6a..a18d7d9cb4 100644 --- a/frontend/src/app/account/_validators/invalid-password.validator.ts +++ b/frontend/src/app/account/_validators/invalid-password.validator.ts @@ -6,7 +6,8 @@ export const PASSWORD_INVALID = 'passwordInvalid'; export function invalidPasswordValidator(): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { - const weak = !PASSWORD_REGEX.test(control.value); + const password = control.value; + const weak = password && !PASSWORD_REGEX.test(password); return weak ? { [PASSWORD_INVALID]: true } : null; }; } diff --git a/frontend/src/app/account/_validators/lowercase-password.ts b/frontend/src/app/account/_validators/lowercase-password.ts new file mode 100644 index 0000000000..a515546898 --- /dev/null +++ b/frontend/src/app/account/_validators/lowercase-password.ts @@ -0,0 +1,12 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +export const LOWERCASE_PASSWORD_REGEX = /^(?=.*[a-z])/; + +export const PASSWORD_LOWERCASE = 'passwordLowercase'; + +export function lowercasePasswordValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const missingLowercase = !LOWERCASE_PASSWORD_REGEX.test(control.value); + return missingLowercase ? { [PASSWORD_LOWERCASE]: true } : null; + }; +} diff --git a/frontend/src/app/account/_validators/numeric-password.ts b/frontend/src/app/account/_validators/numeric-password.ts new file mode 100644 index 0000000000..8e4b338af9 --- /dev/null +++ b/frontend/src/app/account/_validators/numeric-password.ts @@ -0,0 +1,12 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +export const NUMERIC_PASSWORD_REGEX = /^(?=.*[0-9])/; + +export const PASSWORD_NUMERIC = 'passwordNumeric'; + +export function numericPasswordValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const missingNumeric = !NUMERIC_PASSWORD_REGEX.test(control.value); + return missingNumeric ? { [PASSWORD_NUMERIC]: true } : null; + }; +} diff --git a/frontend/src/app/account/_validators/short-password.ts b/frontend/src/app/account/_validators/short-password.ts new file mode 100644 index 0000000000..015817837d --- /dev/null +++ b/frontend/src/app/account/_validators/short-password.ts @@ -0,0 +1,12 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +export const SHORT_PASSWORD_REGEX = /^(?=.{8,})/; + +export const PASSWORD_SHORT = 'passwordShort'; + +export function shortPasswordValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const short = !SHORT_PASSWORD_REGEX.test(control.value); + return short ? { [PASSWORD_SHORT]: true } : null; + }; +} diff --git a/frontend/src/app/account/_validators/special-password.ts b/frontend/src/app/account/_validators/special-password.ts new file mode 100644 index 0000000000..e79fc6200a --- /dev/null +++ b/frontend/src/app/account/_validators/special-password.ts @@ -0,0 +1,12 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +export const SPECIAL_PASSWORD_REGEX = /^(?=.*[!"#$%&'()*+,-.:;<=>?@\\/\\[\]^_`{|}~])/; + +export const PASSWORD_SPECIAL = 'passwordSpecial'; + +export function specialPasswordValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const missingSpecial = !SPECIAL_PASSWORD_REGEX.test(control.value); + return missingSpecial ? { [PASSWORD_SPECIAL]: true } : null; + }; +} diff --git a/frontend/src/app/account/_validators/uppercase-password.ts b/frontend/src/app/account/_validators/uppercase-password.ts new file mode 100644 index 0000000000..f80a38a004 --- /dev/null +++ b/frontend/src/app/account/_validators/uppercase-password.ts @@ -0,0 +1,12 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +export const UPPERCASE_PASSWORD_REGEX = /^(?=.*[A-Z])/; + +export const PASSWORD_UPPERCASE = 'passwordUppercase'; + +export function uppercasePasswordValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const missingUppercase = !UPPERCASE_PASSWORD_REGEX.test(control.value); + return missingUppercase ? { [PASSWORD_UPPERCASE]: true } : null; + }; +} diff --git a/frontend/src/app/account/_validators/weak-password.validator.ts b/frontend/src/app/account/_validators/weak-password.validator.ts index 8b5fd0847b..712f538131 100644 --- a/frontend/src/app/account/_validators/weak-password.validator.ts +++ b/frontend/src/app/account/_validators/weak-password.validator.ts @@ -1,13 +1,21 @@ import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; - -export const STRONG_PASSWORD_REGEX = - /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})(?=.*[!"#$%&'()*+,-.:;<=>?@\\/\\[\]^_`{|}~])/; +import { LOWERCASE_PASSWORD_REGEX } from './lowercase-password'; +import { UPPERCASE_PASSWORD_REGEX } from './uppercase-password'; +import { NUMERIC_PASSWORD_REGEX } from './numeric-password'; +import { SPECIAL_PASSWORD_REGEX } from './special-password'; +import { SHORT_PASSWORD_REGEX } from './short-password'; export const PASSWORD_WEAK = 'passwordWeak'; export function weakPasswordValidator(): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { - const weak = !STRONG_PASSWORD_REGEX.test(control.value); + const weak = !( + LOWERCASE_PASSWORD_REGEX.test(control.value) && + UPPERCASE_PASSWORD_REGEX.test(control.value) && + NUMERIC_PASSWORD_REGEX.test(control.value) && + SPECIAL_PASSWORD_REGEX.test(control.value) && + SHORT_PASSWORD_REGEX.test(control.value) + ); return weak ? { [PASSWORD_WEAK]: true } : null; }; } diff --git a/frontend/src/app/account/account.component.css b/frontend/src/app/account/account.component.css index 516b236f7b..f98f095c42 100644 --- a/frontend/src/app/account/account.component.css +++ b/frontend/src/app/account/account.component.css @@ -1,3 +1,13 @@ +.layout-container { + display: flex; + flex-direction: column; + min-height: calc(100dvh - 160px); + width: 100%; + justify-content: center; + align-items: center; + padding: 1rem; +} + .container { padding: 2rem; background-color: var(--surface-section); diff --git a/frontend/src/app/account/layout.component.html b/frontend/src/app/account/layout.component.html index 0036e0a2f0..0fb48cd93b 100644 --- a/frontend/src/app/account/layout.component.html +++ b/frontend/src/app/account/layout.component.html @@ -1,4 +1,3 @@ -
-

Welcome to PeerPrep

+
diff --git a/frontend/src/app/account/layout.component.ts b/frontend/src/app/account/layout.component.ts index 2cf4d587d8..43b661ab19 100644 --- a/frontend/src/app/account/layout.component.ts +++ b/frontend/src/app/account/layout.component.ts @@ -5,6 +5,7 @@ import { Router, RouterModule } from '@angular/router'; standalone: true, imports: [RouterModule], templateUrl: './layout.component.html', + styleUrl: './account.component.css', }) export class LayoutComponent { constructor(private router: Router) {} diff --git a/frontend/src/app/account/login.component.html b/frontend/src/app/account/login.component.html index d65adba792..0c38961a7d 100644 --- a/frontend/src/app/account/login.component.html +++ b/frontend/src/app/account/login.component.html @@ -3,12 +3,18 @@

Log In

- +
+ + +
- +
+ + +
Register
- +
+ + +
@if (isUsernameInvalid) { @@ -12,42 +15,64 @@

Register

}
- +
+ + +
@if (isEmailInvalid) { The provided email is invalid }
- +
+ + +
- - -

Your password must contain:

-
    -
  • At least one lowercase
  • -
  • At least one uppercase
  • -
  • At least one numeric
  • -
  • At least one special character
  • -
  • Minimum 8 characters
  • -
-
+ [toggleMask]="true" + [feedback]="false">
- @if (isPasswordWeak) { - The provided password is too weak - } @else if (isPasswordInvalid) { + + @if (isPasswordInvalid) { The provided password contains invalid characters } + + @if (isPasswordControlDirty) { +
    + @if (isPasswordWeak) { + @for (req of passwordRequirements; track $index) { +
  • + +

    + {{ req.msg }} +

    +
  • + } + } @else if (isPasswordStrong) { +
  • + +

    Your password is strong enough!

    +
  • + } +
+ }
+
- +
+ + +
this.passwordHasNoLowercase }, + { msg: 'At least one uppercase', check: () => this.passwordHasNoUppercase }, + { msg: 'At least one numeric', check: () => this.passwordHasNoNumeric }, + { msg: 'At least one special character', check: () => this.passwordHasNoSpecial }, + { msg: 'Minimum 8 characters', check: () => this.isPasswordShort }, + ]; + + isProcessingRegistration = false; get isUsernameInvalid(): boolean { const usernameControl = this.userForm.controls['username']; @@ -70,20 +93,49 @@ export class RegisterComponent { return emailControl.dirty && emailControl.invalid; } + get passwordControl(): AbstractControl { + return this.userForm.controls['password']; + } + + get isPasswordControlDirty(): boolean { + return this.passwordControl.dirty; + } + + get passwordHasNoLowercase(): boolean { + return this.passwordControl.pristine || this.passwordControl.hasError(PASSWORD_LOWERCASE); + } + + get passwordHasNoUppercase(): boolean { + return this.passwordControl.pristine || this.passwordControl.hasError(PASSWORD_UPPERCASE); + } + + get passwordHasNoNumeric(): boolean { + return this.passwordControl.pristine || this.passwordControl.hasError(PASSWORD_NUMERIC); + } + + get passwordHasNoSpecial(): boolean { + return this.passwordControl.pristine || this.passwordControl.hasError(PASSWORD_SPECIAL); + } + + get isPasswordShort(): boolean { + return this.passwordControl.pristine || this.passwordControl.hasError(PASSWORD_SHORT); + } + get isPasswordWeak(): boolean { - const passwordControl = this.userForm.controls['password']; - return passwordControl.dirty && passwordControl.hasError(PASSWORD_WEAK); + return this.passwordControl.dirty && this.passwordControl.hasError(PASSWORD_WEAK); + } + + get isPasswordStrong(): boolean { + return this.passwordControl.dirty && !this.passwordControl.hasError(PASSWORD_WEAK); } get isPasswordInvalid(): boolean { - const passwordControl = this.userForm.controls['password']; - return passwordControl.dirty && passwordControl.hasError(PASSWORD_INVALID); + return this.passwordControl.dirty && this.passwordControl.hasError(PASSWORD_INVALID); } get hasPasswordMismatch(): boolean { - const passwordControl = this.userForm.controls['password']; const confirmPasswordControl = this.userForm.controls['confirmPassword']; - return passwordControl.valid && confirmPasswordControl.dirty && this.userForm.hasError(PASSWORD_MISMATCH); + return this.passwordControl.valid && confirmPasswordControl.dirty && this.userForm.hasError(PASSWORD_MISMATCH); } showError() { @@ -113,7 +165,11 @@ export class RegisterComponent { } else if (status === 500) { errorMessage = 'Database Server Error'; } - this.messageService.add({ severity: 'error', summary: 'Log In Error', detail: errorMessage }); + this.messageService.add({ + severity: 'error', + summary: 'Registration Error', + detail: errorMessage, + }); }, }); } else { diff --git a/frontend/src/app/api.config.ts b/frontend/src/app/api.config.ts index b390e8be93..f929801c0c 100644 --- a/frontend/src/app/api.config.ts +++ b/frontend/src/app/api.config.ts @@ -1,3 +1,7 @@ export const API_CONFIG = { baseUrl: 'http://localhost:8080/api/', }; + +export const WEBSOCKET_CONFIG = { + baseUrl: 'ws://localhost:8080/api/', +}; diff --git a/frontend/src/app/app.component.css b/frontend/src/app/app.component.css index 4df7a9e503..4ccc1a23d3 100644 --- a/frontend/src/app/app.component.css +++ b/frontend/src/app/app.component.css @@ -1,21 +1,4 @@ main { width: 100%; min-height: 100%; - display: flex; - justify-content: center; - align-items: center; - padding: 1rem; - box-sizing: inherit; - position: relative; - flex-direction: column; -} - -.content { - display: flex; - flex-direction: column; - width: auto; - margin-bottom: 1rem; - padding: 2rem; - background-color: var(--surface-section); - border-radius: 0.75rem; } \ No newline at end of file diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 7088928e9a..a20a5cb8af 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1,3 +1,4 @@ - - - +
+ + +
diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 7460d579dd..9b50f97036 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,7 +1,9 @@ import { Routes } from '@angular/router'; import { QuestionsComponent } from './questions/questions.component'; +import { CollaborationComponent } from './collaboration/collaboration.component'; import { MatchingComponent } from './matching/matching.component'; import { AuthGuardService } from '../_services/auth.guard.service'; +import { CollabGuardService } from '../_services/collab.guard.service'; const accountModule = () => import('./account/account.module').then(x => x.AccountModule); @@ -15,6 +17,11 @@ export const routes: Routes = [ component: QuestionsComponent, canActivate: [AuthGuardService], }, + { + path: 'collab', + component: CollaborationComponent, + canActivate: [AuthGuardService, CollabGuardService], + }, { path: 'matching', component: MatchingComponent, diff --git a/frontend/src/app/collaboration/collab.model.ts b/frontend/src/app/collaboration/collab.model.ts new file mode 100644 index 0000000000..72f41328db --- /dev/null +++ b/frontend/src/app/collaboration/collab.model.ts @@ -0,0 +1,40 @@ +import { Question } from '../questions/question.model'; + +export interface RoomResponse { + status: string; + data: RoomData; +} + +export interface CloseRoomResponse { + status: string; + data: string; +} + +export interface RoomsResponse { + status: string; + data: string[]; +} + +export interface CollabUser { + id: string; + username: string; + requestId: string; + isForfeit: boolean; +} + +interface RoomData { + room_id: string; + users: CollabUser[]; + question: Question; + createdAt: string; + room_status: boolean; +} + +export interface awarenessData { + user: { + userId: string; + name: string; + color: string; + colorLight: string; + }; +} diff --git a/frontend/src/app/collaboration/collaboration.component.css b/frontend/src/app/collaboration/collaboration.component.css new file mode 100644 index 0000000000..035bef9897 --- /dev/null +++ b/frontend/src/app/collaboration/collaboration.component.css @@ -0,0 +1,25 @@ +.flex-50 { + flex: 0 50%; +} + +::ng-deep .header { + font-size: 25px; + font-weight: 500; +} + +::ng-deep .background { + background-color: #121212; +} + +::ng-deep .container { + background-color: var(--surface-section); + border-radius: 0.75rem; + display: flex; + flex-direction: column; + align-items: center; + margin-top: 1rem; +} + +::ng-deep .b1 { + height: calc(100% - 80px); +} diff --git a/frontend/src/app/collaboration/collaboration.component.html b/frontend/src/app/collaboration/collaboration.component.html new file mode 100644 index 0000000000..bcfb99efb4 --- /dev/null +++ b/frontend/src/app/collaboration/collaboration.component.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/src/app/collaboration/collaboration.component.spec.ts b/frontend/src/app/collaboration/collaboration.component.spec.ts new file mode 100644 index 0000000000..d421e30bc7 --- /dev/null +++ b/frontend/src/app/collaboration/collaboration.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CollaborationComponent } from './collaboration.component'; + +describe('CollaborationComponent', () => { + let component: CollaborationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CollaborationComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CollaborationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/collaboration/collaboration.component.ts b/frontend/src/app/collaboration/collaboration.component.ts new file mode 100644 index 0000000000..f2ba127e52 --- /dev/null +++ b/frontend/src/app/collaboration/collaboration.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { QuestionBoxComponent } from './question-box/question-box.component'; +import { EditorComponent } from './editor/editor.component'; +import { SplitterModule } from 'primeng/splitter'; + +@Component({ + selector: 'app-collaboration', + standalone: true, + imports: [QuestionBoxComponent, EditorComponent, SplitterModule], + templateUrl: './collaboration.component.html', + styleUrl: './collaboration.component.css', +}) +export class CollaborationComponent {} diff --git a/frontend/src/app/collaboration/editor/editor.component.css b/frontend/src/app/collaboration/editor/editor.component.css new file mode 100644 index 0000000000..5f0d083068 --- /dev/null +++ b/frontend/src/app/collaboration/editor/editor.component.css @@ -0,0 +1,8 @@ +:host ::ng-deep .p-scrollpanel-content { + padding: 0 18px 0 0; + height: 100%; +} + +:host ::ng-deep .p-component { + width: 100%; +} \ No newline at end of file diff --git a/frontend/src/app/collaboration/editor/editor.component.html b/frontend/src/app/collaboration/editor/editor.component.html new file mode 100644 index 0000000000..278d5b49e2 --- /dev/null +++ b/frontend/src/app/collaboration/editor/editor.component.html @@ -0,0 +1,37 @@ +
+
+
+

Editor

+ +
+
+
+
+
+ +
+
+ + +
+
+
+
+
+
+ + + + diff --git a/frontend/src/app/collaboration/editor/editor.component.spec.ts b/frontend/src/app/collaboration/editor/editor.component.spec.ts new file mode 100644 index 0000000000..a3d2cdfa06 --- /dev/null +++ b/frontend/src/app/collaboration/editor/editor.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EditorComponent } from './editor.component'; + +describe('EditorComponent', () => { + let component: EditorComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EditorComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(EditorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/collaboration/editor/editor.component.ts b/frontend/src/app/collaboration/editor/editor.component.ts new file mode 100644 index 0000000000..d6a628c56a --- /dev/null +++ b/frontend/src/app/collaboration/editor/editor.component.ts @@ -0,0 +1,271 @@ +import { AfterViewInit, Component, ElementRef, ViewChild, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'; +import { oneDark } from '@codemirror/theme-one-dark'; +import { EditorState, Extension } from '@codemirror/state'; +import { basicSetup } from 'codemirror'; +import { EditorView } from 'codemirror'; +import { java } from '@codemirror/lang-java'; +import { ScrollPanelModule } from 'primeng/scrollpanel'; +import { ButtonModule } from 'primeng/button'; +import { ConfirmationService, MessageService } from 'primeng/api'; +import { ConfirmDialogModule } from 'primeng/confirmdialog'; +import { ToastModule } from 'primeng/toast'; +import * as Y from 'yjs'; +import { WebsocketProvider } from 'y-websocket'; +import { yCollab } from 'y-codemirror.next'; +import * as prettier from 'prettier'; +import * as prettierPluginEstree from 'prettier/plugins/estree'; +import { usercolors } from './user-colors'; +import { WEBSOCKET_CONFIG } from '../../api.config'; +import { AuthenticationService } from '../../../_services/authentication.service'; +import { RoomService } from '../room.service'; +// The 'prettier-plugin-java' package does not provide TypeScript declaration files. +// We are using '@ts-ignore' to bypass TypeScript's missing type declaration error. + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import prettierPluginJava from 'prettier-plugin-java'; +import { SubmitDialogComponent } from '../submit-dialog/submit-dialog.component'; +import { ForfeitDialogComponent } from '../forfeit-dialog/forfeit-dialog.component'; +import { Router } from '@angular/router'; +import { awarenessData } from '../collab.model'; + +enum WebSocketCode { + AUTH_FAILED = 4000, + ROOM_CLOSED = 4001, +} + +@Component({ + selector: 'app-editor', + standalone: true, + imports: [ + ScrollPanelModule, + ButtonModule, + ConfirmDialogModule, + ToastModule, + SubmitDialogComponent, + ForfeitDialogComponent, + ], + providers: [ConfirmationService, MessageService], + templateUrl: './editor.component.html', + styleUrl: './editor.component.css', +}) +export class EditorComponent implements AfterViewInit, OnInit, OnDestroy { + @ViewChild('editor') editor!: ElementRef; + @ViewChild(ForfeitDialogComponent) forfeitChild!: ForfeitDialogComponent; + + state!: EditorState; + view!: EditorView; + ydoc!: Y.Doc; + yeditorText = new Y.Text(''); + ysubmit = new Y.Map(); + yforfeit = new Y.Map(); + undoManager!: Y.UndoManager; + customTheme!: Extension; + wsProvider!: WebsocketProvider; + + isSubmit = false; + isInitiator = false; + isForfeitClick = false; + roomId!: string; + numUniqueUsers = 0; + + constructor( + private messageService: MessageService, + private authService: AuthenticationService, + private roomService: RoomService, + private router: Router, + private changeDetector: ChangeDetectorRef, + ) {} + + ngOnDestroy() { + // This lets the client to disconnect from the websocket on re-route to another page. + this.wsProvider.destroy(); + } + + ngOnInit() { + this.initRoomId(); + this.initConnection(); + this.getNumOfConnectedUsers(); + } + + ngAfterViewInit() { + this.setTheme(); + this.setProvider(); + this.setEditorState(); + this.setEditorView(); + this.setCursorPosition(); + } + + initConnection() { + this.ydoc = new Y.Doc(); + const websocketUrl = WEBSOCKET_CONFIG.baseUrl + 'collaboration/'; + this.wsProvider = new WebsocketProvider(websocketUrl, this.roomId, this.ydoc, { + params: { + accessToken: this.authService.userValue?.accessToken || '', + }, + }); + + this.wsProvider.ws!.onclose = (event: { code: number; reason: string }) => { + if (event.code === WebSocketCode.AUTH_FAILED || event.code === WebSocketCode.ROOM_CLOSED) { + console.error('WebSocket authorization failed:', event.reason); + this.router.navigate(['/matching']); + } + }; + + this.yeditorText = this.ydoc.getText('editorText'); + this.ysubmit = this.ydoc.getMap('submit'); + this.yforfeit = this.ydoc.getMap('forfeit'); + this.undoManager = new Y.UndoManager(this.yeditorText); + } + + getNumOfConnectedUsers() { + this.wsProvider.awareness.on('change', () => { + const data = Array.from(this.wsProvider.awareness.getStates().values()); + const uniqueIds = new Set( + data + .map(x => (x as awarenessData).user?.userId) + .filter((userId): userId is string => userId !== undefined), + ); + + this.numUniqueUsers = uniqueIds.size; + + this.changeDetector.detectChanges(); + }); + } + + showSubmitDialog() { + this.isSubmit = true; + this.isInitiator = true; + } + + initRoomId() { + this.roomService.getRoomId().subscribe(id => { + this.roomId = id!; + }); + } + + async format() { + try { + const currentCode = this.view.state.doc.toString(); + + const formattedCode = prettier.format(currentCode, { + parser: 'java', + plugins: [prettierPluginJava, prettierPluginEstree], // Add necessary plugins + }); + + this.view.dispatch({ + changes: { + from: 0, + to: this.view.state.doc.length, + insert: await formattedCode, + }, + }); + + this.view.focus(); + } catch (e) { + console.error('Error formatting code:', e); + this.messageService.add({ severity: 'error', summary: 'Formatting Error' }); + } + } + + setProvider() { + const randomIndex = Math.floor(Math.random() * usercolors.length); + + this.wsProvider.awareness.setLocalStateField('user', { + userId: this.authService.userValue?.id, + name: this.authService.userValue?.username, + color: usercolors[randomIndex].color, + colorLight: usercolors[randomIndex].light, + }); + } + + setEditorState() { + const undoManager = this.undoManager; + const myExt: Extension = [ + basicSetup, + java(), + this.customTheme, + oneDark, + yCollab(this.yeditorText, this.wsProvider.awareness, { undoManager }), + ]; + + this.state = EditorState.create({ + doc: this.yeditorText.toString(), + extensions: myExt, + }); + } + + setEditorView() { + const editorElement = this.editor.nativeElement; + const state = this.state; + this.view = new EditorView({ + state, + parent: editorElement, + }); + } + + setTheme() { + this.customTheme = EditorView.theme( + { + '&': { + backgroundColor: 'var(--surface-section)', + }, + '.cm-gutters': { + backgroundColor: 'var(--surface-section)', + }, + }, + { dark: true }, + ); + } + + setCursorPosition() { + // set new cursor position + const cursorPosition = this.state.doc.line(1).from; + + this.view.dispatch({ + selection: { + anchor: cursorPosition, + head: cursorPosition, + }, + }); + + this.view.focus(); + } + + onSubmitDialogClose(numForfeit: number) { + if (numForfeit == 0 && this.ysubmit.size > 0) { + this.messageService.add({ + severity: 'error', + summary: 'Fail', + detail: 'Submission failed: Not all participants agreed. Please try again.', + }); + } + + this.isSubmit = false; + } + + onSuccess() { + this.messageService.add({ + severity: 'success', + summary: 'Success', + detail: 'You have successfully submitted!', + }); + this.isSubmit = false; + } + + forfeit() { + this.isForfeitClick = true; + } + + onForfeitDialogClose() { + this.isForfeitClick = false; + } + + forfeitNotify() { + this.messageService.add({ + severity: 'error', + summary: 'Warning', + detail: 'Your peer has chosen to forfeit the session.', + }); + } +} diff --git a/frontend/src/app/collaboration/editor/user-colors.ts b/frontend/src/app/collaboration/editor/user-colors.ts new file mode 100644 index 0000000000..023e4ba80e --- /dev/null +++ b/frontend/src/app/collaboration/editor/user-colors.ts @@ -0,0 +1,10 @@ +export const usercolors = [ + { color: '#30bced', light: '#30bced33' }, + { color: '#6eeb83', light: '#6eeb8333' }, + { color: '#ffbc42', light: '#ffbc4233' }, + { color: '#ecd444', light: '#ecd44433' }, + { color: '#ee6352', light: '#ee635233' }, + { color: '#9ac2c9', light: '#9ac2c933' }, + { color: '#8acb88', light: '#8acb8833' }, + { color: '#1be7ff', light: '#1be7ff33' }, +]; diff --git a/frontend/src/app/collaboration/forfeit-dialog/forfeit-dialog.component.css b/frontend/src/app/collaboration/forfeit-dialog/forfeit-dialog.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/app/collaboration/forfeit-dialog/forfeit-dialog.component.html b/frontend/src/app/collaboration/forfeit-dialog/forfeit-dialog.component.html new file mode 100644 index 0000000000..612697a62e --- /dev/null +++ b/frontend/src/app/collaboration/forfeit-dialog/forfeit-dialog.component.html @@ -0,0 +1,29 @@ + + +

Forfeit

+
+
+ +

{{ message }}

+
+
+
+ @if (isForfeit) { + + } +
+ + @if (!hideButtons) { +
+ + +
+ } +
+
diff --git a/frontend/src/app/collaboration/forfeit-dialog/forfeit-dialog.component.spec.ts b/frontend/src/app/collaboration/forfeit-dialog/forfeit-dialog.component.spec.ts new file mode 100644 index 0000000000..424bc147a0 --- /dev/null +++ b/frontend/src/app/collaboration/forfeit-dialog/forfeit-dialog.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ForfeitDialogComponent } from './forfeit-dialog.component'; + +describe('ForfeitDialogComponent', () => { + let component: ForfeitDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ForfeitDialogComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ForfeitDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/collaboration/forfeit-dialog/forfeit-dialog.component.ts b/frontend/src/app/collaboration/forfeit-dialog/forfeit-dialog.component.ts new file mode 100644 index 0000000000..74bf3d014b --- /dev/null +++ b/frontend/src/app/collaboration/forfeit-dialog/forfeit-dialog.component.ts @@ -0,0 +1,82 @@ +import { Component, EventEmitter, Input, Output, OnInit } from '@angular/core'; +import { DialogModule } from 'primeng/dialog'; +import { ButtonModule } from 'primeng/button'; +import { Router } from '@angular/router'; +import { CollabService } from '../../../_services/collab.service'; +import { AuthenticationService } from '../../../_services/authentication.service'; +import { ProgressSpinnerModule } from 'primeng/progressspinner'; +import * as Y from 'yjs'; + +@Component({ + selector: 'app-forfeit-dialog', + standalone: true, + imports: [DialogModule, ButtonModule, ProgressSpinnerModule], + + templateUrl: './forfeit-dialog.component.html', + styleUrl: './forfeit-dialog.component.css', +}) +export class ForfeitDialogComponent implements OnInit { + @Input() roomId!: string; + @Input() isVisible = false; + @Input() yforfeit!: Y.Map; + + @Output() dialogClose = new EventEmitter(); + @Output() notify = new EventEmitter(); + + message!: string; + isForfeit = false; + userId!: string; + hideButtons = false; + + constructor( + private authService: AuthenticationService, + private collabService: CollabService, + private router: Router, + ) {} + ngOnInit() { + this.getUserId(); + this.setMessage(); + this.initDocListener(); + } + + getUserId() { + this.userId = this.authService.userValue?.id || ''; + } + + initDocListener() { + this.yforfeit.observe(() => { + const numForfeit = this.yforfeit.size; + const isQuitter = this.yforfeit.entries().next().value[0] == this.userId; + + if (!isQuitter && numForfeit == 1) { + this.message = 'Are you sure you want to forfeit?'; + this.notify.emit(); + } + }); + } + + setMessage() { + this.message = 'Are you sure you want to forfeit?\n\nForfeiting would result in your peer working alone.'; + } + + onForfeit() { + const userId = this.authService.userValue?.id; + if (userId) { + this.collabService.forfeit(this.roomId).subscribe({ + next: () => { + this.yforfeit.set(userId, true); + this.message = 'You have forfeited. \n\n Redirecting you to homepage...'; + this.isForfeit = true; + this.hideButtons = true; + setTimeout(() => { + this.router.navigate(['/matching']); + }, 1500); + }, + }); + } + } + + onCancel() { + this.dialogClose.emit(); + } +} diff --git a/frontend/src/app/collaboration/question-box/question-box.component.css b/frontend/src/app/collaboration/question-box/question-box.component.css new file mode 100644 index 0000000000..093f9b4ef8 --- /dev/null +++ b/frontend/src/app/collaboration/question-box/question-box.component.css @@ -0,0 +1,11 @@ +:host ::ng-deep .easy-chip .p-chip { + background-color: var(--green-700); +} + +:host ::ng-deep .medium-chip .p-chip { + background-color: var(--orange-600); +} + +:host ::ng-deep .hard-chip .p-chip { + background-color: var(--red-700); +} \ No newline at end of file diff --git a/frontend/src/app/collaboration/question-box/question-box.component.html b/frontend/src/app/collaboration/question-box/question-box.component.html new file mode 100644 index 0000000000..1fae6f816a --- /dev/null +++ b/frontend/src/app/collaboration/question-box/question-box.component.html @@ -0,0 +1,30 @@ +
+
+
+ +

{{ question.title }}

+
+ @switch (question.difficulty) { + @case (difficultyLevels.EASY) { + {{ question.difficulty }} + } + @case (difficultyLevels.MEDIUM) { + {{ question.difficulty }} + } + @case (difficultyLevels.HARD) { + {{ question.difficulty }} + } + @default { + {{ question.difficulty }} + } + } + + @for (topic of question.topics; track topic) { + {{ topic }} + } +
+

{{ question.description }}

+
+
+
+
diff --git a/frontend/src/app/collaboration/question-box/question-box.component.spec.ts b/frontend/src/app/collaboration/question-box/question-box.component.spec.ts new file mode 100644 index 0000000000..feef79be0e --- /dev/null +++ b/frontend/src/app/collaboration/question-box/question-box.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { QuestionBoxComponent } from './question-box.component'; + +describe('QuestionBoxComponent', () => { + let component: QuestionBoxComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [QuestionBoxComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(QuestionBoxComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/collaboration/question-box/question-box.component.ts b/frontend/src/app/collaboration/question-box/question-box.component.ts new file mode 100644 index 0000000000..ba1234e767 --- /dev/null +++ b/frontend/src/app/collaboration/question-box/question-box.component.ts @@ -0,0 +1,57 @@ +import { Component, OnInit } from '@angular/core'; +import { Question } from '../../questions/question.model'; +import { QuestionService } from '../../../_services/question.service'; +import { ChipModule } from 'primeng/chip'; +import { ScrollPanelModule } from 'primeng/scrollpanel'; +import { DifficultyLevels } from '../../questions/difficulty-levels.enum'; +import { MessageService } from 'primeng/api'; +import { CollabService } from '../../../_services/collab.service'; +import { RoomService } from '../room.service'; + +@Component({ + selector: 'app-question-box', + standalone: true, + imports: [ChipModule, ScrollPanelModule], + providers: [QuestionService, MessageService], + templateUrl: './question-box.component.html', + styleUrl: './question-box.component.css', +}) +export class QuestionBoxComponent implements OnInit { + question = {} as Question; + difficultyLevels = DifficultyLevels; + roomId!: string; + + constructor( + private collabService: CollabService, + private messageService: MessageService, + private roomService: RoomService, + ) {} + + ngOnInit() { + this.initRoomId(); + this.initQuestion(); + } + + initRoomId() { + this.roomService.getRoomId().subscribe(id => { + this.roomId = id!; + }); + } + + initQuestion() { + this.collabService.getRoomDetails(this.roomId).subscribe({ + next: response => { + this.question = response.data.question; + }, + error: () => { + this.question = {} as Question; + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: 'Failed to retrieve room details', + life: 3000, + }); + }, + }); + } +} diff --git a/frontend/src/app/collaboration/room.service.ts b/frontend/src/app/collaboration/room.service.ts new file mode 100644 index 0000000000..99cf800f22 --- /dev/null +++ b/frontend/src/app/collaboration/room.service.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root', +}) +export class RoomService { + constructor(private route: ActivatedRoute) {} + + getRoomId(): Observable { + return this.route.queryParams.pipe(map(params => params['roomId'] || null)); + } +} diff --git a/frontend/src/app/collaboration/submit-dialog/submit-dialog.component.css b/frontend/src/app/collaboration/submit-dialog/submit-dialog.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/app/collaboration/submit-dialog/submit-dialog.component.html b/frontend/src/app/collaboration/submit-dialog/submit-dialog.component.html new file mode 100644 index 0000000000..e42cbb1216 --- /dev/null +++ b/frontend/src/app/collaboration/submit-dialog/submit-dialog.component.html @@ -0,0 +1,26 @@ + + +

Submit

+
+
+ +

{{ message }}

+
+
+ + +
+ + @if (!isInitiator || (numForfeit !== 2 && numUniqueUsers === 1)) { + + } +
+
+
diff --git a/frontend/src/app/collaboration/submit-dialog/submit-dialog.component.spec.ts b/frontend/src/app/collaboration/submit-dialog/submit-dialog.component.spec.ts new file mode 100644 index 0000000000..69f6380123 --- /dev/null +++ b/frontend/src/app/collaboration/submit-dialog/submit-dialog.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SubmitDialogComponent } from './submit-dialog.component'; + +describe('SubmitDialogComponent', () => { + let component: SubmitDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SubmitDialogComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SubmitDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/collaboration/submit-dialog/submit-dialog.component.ts b/frontend/src/app/collaboration/submit-dialog/submit-dialog.component.ts new file mode 100644 index 0000000000..324799f1b1 --- /dev/null +++ b/frontend/src/app/collaboration/submit-dialog/submit-dialog.component.ts @@ -0,0 +1,147 @@ +import { Component, EventEmitter, Input, Output, Inject, AfterViewInit } from '@angular/core'; +import { AuthenticationService } from '../../../_services/authentication.service'; +import { DOCUMENT } from '@angular/common'; +import { DialogModule } from 'primeng/dialog'; +import { ButtonModule } from 'primeng/button'; +import { ProgressBarModule } from 'primeng/progressbar'; +import { Router } from '@angular/router'; +import { CollabService } from '../../../_services/collab.service'; +import * as Y from 'yjs'; + +@Component({ + selector: 'app-submit-dialog', + standalone: true, + imports: [DialogModule, ButtonModule, ProgressBarModule], + templateUrl: './submit-dialog.component.html', + styleUrl: './submit-dialog.component.css', +}) +export class SubmitDialogComponent implements AfterViewInit { + @Input() isVisible = false; + @Input() isInitiator = false; + @Input() roomId!: string; + @Input() numUniqueUsers!: number; + @Input() ydoc!: Y.Doc; + + @Output() dialogClose = new EventEmitter(); + @Output() successfulSubmit = new EventEmitter(); + + message!: string; + numForfeit = 0; + yshow!: Y.Map; + ysubmit!: Y.Map; + yforfeit!: Y.Map; + userId!: string; + + constructor( + @Inject(DOCUMENT) private document: Document, + private authService: AuthenticationService, + private collabService: CollabService, + private router: Router, + ) {} + + ngAfterViewInit() { + this.getUserId(); + this.initDoc(); + this.initDocListener(); + } + + initDoc() { + this.ysubmit = this.ydoc.getMap('submit'); + this.yforfeit = this.ydoc.getMap('forfeit'); + this.yshow = this.ydoc.getMap('cancel'); + } + + getUserId() { + this.userId = this.authService.userValue?.id || ''; + } + + initDocListener() { + this.ysubmit.observe(() => { + const firstEntry = this.ysubmit.entries().next().value; + + if (firstEntry && firstEntry[0] !== undefined) { + this.isInitiator = firstEntry[0] === this.userId; + } + + const counter = this.ysubmit.size; + if (this.ysubmit.size > 0) { + this.showSubmitDialog(); + this.checkVoteOutcome(counter); + } + }); + + this.yforfeit.observe(() => { + this.numForfeit = this.yforfeit.size; + }); + + this.yshow.observe(() => { + const isShow = this.yshow.get('show'); + + if (isShow) { + this.isVisible = true; + } else { + this.dialogClose.emit(this.numForfeit); + this.isVisible = false; + this.ysubmit.clear(); + } + }); + } + + onDialogShow() { + this.yshow.set('show', true); + if (this.isInitiator) { + if (this.numForfeit == 0 && this.numUniqueUsers == 2) { + this.message = "Waiting for the other user's decision..."; + this.ysubmit.set(this.userId!, true); + } else if (this.numForfeit == 0 && this.numUniqueUsers == 1) { + this.message = + 'Are you sure you want to submit?\n\n If you submit now, while your peer is disconnected, both of your submissions will be finalised.'; + } else { + this.message = 'Are you sure you want to submit?'; + } + } else { + this.message = 'Your peer has initiated a submission.\n\nDo you agree?'; + } + } + + agreeSubmit() { + const userId = this.authService.userValue?.id; + if (userId) { + this.ysubmit.set(userId, true); + } + } + + checkVoteOutcome(counter: number) { + const isConsent = counter == this.numUniqueUsers; + + if (!isConsent) { + return; + } + + this.successfulSubmit.emit(); + + if (this.isInitiator) { + this.collabService.closeRoom(this.roomId).subscribe({ + next: () => { + this.message = 'Successfully submitted. \n\n Redirecting you to homepage...'; + setTimeout(() => { + this.router.navigate(['/matching']); + }, 1500); + }, + }); + } + + setTimeout(() => { + this.router.navigate(['/matching']); + }, 1500); + } + + cancel() { + this.yshow.set('show', false); + this.ysubmit.clear(); + } + + showSubmitDialog() { + this.isVisible = true; + } +} diff --git a/frontend/src/app/matching/finding-match/finding-match.component.html b/frontend/src/app/matching/finding-match/finding-match.component.html index 574fd45191..273ac368b1 100644 --- a/frontend/src/app/matching/finding-match/finding-match.component.html +++ b/frontend/src/app/matching/finding-match/finding-match.component.html @@ -55,7 +55,10 @@

Match Found!

[outlined]="true" (click)="closeDialog()" /> } @else { -

Redirecting you to the workspace...

+
+ +

Redirecting you to the workspace...

+
}
diff --git a/frontend/src/app/matching/finding-match/finding-match.component.ts b/frontend/src/app/matching/finding-match/finding-match.component.ts index 72b423a42e..ab12a12306 100644 --- a/frontend/src/app/matching/finding-match/finding-match.component.ts +++ b/frontend/src/app/matching/finding-match/finding-match.component.ts @@ -9,6 +9,7 @@ import { catchError, Observable, of, Subscription, switchMap, takeUntil, tap, ti import { MessageService } from 'primeng/api'; import { MatchService } from '../../../_services/match.service'; import { MatchResponse, MatchStatus } from '../match.model'; +import { Router } from '@angular/router'; @Component({ selector: 'app-finding-match', @@ -23,8 +24,7 @@ export class FindingMatchComponent { @Input() isVisible = false; @Output() dialogClose = new EventEmitter(); - @Output() matchFailed = new EventEmitter(); - @Output() matchSuccess = new EventEmitter(); + @Output() matchTimeout = new EventEmitter(); protected isFindingMatch = true; protected matchTimeLeft = 0; @@ -35,18 +35,27 @@ export class FindingMatchComponent { constructor( private matchService: MatchService, private messageService: MessageService, + private router: Router, ) {} onMatchFailed() { + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: 'Something went wrong while matching. Please try again later.', + life: 3000, + }); + this.closeDialog(); + } + + onMatchTimeout() { this.stopTimer(); - this.matchFailed.emit(); + this.matchTimeout.emit(); } onMatchSuccess() { this.stopTimer(); this.isFindingMatch = false; - this.matchSuccess.emit(); - // Possible to handle routing to workspace here. } onDialogShow() { @@ -55,7 +64,7 @@ export class FindingMatchComponent { } startPolling(interval: number): Observable { - return timer(0, interval).pipe(switchMap(() => this.requestData())); + return timer(5000, interval).pipe(switchMap(() => this.requestData())); } requestData() { @@ -64,24 +73,28 @@ export class FindingMatchComponent { console.log(response); const status: MatchStatus = response.data.status || MatchStatus.PENDING; switch (status) { + case MatchStatus.MATCH_FAILED: + this.stopPolling$.next(false); + this.onMatchFailed(); + break; case MatchStatus.MATCH_FOUND: this.onMatchSuccess(); break; + case MatchStatus.COLLAB_CREATED: + this.onMatchSuccess(); + setTimeout(() => { + this.redirectToCollab(response.data.collabId!); + this.matchPoll.unsubscribe(); + }, 2000); + break; case MatchStatus.TIME_OUT: this.stopPolling$.next(false); - this.onMatchFailed(); + this.onMatchTimeout(); break; - // TODO: Add case for MatchStatus.COLLAB_CREATED } }), catchError(() => { - this.messageService.add({ - severity: 'error', - summary: 'Error', - detail: `Something went wrong while matching.`, - life: 3000, - }); - this.closeDialog(); + this.onMatchFailed(); return of(null); }), ); @@ -124,4 +137,12 @@ export class FindingMatchComponent { clearInterval(this.matchTimeInterval); } } + + redirectToCollab(collabId: string) { + this.router.navigate(['/collab'], { + queryParams: { + roomId: collabId, + }, + }); + } } diff --git a/frontend/src/app/matching/match.model.ts b/frontend/src/app/matching/match.model.ts index f18182fafc..96191c814d 100644 --- a/frontend/src/app/matching/match.model.ts +++ b/frontend/src/app/matching/match.model.ts @@ -9,6 +9,7 @@ export enum MatchStatus { PENDING = 'PENDING', TIME_OUT = 'TIME_OUT', MATCH_FOUND = 'MATCH_FOUND', + MATCH_FAILED = 'MATCH_FAILED', COLLAB_CREATED = 'COLLAB_CREATED', } diff --git a/frontend/src/app/matching/matching.component.css b/frontend/src/app/matching/matching.component.css index f24d5cbe45..31aafa0630 100644 --- a/frontend/src/app/matching/matching.component.css +++ b/frontend/src/app/matching/matching.component.css @@ -3,7 +3,7 @@ justify-content: center; align-items: center; width: 100%; - min-height: calc(100vh - 160px); + min-height: calc(100dvh - 160px); padding: 1rem; } diff --git a/frontend/src/app/matching/matching.component.html b/frontend/src/app/matching/matching.component.html index 3cbd2d22c2..5af5dc3b87 100644 --- a/frontend/src/app/matching/matching.component.html +++ b/frontend/src/app/matching/matching.component.html @@ -61,12 +61,12 @@

Matching Criteria

[userCriteria]="{ topics: topics, difficulty: difficulty }" [matchId]="matchId" [isVisible]="isProcessingMatch" - (matchFailed)="onMatchFailed()" + (matchTimeout)="onMatchTimeout()" (dialogClose)="onMatchDialogClose()" /> diff --git a/frontend/src/app/matching/matching.component.ts b/frontend/src/app/matching/matching.component.ts index 110a84898a..91879fd3e4 100644 --- a/frontend/src/app/matching/matching.component.ts +++ b/frontend/src/app/matching/matching.component.ts @@ -47,7 +47,7 @@ export class MatchingComponent implements OnInit { isLoadingTopics = true; isInitiatingMatch = false; isProcessingMatch = false; - isMatchFailed = false; + isMatchTimeout = false; matchId!: string; constructor( @@ -102,6 +102,10 @@ export class MatchingComponent implements OnInit { return this.matchForm.dirty && this.matchForm.hasError(HAS_NO_QUESTIONS); } + get matchRequest(): MatchRequest { + return { topics: this.topics, difficulty: this.difficulty }; + } + onErrorReceive(errorMessage: string) { this.messageService.add({ severity: 'error', @@ -112,9 +116,7 @@ export class MatchingComponent implements OnInit { onMatch() { this.isInitiatingMatch = true; - const matchRequest: MatchRequest = { topics: this.topics, difficulty: this.difficulty }; - console.log(matchRequest); - this.matchService.createMatchRequest(matchRequest).subscribe({ + this.matchService.createMatchRequest(this.matchRequest).subscribe({ next: response => { this.matchId = response.data._id; }, @@ -128,13 +130,14 @@ export class MatchingComponent implements OnInit { }); } - onMatchFailed() { + onMatchTimeout() { this.isProcessingMatch = false; - this.isMatchFailed = true; + this.isMatchTimeout = true; } - onRetryMatchRequest() { - this.isMatchFailed = false; + onRetryMatchRequest(matchId: string) { + this.matchId = matchId; + this.isMatchTimeout = false; this.isProcessingMatch = true; } @@ -143,7 +146,7 @@ export class MatchingComponent implements OnInit { } onRetryMatchDialogClose() { - this.isMatchFailed = false; + this.isMatchTimeout = false; } removeTopic(topic: string) { diff --git a/frontend/src/app/matching/retry-matching/retry-matching.component.ts b/frontend/src/app/matching/retry-matching/retry-matching.component.ts index da3785c959..6499a63af0 100644 --- a/frontend/src/app/matching/retry-matching/retry-matching.component.ts +++ b/frontend/src/app/matching/retry-matching/retry-matching.component.ts @@ -3,6 +3,7 @@ import { ButtonModule } from 'primeng/button'; import { DialogModule } from 'primeng/dialog'; import { MatchService } from '../../../_services/match.service'; import { MessageService } from 'primeng/api'; +import { MatchRequest } from '../match.model'; @Component({ selector: 'app-retry-matching', @@ -13,10 +14,10 @@ import { MessageService } from 'primeng/api'; }) export class RetryMatchingComponent { @Input() isVisible = false; - @Input() matchId!: string; + @Input({ required: true }) matchRequest!: MatchRequest; @Output() dialogClose = new EventEmitter(); - @Output() retryMatch = new EventEmitter(); + @Output() retryMatch = new EventEmitter(); constructor( private matchService: MatchService, @@ -28,7 +29,8 @@ export class RetryMatchingComponent { } onRetryMatch() { - this.matchService.updateMatchRequest(this.matchId).subscribe({ + this.matchService.createMatchRequest(this.matchRequest).subscribe({ + next: response => this.retryMatch.emit(response.data._id), error: () => { this.messageService.add({ severity: 'error', @@ -38,9 +40,6 @@ export class RetryMatchingComponent { }); this.closeDialog(); }, - complete: () => { - this.retryMatch.emit(); - }, }); } } diff --git a/frontend/src/app/navigation-bar/navigation-bar.component.css b/frontend/src/app/navigation-bar/navigation-bar.component.css index 89b342b3bf..0bd7a16f90 100644 --- a/frontend/src/app/navigation-bar/navigation-bar.component.css +++ b/frontend/src/app/navigation-bar/navigation-bar.component.css @@ -8,7 +8,10 @@ z-index: 100; backdrop-filter: blur(10px); justify-content: center; + align-items: center; height: 80px; + width: 100%; + overflow: hidden; } :host ::ng-deep p-menubarsub { @@ -24,6 +27,8 @@ background: transparent; border: transparent; color: white; + padding: 5px; + border-radius: 0.5rem; } a.fill-div { @@ -38,14 +43,14 @@ a.fill-div { font-family: "Poppins", sans-serif; font-weight: 100; font-style: normal; - } +} - .poppins-bold { +.poppins-bold { font-family: "Poppins", sans-serif; font-weight: 700; font-style: normal; - } +} p.logo-font-size { - font-size: 28px; + font-size: 1.5rem; } diff --git a/frontend/src/app/navigation-bar/navigation-bar.component.html b/frontend/src/app/navigation-bar/navigation-bar.component.html index 9261df7948..c4ccb399d1 100644 --- a/frontend/src/app/navigation-bar/navigation-bar.component.html +++ b/frontend/src/app/navigation-bar/navigation-bar.component.html @@ -1,7 +1,7 @@
- logo + logo

PeerPrep

@@ -14,17 +14,17 @@ icon="pi pi-user" class="nav-dropdown"> } @else { -
+
+ routerLink="account/login" /> + routerLink="/account/register" />
} diff --git a/frontend/src/app/navigation-bar/navigation-bar.component.ts b/frontend/src/app/navigation-bar/navigation-bar.component.ts index 9b3a18ee6e..ba9a58a137 100644 --- a/frontend/src/app/navigation-bar/navigation-bar.component.ts +++ b/frontend/src/app/navigation-bar/navigation-bar.component.ts @@ -1,10 +1,10 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewChild, HostListener, Renderer2 } from '@angular/core'; import { MenuItem } from 'primeng/api'; import { CommonModule, NgFor } from '@angular/common'; import { MenubarModule } from 'primeng/menubar'; import { AuthenticationService } from '../../_services/authentication.service'; import { User } from '../../_models/user.model'; -import { MenuModule } from 'primeng/menu'; +import { Menu, MenuModule } from 'primeng/menu'; import { ButtonModule } from 'primeng/button'; @Component({ @@ -15,10 +15,14 @@ import { ButtonModule } from 'primeng/button'; styleUrl: './navigation-bar.component.css', }) export class NavigationBarComponent implements OnInit { + @ViewChild('menu') menu!: Menu; items: MenuItem[] | undefined; user: User | null = null; - constructor(private authService: AuthenticationService) {} + constructor( + private authService: AuthenticationService, + private renderer: Renderer2, + ) {} ngOnInit() { this.setMenuItems(); @@ -28,6 +32,15 @@ export class NavigationBarComponent implements OnInit { }); } + @HostListener('window:scroll', []) + hideMenuOnScroll() { + if (this.menu && this.menu.overlayVisible) { + this.menu.hide(); + // This makes hiding instant + this.renderer.setStyle(this.menu.container, 'display', 'none'); + } + } + setMenuItems() { if (this.authService.isLoggedIn) { this.items = [ diff --git a/nginx/default.conf b/nginx/default.conf index 3e3730a84b..98454f5f69 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -10,6 +10,10 @@ upstream match-api { server match:8083; } +upstream collaboration-api { + server collaboration:8084; +} + server { listen 8080; server_name localhost; @@ -28,4 +32,12 @@ server { proxy_pass http://match-api/; proxy_set_header Host $host; } + + location /api/collaboration/ { + proxy_pass http://collaboration-api/; + proxy_set_header Host $host; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } } diff --git a/services/collaboration/.env.sample b/services/collaboration/.env.sample new file mode 100644 index 0000000000..2ea1cfa8a9 --- /dev/null +++ b/services/collaboration/.env.sample @@ -0,0 +1,22 @@ +# MongoDB Cloud URI for the room service. Replace the placeholders with your MongoDB username, password, and cluster details. +COLLAB_DB_CLOUD_URI=mongodb+srv://:@cluster0.h5ukw.mongodb.net/collaboration-service?retryWrites=true&w=majority&appName=Cluster0 +COLLAB_DB_LOCAL_URI=mongodb://collaboration-db:27017/collaboration-service + +# MongoDB Cloud URI for Yjs documents. Replace the placeholders with your MongoDB username, password, and cluster details. +YJS_DB_CLOUD_URI=mongodb+srv://:@cluster0.h5ukw.mongodb.net/yjs-documents?retryWrites=true&w=majority&appName=Cluster0 +YJS_DB_LOCAL_URI=mongodb://collaboration-db:27017/yjs-documents + +# Broker Service +BROKER_URL=amqp://broker:5672 + +# CORS origin configuration +CORS_ORIGIN=* + +# Port +PORT=8084 + +# Secret for creating JWT signature +JWT_SECRET=you-can-replace-this-with-your-own-secret + +# Node environment +ENV=development \ No newline at end of file diff --git a/services/collaboration/.gitignore b/services/collaboration/.gitignore new file mode 100644 index 0000000000..931232e706 --- /dev/null +++ b/services/collaboration/.gitignore @@ -0,0 +1,24 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build +/dist + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* \ No newline at end of file diff --git a/services/collaboration/Dockerfile b/services/collaboration/Dockerfile new file mode 100644 index 0000000000..07fd4a4f2a --- /dev/null +++ b/services/collaboration/Dockerfile @@ -0,0 +1,9 @@ +FROM node:20-alpine + +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm install +COPY . . +EXPOSE 8084 + +CMD ["npm", "start"] \ No newline at end of file diff --git a/services/collaboration/README.md b/services/collaboration/README.md new file mode 100644 index 0000000000..7fae2c4362 --- /dev/null +++ b/services/collaboration/README.md @@ -0,0 +1,376 @@ +# Collaboration Service User Guide + +## Pre-requisites + +1. Run the following command to create the `.env` files at the root directory: + +```cmd +cp .env.sample .env +``` + +2. After setting up the .env files, build the Docker images and start the containers using the following command: + +```cmd +docker compose build +docker compose up -d +``` + +3. To stop and remove the containers and associated volumes, use the following command: + +```cmd +docker compose down -v +``` + +--- + +## Overview + +The `Collaboration Service` manages the lifecycle of collaboration sessions, including room creation, retrieval, and +closure. When +a room is created, it is assigned to two users, a Yjs document is initialized for real-time collaboration, and the +room’s status is set to `open`. Rooms are used to group users working together on a shared task, such as collaborative +coding, and are identified by a unique `room_id`. The room’s status can be updated to `closed` when users leave +or forfeit the session, which also removes the Yjs document and its data from MongoDB to free resources. + +### Useful Links + +- [Yjs](https://github.com/yjs/yjs) to sync document states between clients. +- [y-websocket](https://github.com/yjs/y-websocket) as the WebSocket provider. +- [y-mongodb-provider](https://github.com/MaxNoetzold/y-mongodb-provider) using MongoDB to provide data persistence. + +### Key Features + +- `Real-time Collaboration`: Synchronize changes between clients in real-time using Yjs, ensuring that users always have + the latest document state. +- `Room Management`: Handle the creation, retrieval, and closure of rooms, allowing two users to work together in a + shared environment. +- `Room Status Tracking`: Rooms are automatically created with an `open` status and can be `closed` when users leave, + ensuring active collaboration sessions are properly managed. +- `WebSocket-based Communication`: Uses WebSocket connections to handle real-time synchronization of Yjs documents + between users. +- `MongoDB Persistence`: Yjs document updates and room data are persisted in MongoDB, ensuring fault tolerance and the + ability to resume sessions after interruptions. +- `Automatic Cleanup`: When a room is closed, the corresponding Yjs document is removed from MongoDB, ensuring efficient + use of resources. + +--- + +## Environment Variables + +Here are the key environment variables used in the `.env` file: + +| Variable | Description | +|--------------------------|---------------------------------------------------------------------------------------| +| `COLLAB_CLOUD_MONGO_URI` | URI for connecting to the MongoDB Atlas database for the collaboration service (room) | +| `COLLAB_LOCAL_MONGO_URI` | URI for connecting to the local MongoDB database for the collaboration service (room) | +| `YJS_CLOUD_MONGO_URI` | URI for connecting to the MongoDB Atlas database for Yjs document persistence | +| `YJS_LOCAL_MONGO_URI` | URI for connecting to the local MongoDB database for Yjs document persistence | +| `DB_USERNAME` | Username for the MongoDB databases (for both cloud and local environments) | +| `DB_PASSWORD` | Password for the MongoDB databases (for both cloud and local environments) | +| `CORS_ORIGIN` | Allowed origins for CORS (default: * to allow all origins) | +| `PORT` | Port for the Room and Collaboration Service (default: 8084) | +| `ENV` | Environment setting (`development` or `production`) | + +--- + +## Documentation on API Endpoints + +The `Collaboration Service` provides HTTP API endpoints to manage and retrieve details about rooms used in the real-time +collaboration service. It enables creating rooms, retrieving room details, and managing room statuses. + +--- + +## Get Room IDs by User (JWT Authentication) + +This endpoint retrieves all active room IDs associated with the authenticated user. Only rooms where `room_status` +is `true` will be retrieved. + +- **HTTP Method**: `GET` +- **Endpoint**: `/room/user/rooms` + +### Authorization + +This endpoint requires a valid JWT token in the `Authorization` header. The `userId` is derived from the token and is +not provided directly. + +### Responses: + +| Response Code | Explanation | +|-----------------------------|---------------------------------------------| +| 200 (OK) | Success, room IDs retrieved. | +| 404 (Not Found) | No rooms found for the user. | +| 500 (Internal Server Error) | Unexpected error in the server or database. | + +### Command Line Example: + +```bash +curl -X GET http://localhost:8080/api/collaboration/room/user/rooms \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3MjFhNWZiZWFlNjBjOGViMWU1ZWYzNCIsInVzZXJuYW1lIjoiVGVzdGluZyIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzMwMjU4NDI4LCJleHAiOjE3MzAzNDQ4Mjh9.DF9CaChoG3-UmeZgZG9SlpjtTknVzeVSBAJDJRdqGk0 +``` + +### Example of Response Body for Success: + +```json +{ + "status": "Success", + "data": [ + "6721a64b0c4d990bc0feee4c" + ] +} +``` + +--- + +## Get Room by Room ID + +This endpoint retrieves the details of a room by its room ID. + +- **HTTP Method**: `GET` +- **Endpoint**: `/room/{roomId}` + +### Authorization + +This endpoint requires a valid JWT token in the Authorization header. + +### Parameters: + +- `roomId` (Required) - The ID of the room to retrieve. + +### Responses: + +| Response Code | Explanation | +|-----------------------------|---------------------------------------------| +| 200 (OK) | Success, room details returned. | +| 404 (Not Found) | Room not found. | +| 500 (Internal Server Error) | Unexpected error in the server or database. | + +### Command Line Example: + +```bash +curl -X GET http://localhost:8080/api/collaboration/room/6721a64b0c4d990bc0feee4c \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3MjFhNWZiZWFlNjBjOGViMWU1ZWYzNCIsInVzZXJuYW1lIjoiVGVzdGluZyIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzMwMjU4NDI4LCJleHAiOjE3MzAzNDQ4Mjh9.DF9CaChoG3-UmeZgZG9SlpjtTknVzeVSBAJDJRdqGk0 +``` + +### Example of Response Body for Success: + +```json +{ + "status": "Success", + "data": { + "room_id": "6721a64b0c4d990bc0feee4c", + "users": [ + { + "id": "6718b0050e24954ac125e5dd", + "username": "Testing", + "requestId": "6718b027a8144e99bbee17ce", + "isForfeit": false + }, + { + "id": "6718b0070e24954ac125e5e1", + "username": "Testing1", + "requestId": "6718b026a8144e99bbee17c8", + "isForfeit": false + } + ], + "question_id": 2, + "createdAt": "2024-10-23T08:13:27.886Z", + "room_status": true + } +} +``` + +--- + +## Update User Forfeit Status in Room + +This endpoint updates the `isForfeit` status of a specified user in a particular room. Each user in a room has +a `isForfeit` field that tracks whether the user has left the room through forfeiting or is still active. + +- **HTTP Method**: `PATCH` +- **Endpoint**: `/room/{roomId}/user/isForfeit` + +### Authorization + +This endpoint requires a valid JWT token in the Authorization header. The userId is derived from the token. + +### Parameters: + +- `roomId` (Required) - The ID of the room to update. +- `isForfeit` (Required, Boolean) - The forfeit status of the user. + +### Responses: + +| Response Code | Explanation | +|-----------------------------|-----------------------------------------------| +| 200 (OK) | Success, user status updated successfully. | +| 404 (Not Found) | Room or user not found in the specified room. | +| 400 (Bad Request) | Invalid or missing `statusExist` parameter. | +| 500 (Internal Server Error) | Unexpected error in the server or database. | + +### Command Line Example: + +```bash +curl -X PATCH http://localhost:8080/api/collaboration/room/6721a64b0c4d990bc0feee4c/user/isForfeit \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3MjFhNWZiZWFlNjBjOGViMWU1ZWYzNCIsInVzZXJuYW1lIjoiVGVzdGluZyIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzMwMjU4NDI4LCJleHAiOjE3MzAzNDQ4Mjh9.DF9CaChoG3-UmeZgZG9SlpjtTknVzeVSBAJDJRdqGk0" \ + -H "Content-Type: application/json" \ + -d '{"isForfeit": true}' +``` + +### Example of Response Body for Success: + +```json +{ + "status": "Success", + "data": { + "message": "User status updated successfully", + "room": { + "room_id": "6721a64b0c4d990bc0feee4c", + "users": [ + { + "id": "6718b0050e24954ac125e5dd", + "username": "Testing", + "requestId": "6718b027a8144e99bbee17ce", + "isForfeit": false + }, + { + "id": "6718b0070e24954ac125e5e1", + "username": "Testing1", + "requestId": "6718b026a8144e99bbee17c8", + "isForfeit": true + } + ], + "question_id": 2, + "createdAt": "2024-10-23T08:13:27.886Z", + "room_status": true + } + } +} +``` + +--- + +## Close Room + +This endpoint allows a user to close a room (change `room_status` to `false`) and delete the associated Yjs document. + +- **HTTP Method**: `PATCH` +- **Endpoint**: `/room/{roomId}/close` + +### Authorization + +This endpoint requires a valid JWT token in the Authorization header. + +### Parameters: + +- `roomId` (Required) - The ID of the room to close. + +### Responses: + +| Response Code | Explanation | +|-----------------------------|----------------------------------------------------------------------------| +| 200 (OK) | Success, room closed and Yjs document removed, or room was already closed. | +| 404 (Not Found) | Room not found. | +| 500 (Internal Server Error) | Unexpected error in the server or database. | + +### Command Line Example: + +```bash +curl -X PATCH http://localhost:8080/api/collaboration/room/6721a64b0c4d990bc0feee4c/close \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3MjFhNWZiZWFlNjBjOGViMWU1ZWYzNCIsInVzZXJuYW1lIjoiVGVzdGluZyIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzMwMjU4NDI4LCJleHAiOjE3MzAzNDQ4Mjh9.DF9CaChoG3-UmeZgZG9SlpjtTknVzeVSBAJDJRdqGk0" +``` + +### Example of Response Body for Success: + +```json +{ + "status": "Success", + "data": "Room 6721a64b0c4d990bc0feee4c successfully closed" +} +``` + +--- + +## Documentation on Queue (RabbitMQ) + +The collaboration service uses RabbitMQ as a message broker to facilitate communication between microservices (such as +the `matching service` and `collaboration service`) in an asynchronous manner. The system consists of a consumer and two +producers: + +### Queues Used + +- `QUESTION_FOUND`: Handles messages related to matching users and creating collaboration rooms. +- `COLLAB_CREATED`: Sends messages indicating that a collaboration room has been successfully created. +- `MATCH_FAILED`: Sends messages indicating that a collaboration room could not be created. + +--- + +## Producer + +The producer will send a message to the `COLLAB_CREATED` queue when a collaboration room is successfully created. + +- **Queue**: `COLLAB_CREATED` +- **Data in the Message**: + - `requestId1` (Required) - The request ID of the first user. + - `requestId2` (Required) - The request ID of the second user. + - `collabId` (Required) - The ID of the collaboration room. + +```json +{ + "requestId1": "user1-request-id", + "requestId2": "user2-request-id", + "collabId": "generated-room-id" +} +``` + +The producer will send a message to the `MATCH_FAILED` queue when a collaboration room was unable to be created. + +- **Queue**: `MATCH_FAILED` +- **Data Produced** + - `requestId1` (Required) - The first request ID associated with the match failure. + - `requestId2` (Required) - The second request ID associated with the match failure. + - `reason` (Required) - The error encountered. + + ```json + { + "requestId1": "6714d1806da8e6d033ac2be1", + "requestId2": "67144180cda8e610333e4b12", + "reason": "Failed to create room", + } + ``` + +--- + +## Consumer + +The consumer will listen for messages on the `QUESTION_FOUND` queue and create a collaboration room when two users are matched. + +- **Queue**: `QUESTION_FOUND` +- **Data in the Message**: + - `user1` (Required) - The details of the first user. + - `user2` (Required) - The details of the second user. + - `question` (Required) - The question assigned to the users. + +```json +{ + "user1": { + "id": "user1-id", + "username": "user1-username", + "requestId": "user1-request-id" + }, + "user2": { + "id": "user2-id", + "username": "user2-username", + "requestId": "user2-request-id" + }, + "question": { + "_id": "66f77e7bf9530832bd839239", + "id": 21, + "title": "Reverse Integer", + "description": "Given a signed 32-bit integer x, return x with its digits reversed.", + "topics": ["Math"], + "difficulty": "Medium" + } +} +``` + +--- \ No newline at end of file diff --git a/services/collaboration/eslint.config.mjs b/services/collaboration/eslint.config.mjs new file mode 100644 index 0000000000..6d1e9b2d4c --- /dev/null +++ b/services/collaboration/eslint.config.mjs @@ -0,0 +1,30 @@ +// @ts-check + +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; + +export default tseslint.config({ + files: ['**/*.ts'], + extends: [ + eslint.configs.recommended, + ...tseslint.configs.strict, + ...tseslint.configs.stylistic, + eslintPluginPrettierRecommended, + ], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + "@typescript-eslint/no-extraneous-class": "off", + "@typescript-eslint/no-require-imports": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-non-null-assertion": "off", + + // https://stackoverflow.com/questions/68816664/get-rid-of-error-delete-eslint-prettier-prettier-and-allow-use-double + 'prettier/prettier': [ + 'error', + { + 'endOfLine': 'auto', + } + ] + }, +}); diff --git a/services/collaboration/package-lock.json b/services/collaboration/package-lock.json new file mode 100644 index 0000000000..2d10bac8a8 --- /dev/null +++ b/services/collaboration/package-lock.json @@ -0,0 +1,4822 @@ +{ + "name": "collaboration-service", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "collaboration-service", + "version": "1.0.0", + "dependencies": { + "amqplib": "^0.10.4", + "body-parser": "^1.19.0", + "cors": "^2.8.5", + "express": "^4.21.0", + "jsonwebtoken": "^9.0.2", + "mongodb": "^5.9.2", + "mongoose": "^8.7.3", + "morgan": "^1.10.0", + "ws": "^8.18.0", + "y-mongodb-provider": "^0.2.0", + "y-websocket": "^1.3.6", + "yjs": "^13.6.15", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/amqplib": "^0.10.5", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.13", + "@types/jsonwebtoken": "^9.0.7", + "@types/mongoose": "^5.11.96", + "@types/morgan": "^1.9.9", + "@types/node": "^18.14.2", + "@types/ws": "^8.5.12", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "eslint": "^8.57.1", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "nodemon": "^3.1.7", + "prettier": "^3.3.3", + "ts-node": "^10.9.1", + "typescript": "^5.0.0", + "typescript-eslint": "^8.11.0" + } + }, + "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/@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, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "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, + "license": "MIT", + "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/regexpp": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.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": "^12.22.0 || ^14.17.0 || >=16.0.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, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/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, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@humanwhocodes/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, + "license": "MIT" + }, + "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, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", + "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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/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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "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, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mongoose": { + "version": "5.11.96", + "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.11.96.tgz", + "integrity": "sha512-keiY22ljJtXyM7osgScmZOHV6eL5VFUD5tQumlu+hjS++HND5nM8jNEdj5CSWfKIJpVwQfPuwQ2SfBqUnCAVRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mongoose": "*" + } + }, + "node_modules/@types/morgan": { + "version": "1.9.9", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz", + "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "18.19.54", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.54.tgz", + "integrity": "sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "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.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "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.0.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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC", + "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, + "license": "MIT" + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/abstract-leveldown": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz", + "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "immediate": "^3.2.3", + "level-concat-iterator": "~2.0.0", + "level-supports": "~1.0.0", + "xtend": "~4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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/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/amqplib/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/amqplib/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/amqplib/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/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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC", + "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, + "license": "MIT" + }, + "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, + "license": "Python-2.0" + }, + "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==", + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "license": "MIT", + "optional": true + }, + "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, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/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/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz", + "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/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", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "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==", + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "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/chalk/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/deferred-leveldown": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz", + "integrity": "sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==", + "license": "MIT", + "dependencies": { + "abstract-leveldown": "~6.2.1", + "inherits": "^2.0.3" + }, + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "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/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding-down": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/encoding-down/-/encoding-down-6.3.0.tgz", + "integrity": "sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==", + "license": "MIT", + "dependencies": { + "abstract-leveldown": "^6.2.1", + "inherits": "^2.0.3", + "level-codec": "^9.0.0", + "level-errors": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "license": "MIT", + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "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, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.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, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/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, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/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, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT" + }, + "node_modules/eslint/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, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/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, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "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, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/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, + "license": "BSD-2-Clause", + "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, + "license": "BSD-2-Clause", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "license": "MIT", + "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.6.0", + "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, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "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, + "license": "MIT", + "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-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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "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/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "license": "ISC" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "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==", + "license": "MIT", + "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, + "license": "ISC" + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "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/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, + "license": "ISC", + "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": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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==", + "license": "MIT", + "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/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/immediate": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", + "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==", + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "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, + "license": "ISC", + "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==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "license": "MIT", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "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, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/level": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/level/-/level-6.0.1.tgz", + "integrity": "sha512-psRSqJZCsC/irNhfHzrVZbmPYXDcEYhA5TVNwr+V92jF44rbf86hqGp8fiT702FyiArScYIlPSBTDUASCVNSpw==", + "license": "MIT", + "dependencies": { + "level-js": "^5.0.0", + "level-packager": "^5.1.0", + "leveldown": "^5.4.0" + }, + "engines": { + "node": ">=8.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/level" + } + }, + "node_modules/level-codec": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-9.0.2.tgz", + "integrity": "sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==", + "license": "MIT", + "dependencies": { + "buffer": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-concat-iterator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz", + "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/level-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-2.0.1.tgz", + "integrity": "sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==", + "license": "MIT", + "dependencies": { + "errno": "~0.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-iterator-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz", + "integrity": "sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^3.4.0", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-js": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/level-js/-/level-js-5.0.2.tgz", + "integrity": "sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg==", + "license": "MIT", + "dependencies": { + "abstract-leveldown": "~6.2.3", + "buffer": "^5.5.0", + "inherits": "^2.0.3", + "ltgt": "^2.1.2" + } + }, + "node_modules/level-packager": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/level-packager/-/level-packager-5.1.1.tgz", + "integrity": "sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==", + "license": "MIT", + "dependencies": { + "encoding-down": "^6.3.0", + "levelup": "^4.3.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-supports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz", + "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/leveldown": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-5.6.0.tgz", + "integrity": "sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "abstract-leveldown": "~6.2.1", + "napi-macros": "~2.0.0", + "node-gyp-build": "~4.1.0" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/levelup": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/levelup/-/levelup-4.4.0.tgz", + "integrity": "sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==", + "license": "MIT", + "dependencies": { + "deferred-leveldown": "~5.3.0", + "level-errors": "~2.0.0", + "level-iterator-stream": "~4.0.0", + "level-supports": "~1.0.0", + "xtend": "~4.0.0" + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lib0": { + "version": "0.2.98", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.98.tgz", + "integrity": "sha512-XteTiNO0qEXqqweWx+b21p/fBnNHUA1NwAtJNJek1oPrewEZs2uiT4gWivHKr9GqCjDPAhchz0UQO8NwU3bBNA==", + "license": "MIT", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.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, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/ltgt": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", + "integrity": "sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==", + "license": "MIT" + }, + "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, + "license": "ISC" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "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, + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mongodb": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", + "integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==", + "license": "Apache-2.0", + "dependencies": { + "bson": "^5.5.0", + "mongodb-connection-string-url": "^2.6.0", + "socks": "^2.7.1" + }, + "engines": { + "node": ">=14.20.1" + }, + "optionalDependencies": { + "@mongodb-js/saslprep": "^1.1.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.0.0", + "kerberos": "^1.0.0 || ^2.0.0", + "mongodb-client-encryption": ">=2.3.0 <3", + "snappy": "^7.2.2" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.7.3", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.7.3.tgz", + "integrity": "sha512-Xl6+dzU5ZpEcDoJ8/AyrIdAwTY099QwpolvV73PIytpK13XqwllLq/9XeVzzLEQgmyvwBVGVgjmMrKbuezxrIA==", + "license": "MIT", + "dependencies": { + "bson": "^6.7.0", + "kareem": "2.6.3", + "mongodb": "6.9.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/mongoose/node_modules/bson": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.9.0.tgz", + "integrity": "sha512-X9hJeyeM0//Fus+0pc5dSUMhhrrmWwQUtdavaQeF3Ta6m69matZkGWV/MrBcnwUeLC8W9kwwc2hfkZgUuCX3Ig==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/mongoose/node_modules/mongodb": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.9.0.tgz", + "integrity": "sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.7.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongoose/node_modules/mongodb-connection-string-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", + "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mongoose/node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/mongoose/node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "license": "MIT", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/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/mquery/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/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/napi-macros": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", + "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==", + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-gyp-build": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.1.1.tgz", + "integrity": "sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nodemon": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", + "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/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, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/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, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "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, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "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/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, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "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", + "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" + } + ], + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.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/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "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" + } + ], + "license": "MIT", + "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" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "ISC" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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==", + "license": "MIT", + "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/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/synckit": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "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, + "license": "MIT", + "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/tslib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "dev": true, + "license": "0BSD" + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "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==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.11.0.tgz", + "integrity": "sha512-cBRGnW3FSlxaYwU8KfAewxFK5uzeOAp0l2KebIlPDOT5olVi65KDG/yjBooPBG0kGW/HLkoz1c/iuBFehcS3IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.11.0", + "@typescript-eslint/parser": "8.11.0", + "@typescript-eslint/utils": "8.11.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/node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz", + "integrity": "sha512-KhGn2LjW1PJT2A/GfDpiyOfS4a8xHQv2myUagTM5+zsormOmBlYsnQ6pobJ8XxJmh6hnHwa2Mbe3fPrDJoDhbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/type-utils": "8.11.0", + "@typescript-eslint/utils": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", + "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/node_modules/@typescript-eslint/parser": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.11.0.tgz", + "integrity": "sha512-lmt73NeHdy1Q/2ul295Qy3uninSqi6wQI18XwSpm8w0ZbQXUpjCAWP1Vlv/obudoBiIjJVjlztjQ+d/Md98Yxg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/typescript-estree": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", + "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/node_modules/@typescript-eslint/scope-manager": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.11.0.tgz", + "integrity": "sha512-Uholz7tWhXmA4r6epo+vaeV7yjdKy5QFCERMjs1kMVsLRKIrSdM6o21W2He9ftp5PP6aWOVpD5zvrvuHZC0bMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/type-utils": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.11.0.tgz", + "integrity": "sha512-ItiMfJS6pQU0NIKAaybBKkuVzo6IdnAhPFZA/2Mba/uBjuPQPet/8+zh5GtLHwmuFRShZx+8lhIs7/QeDHflOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.11.0", + "@typescript-eslint/utils": "8.11.0", + "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/node_modules/@typescript-eslint/types": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.11.0.tgz", + "integrity": "sha512-tn6sNMHf6EBAYMvmPUaKaVeYvhUsrE6x+bXQTxjQRp360h1giATU0WvgeEys1spbvb5R+VpNOZ+XJmjD8wOUHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.11.0.tgz", + "integrity": "sha512-yHC3s1z1RCHoCz5t06gf7jH24rr3vns08XXhfEqzYpd6Hll3z/3g23JRi0jM8A47UFKNc3u/y5KIMx8Ynbjohg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", + "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/node_modules/@typescript-eslint/utils": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.11.0.tgz", + "integrity": "sha512-CYiX6WZcbXNJV7UNB4PLDIBtSdRmRI/nb0FMyqHPTQD1rMjA0foPLaPUV39C/MxkTd/QKSeX+Gb34PPsDVC35g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/typescript-estree": "8.11.0" + }, + "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/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.11.0.tgz", + "integrity": "sha512-EaewX6lxSjRJnc+99+dqzTeoDZUfyrA52d2/HRrkI830kgovWsmIiTfmr0NZorzqic7ga+1bS60lRBUgR3n/Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.11.0", + "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/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typescript-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, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/typescript-eslint/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typescript-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, + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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, + "license": "BSD-2-Clause", + "dependencies": { + "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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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, + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=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, + "license": "ISC", + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "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/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y-leveldb": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/y-leveldb/-/y-leveldb-0.1.2.tgz", + "integrity": "sha512-6ulEn5AXfXJYi89rXPEg2mMHAyyw8+ZfeMMdOtBbV8FJpQ1NOrcgi6DTAcXof0dap84NjHPT2+9d0rb6cFsjEg==", + "license": "MIT", + "dependencies": { + "level": "^6.0.1", + "lib0": "^0.2.31" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, + "node_modules/y-mongodb-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/y-mongodb-provider/-/y-mongodb-provider-0.2.0.tgz", + "integrity": "sha512-l2Qus1lix7TkxemLGzMJn8HYKiUD+vLJpZxYjtPvdBNbM7THhgVuyHvZYzknUDonvnBBjnpbDaZkKPWOMAsSAw==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.94", + "mongodb": "^6.7.0" + }, + "peerDependencies": { + "yjs": "^13.6.15" + } + }, + "node_modules/y-mongodb-provider/node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/y-mongodb-provider/node_modules/bson": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz", + "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/y-mongodb-provider/node_modules/mongodb": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.9.0.tgz", + "integrity": "sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.7.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/y-mongodb-provider/node_modules/mongodb-connection-string-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", + "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/y-mongodb-provider/node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/y-mongodb-provider/node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "license": "MIT", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/y-protocols": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", + "integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.85" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, + "node_modules/y-websocket": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/y-websocket/-/y-websocket-1.3.6.tgz", + "integrity": "sha512-b4V/xw7l0NN6E7FTLEXayHq67QQA2UoXH/y48gvd6L7wQApnB84tg6t3Wo/aXtby1x02G5J3moN+4k+IIQsCDQ==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.31", + "lodash.debounce": "^4.0.8", + "y-leveldb": "^0.1.0", + "y-protocols": "^1.0.0" + }, + "bin": { + "y-websocket-server": "bin/server.js" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "optionalDependencies": { + "ws": "^6.2.1" + } + }, + "node_modules/y-websocket/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "license": "MIT", + "optional": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/yjs": { + "version": "13.6.19", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.19.tgz", + "integrity": "sha512-GNKw4mEUn5yWU2QPHRx8jppxmCm9KzbBhB4qJLUJFiiYD0g/tDVgXQ7aPkyh01YO28kbs2J/BEbWBagjuWyejw==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.86" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/services/collaboration/package.json b/services/collaboration/package.json new file mode 100644 index 0000000000..8048bb3b05 --- /dev/null +++ b/services/collaboration/package.json @@ -0,0 +1,52 @@ +{ + "name": "collaboration-service", + "version": "1.0.0", + "description": "Collaboration service using Yjs, WebSocket, and MongoDB.", + "main": "index.js", + "scripts": { + "start": "tsc && node dist/index.js", + "build": "tsc", + "dev": "nodemon --files src/index.ts", + "test": "echo \"No test specified\" && exit 1", + "lint": "npx eslint .", + "lint:fix": "npx eslint . --fix" + }, + "dependencies": { + "amqplib": "^0.10.4", + "body-parser": "^1.19.0", + "cors": "^2.8.5", + "express": "^4.21.0", + "jsonwebtoken": "^9.0.2", + "mongodb": "^5.9.2", + "mongoose": "^8.7.3", + "morgan": "^1.10.0", + "ws": "^8.18.0", + "y-mongodb-provider": "^0.2.0", + "y-websocket": "^1.3.6", + "yjs": "^13.6.15", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/amqplib": "^0.10.5", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.13", + "@types/jsonwebtoken": "^9.0.7", + "@types/mongoose": "^5.11.96", + "@types/morgan": "^1.9.9", + "@types/node": "^18.14.2", + "@types/ws": "^8.5.12", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "eslint": "^8.57.1", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "nodemon": "^3.1.7", + "prettier": "^3.3.3", + "ts-node": "^10.9.1", + "typescript": "^5.0.0", + "typescript-eslint": "^8.11.0" + }, + "resolutions": { + "yjs": "^13.6.15" + } +} diff --git a/services/collaboration/src/.prettierignore b/services/collaboration/src/.prettierignore new file mode 100644 index 0000000000..420f9273b6 --- /dev/null +++ b/services/collaboration/src/.prettierignore @@ -0,0 +1,39 @@ +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db + +package.json +package-lock.json diff --git a/services/collaboration/src/.prettierrc.json b/services/collaboration/src/.prettierrc.json new file mode 100644 index 0000000000..58416554c2 --- /dev/null +++ b/services/collaboration/src/.prettierrc.json @@ -0,0 +1,11 @@ +{ + "tabWidth": 4, + "useTabs": false, + "singleQuote": true, + "semi": true, + "bracketSpacing": true, + "arrowParens": "avoid", + "trailingComma": "all", + "bracketSameLine": true, + "printWidth": 120 +} diff --git a/services/collaboration/src/app.ts b/services/collaboration/src/app.ts new file mode 100644 index 0000000000..6609ee19ab --- /dev/null +++ b/services/collaboration/src/app.ts @@ -0,0 +1,27 @@ +import express, { Express } from 'express'; +import morgan from 'morgan'; +import cors from 'cors'; +import roomRouter from './routes/roomRoutes'; +import bodyParser from 'body-parser'; +import router from './routes'; +import config from './config'; +import { verifyAccessToken } from './middleware/jwt'; + +const app: Express = express(); + +app.use(morgan('dev')); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(express.json()); + +app.use( + cors({ + origin: config.CORS_ORIGIN, + methods: ['GET', 'PATCH'], + allowedHeaders: ['Origin', 'X-Request-With', 'Content-Type', 'Accept', 'Authorization'], + }), +); + +app.use('/', router); +app.use('/room', verifyAccessToken, roomRouter); + +export default app; diff --git a/services/collaboration/src/config.ts b/services/collaboration/src/config.ts new file mode 100644 index 0000000000..8d6307806a --- /dev/null +++ b/services/collaboration/src/config.ts @@ -0,0 +1,47 @@ +import { z } from 'zod'; + +const envSchema = z + .object({ + COLLAB_DB_CLOUD_URI: z.string().trim().optional(), + COLLAB_DB_LOCAL_URI: z.string().trim().optional(), + YJS_DB_CLOUD_URI: z.string().trim().optional(), + YJS_DB_LOCAL_URI: z.string().trim().optional(), + BROKER_URL: z.string().url(), + NODE_ENV: z.enum(['development', 'production']).default('development'), + CORS_ORIGIN: z.union([z.string().url(), z.literal('*')]).default('*'), + PORT: z.coerce.number().min(1024).default(8084), + JWT_SECRET: z.string().trim().min(32), + }) + .superRefine((data, ctx) => { + const isUrl = z.string().url(); + const cloudRes = isUrl.safeParse(data.COLLAB_DB_CLOUD_URI); + const localRes = isUrl.safeParse(data.COLLAB_DB_LOCAL_URI); + if (data.NODE_ENV === 'production') { + cloudRes.error?.issues.forEach(i => ctx.addIssue({ ...i, path: ['COLLAB_DB_CLOUD_URI'] })); + } else if (data.NODE_ENV === 'development') { + localRes.error?.issues.forEach(i => ctx.addIssue({ ...i, path: ['COLLAB_DB_LOCAL_URI'] })); + } + }) + .superRefine((data, ctx) => { + const isUrl = z.string().url(); + const cloudRes = isUrl.safeParse(data.YJS_DB_CLOUD_URI); + const localRes = isUrl.safeParse(data.YJS_DB_LOCAL_URI); + if (data.NODE_ENV === 'production') { + cloudRes.error?.issues.forEach(i => ctx.addIssue({ ...i, path: ['YJS_DB_CLOUD_URI'] })); + } else if (data.NODE_ENV === 'development') { + localRes.error?.issues.forEach(i => ctx.addIssue({ ...i, path: ['YJS_DB_LOCAL_URI'] })); + } + }); + +const result = envSchema.safeParse(process.env); +if (!result.success) { + console.error('There is an error with the environment variables:', result.error.issues); + process.exit(1); +} + +const { NODE_ENV, COLLAB_DB_CLOUD_URI, COLLAB_DB_LOCAL_URI, YJS_DB_CLOUD_URI, YJS_DB_LOCAL_URI } = result.data; +const COLLAB_DB_URI = (NODE_ENV === 'production' ? COLLAB_DB_CLOUD_URI : COLLAB_DB_LOCAL_URI) as string; +const YJS_DB_URI = (NODE_ENV === 'production' ? YJS_DB_CLOUD_URI : YJS_DB_LOCAL_URI) as string; +const config = { ...result.data, COLLAB_DB_URI, YJS_DB_URI }; + +export default config; diff --git a/services/collaboration/src/controllers/index.ts b/services/collaboration/src/controllers/index.ts new file mode 100644 index 0000000000..98bd3586eb --- /dev/null +++ b/services/collaboration/src/controllers/index.ts @@ -0,0 +1,7 @@ +import { Request, Response } from 'express'; + +export const getHealth = async (req: Request, res: Response) => { + res.status(200).json({ + message: 'Server is up and running!', + }); +}; diff --git a/services/collaboration/src/controllers/roomController.ts b/services/collaboration/src/controllers/roomController.ts new file mode 100644 index 0000000000..c80f134327 --- /dev/null +++ b/services/collaboration/src/controllers/roomController.ts @@ -0,0 +1,159 @@ +import { Request, Response } from 'express'; +import { + createRoomInDB, + createYjsDocument, + deleteYjsDocument, + findRoomById, + findRoomsByUserId, + closeRoomById, + updateRoomUserStatus, +} from '../services/mongodbService'; +import { handleHttpNotFound, handleHttpSuccess, handleHttpServerError, handleHttpBadRequest } from '../utils/helper'; +import { Room } from './types'; + +export enum Difficulty { + Easy = 'Easy', + Medium = 'Medium', + Hard = 'Hard', +} + +export interface Question { + id: number; + description: string; + difficulty: Difficulty; + title: string; + topics?: string[]; +} + +/** + * Create a room with users, question details, and Yjs document + * @param user1 + * @param user2 + * @param question + * @returns roomId + */ +export const createRoomWithQuestion = async (user1: any, user2: any, question: Question) => { + try { + const roomId = await createRoomInDB(user1, user2, question); + await createYjsDocument(roomId.toString()); + return roomId; + } catch (error) { + console.error('Error fetching question or creating room:', error); + return null; + } +}; + +export const getRoomIdsByUserIdController = async (req: Request, res: Response) => { + const userId = req.user.id; + + console.log('Received request for user ID:', userId); + try { + const rooms = await findRoomsByUserId(userId); + if (!rooms || rooms.length === 0) { + return handleHttpNotFound(res, 'No rooms found for the given user'); + } + + const roomIds = rooms.map(room => (room as Room)._id); + return handleHttpSuccess(res, roomIds); + } catch (error) { + console.error('Error fetching rooms by user ID:', error); + return handleHttpServerError(res, 'Failed to retrieve room IDs by user ID'); + } +}; + +/** + * Controller function to get room details by room ID + * @param req + * @param res + */ +export const getRoomByRoomIdController = async (req: Request, res: Response) => { + try { + const roomId = req.params.roomId; + + const room = await findRoomById(roomId, req.user.id); + if (!room) { + return handleHttpNotFound(res, 'Room not found'); + } + + return handleHttpSuccess(res, { + room_id: room._id, + users: room.users, + question: room.question, + createdAt: room.createdAt, + room_status: room.room_status, + }); + } catch (error) { + console.error('Error fetching room by room ID:', error); + return handleHttpServerError(res, 'Failed to retrieve room by room ID'); + } +}; + +/** + * Controller function to close a room and delete its Yjs document + * @param req + * @param res + */ +export const closeRoomController = async (req: Request, res: Response) => { + try { + const userId = req.user.id; + const roomId = req.params.roomId; + + const room = await findRoomById(roomId, userId); + if (!room) { + return handleHttpNotFound(res, 'Room not found'); + } + + if (!room.room_status) { + console.log(`Room ${roomId} is already closed.`); + return handleHttpSuccess(res, `Room ${roomId} is already closed`); + } + + const result = await closeRoomById(roomId); + if (result.modifiedCount === 0) { + return handleHttpNotFound(res, 'Room not found'); + } + + await deleteYjsDocument(roomId); + console.log(`Room ${roomId} closed and Yjs document removed`); + + return handleHttpSuccess(res, `Room ${roomId} successfully closed`); + } catch (error) { + console.error('Error closing room:', error); + return handleHttpServerError(res, 'Failed to close room'); + } +}; + +/** + * Controller function to update user status in a room + * @param req + * @param res + */ +export const updateUserStatusInRoomController = async (req: Request, res: Response) => { + const userId = req.user.id; + const { roomId } = req.params; + const { isForfeit } = req.body; + + if (typeof isForfeit !== 'boolean') { + return handleHttpBadRequest(res, 'Invalid isForfeit value. Must be true or false.'); + } + + try { + const room = await findRoomById(roomId, userId); + if (!room) { + return handleHttpNotFound(res, 'Room not found'); + } + + const updatedRoom = await updateRoomUserStatus(roomId, userId, isForfeit); + if (!updatedRoom) { + return handleHttpNotFound(res, 'User not found in room'); + } + + return handleHttpSuccess(res, { + message: 'User isForfeit status updated successfully', + room: updatedRoom, + }); + } catch (error) { + console.error('Error updating user isForfeit status in room:', error); + return handleHttpServerError(res, 'Failed to update user isForfeit status in room'); + } +}; diff --git a/services/collaboration/src/controllers/types.ts b/services/collaboration/src/controllers/types.ts new file mode 100644 index 0000000000..89dcab9caa --- /dev/null +++ b/services/collaboration/src/controllers/types.ts @@ -0,0 +1,21 @@ +import { ObjectId } from 'mongodb'; +import { Question } from './roomController'; + +/** + * @fileoverview Types for the collaboration service. + */ + +export interface User { + id: string; + username: string; + requestId: string; + isForfeit?: boolean; +} + +export interface Room { + _id: ObjectId; + users: User[]; + question: Question; + createdAt: Date; + room_status: boolean; +} diff --git a/services/collaboration/src/events/broker.ts b/services/collaboration/src/events/broker.ts new file mode 100644 index 0000000000..095201dc5d --- /dev/null +++ b/services/collaboration/src/events/broker.ts @@ -0,0 +1,68 @@ +import client, { Channel, Connection } from 'amqplib'; +import config from '../config'; + +/** + * Adapated from + * https://hassanfouad.medium.com/using-rabbitmq-with-nodejs-and-typescript-8b33d56a62cc + */ +class MessageBroker { + connection!: Connection; + channel!: Channel; + private connected = false; + + async connect(): Promise { + if (this.connection && this.channel) { + return; + } + + try { + this.connection = await client.connect(config.BROKER_URL); + console.log('Connected to RabbitMQ'); + this.channel = await this.connection.createChannel(); + this.connected = true; + } catch (error) { + console.error('Failed to connect to RabbitMQ:', error); + throw error; + } + } + + async produce(queue: string, message: any): Promise { + try { + if (!this.connected) { + await this.connect(); + } + this.channel.sendToQueue(queue, Buffer.from(JSON.stringify(message))); + } catch (error) { + console.error('Failed to produce message:', error); + throw error; + } + } + + async consume(queue: string, onMessage: (message: T) => void): Promise { + try { + if (!this.connected) { + await this.connect(); + } + + await this.channel.assertQueue(queue, { durable: true }); + await this.channel.consume( + queue, + msg => { + if (!msg) { + return console.error('Invalid message from queue', queue); + } + const parsedMessage = JSON.parse(msg.content.toString()) as T; + onMessage(parsedMessage); + this.channel.ack(msg); + }, + { noAck: false }, + ); + } catch (error) { + console.error('Failed to consume message:', error); + throw error; + } + } +} + +const messageBroker = new MessageBroker(); +export default messageBroker; diff --git a/services/collaboration/src/events/consumer.ts b/services/collaboration/src/events/consumer.ts new file mode 100644 index 0000000000..e5882282bc --- /dev/null +++ b/services/collaboration/src/events/consumer.ts @@ -0,0 +1,26 @@ +import { Queues } from './queues'; +import messageBroker from './broker'; +import { createRoomWithQuestion } from '../controllers/roomController'; +import { QuestionFoundEvent } from '../types/event'; +import { produceCollabCreated, produceCollabCreateFailedEvent } from './producer'; + +async function consumeQuestionFound(message: QuestionFoundEvent) { + console.log('Attempting to create room:', message); + const { user1, user2, question } = message; + + const { requestId: requestId1 } = user1; + const { requestId: requestId2 } = user2; + + const roomId = await createRoomWithQuestion(user1, user2, question); + if (roomId) { + console.log('Room created with ID:', message, roomId); + await produceCollabCreated(requestId1, requestId2, roomId, question); + } else { + console.log('Failed to create room:', message); + await produceCollabCreateFailedEvent(requestId1, requestId2); + } +} + +export async function initializeConsumers() { + messageBroker.consume(Queues.QUESTION_FOUND, consumeQuestionFound); +} diff --git a/services/collaboration/src/events/producer.ts b/services/collaboration/src/events/producer.ts new file mode 100644 index 0000000000..d55b529a31 --- /dev/null +++ b/services/collaboration/src/events/producer.ts @@ -0,0 +1,20 @@ +import { CollabCreatedEvent, IdType, MatchFailedEvent, Question } from '../types/event'; +import messageBroker from './broker'; +import { Queues } from './queues'; + +const COLLAB_CREATED_ERROR = 'Failed to create room'; + +export async function produceCollabCreated( + requestId1: IdType, + requestId2: IdType, + collabId: IdType, + question: Question, +) { + const message: CollabCreatedEvent = { requestId1, requestId2, collabId, question }; + await messageBroker.produce(Queues.COLLAB_CREATED, message); +} + +export async function produceCollabCreateFailedEvent(requestId1: IdType, requestId2: IdType) { + const message: MatchFailedEvent = { requestId1, requestId2, reason: COLLAB_CREATED_ERROR }; + await messageBroker.produce(Queues.MATCH_FAILED, message); +} diff --git a/services/collaboration/src/events/queues.ts b/services/collaboration/src/events/queues.ts new file mode 100644 index 0000000000..714d3e38ff --- /dev/null +++ b/services/collaboration/src/events/queues.ts @@ -0,0 +1,9 @@ +/** + * Enum for queues + */ +export enum Queues { + MATCH_FOUND = 'MATCH_FOUND', + QUESTION_FOUND = 'QUESTION_FOUND', + COLLAB_CREATED = 'COLLAB_CREATED', + MATCH_FAILED = 'MATCH_FAILED', +} diff --git a/services/collaboration/src/index.ts b/services/collaboration/src/index.ts new file mode 100644 index 0000000000..a5e0e273ca --- /dev/null +++ b/services/collaboration/src/index.ts @@ -0,0 +1,22 @@ +import { startMongoDB } from './services/mongodbService'; +import { startWebSocketServer } from './services/webSocketService'; +import app from './app'; +import http from 'http'; +import { initializeConsumers } from './events/consumer'; +import config from './config'; + +const PORT = config.PORT; + +startMongoDB() + .then(() => { + const server = http.createServer(app); + startWebSocketServer(server); + server.listen(PORT, () => { + console.log(`Server (HTTP + WebSocket) running on port ${PORT}`); + }); + }) + .then(() => initializeConsumers()) + .then(() => console.log('Consumers are listening')) + .catch(error => { + console.error('Failed to start services:', error); + }); diff --git a/services/collaboration/src/middleware/express.d.ts b/services/collaboration/src/middleware/express.d.ts new file mode 100644 index 0000000000..c336d2d933 --- /dev/null +++ b/services/collaboration/src/middleware/express.d.ts @@ -0,0 +1,9 @@ +import { RequestUser } from './request'; + +declare global { + namespace Express { + export interface Request { + user: RequestUser; + } + } +} diff --git a/services/collaboration/src/middleware/jwt.ts b/services/collaboration/src/middleware/jwt.ts new file mode 100644 index 0000000000..590717be1b --- /dev/null +++ b/services/collaboration/src/middleware/jwt.ts @@ -0,0 +1,28 @@ +import jwt from 'jsonwebtoken'; +import { NextFunction, Request, Response } from 'express'; +import config from '../config'; +import { userSchema } from './request'; +import { handleHttpBadRequest } from '../utils/helper'; + +export async function verifyAccessToken(req: Request, res: Response, next: NextFunction) { + const authHeader = req.headers['authorization']; + if (!authHeader) { + return handleHttpBadRequest(res, 'Authentication failed: No token provided'); + } + + const token = authHeader.split(' ')[1]; + + jwt.verify(token, config.JWT_SECRET, (err, user) => { + if (err) { + return handleHttpBadRequest(res, 'Authentication failed: Invalid token'); + } + + const result = userSchema.safeParse(user); + if (result.error) { + return handleHttpBadRequest(res, 'Authentication failed: Token validation error'); + } + + req.user = result.data; + next(); + }); +} diff --git a/services/collaboration/src/middleware/request.ts b/services/collaboration/src/middleware/request.ts new file mode 100644 index 0000000000..d60cad964e --- /dev/null +++ b/services/collaboration/src/middleware/request.ts @@ -0,0 +1,19 @@ +import { Types } from 'mongoose'; +import { z } from 'zod'; + +export enum Role { + Admin = 'admin', + User = 'user', +} + +export interface RequestUser { + id: string; + username: string; + role: Role; +} + +export const userSchema = z.object({ + id: z.string(), + username: z.string(), + role: z.nativeEnum(Role), +}); diff --git a/services/collaboration/src/routes/index.ts b/services/collaboration/src/routes/index.ts new file mode 100644 index 0000000000..7e7faddcfd --- /dev/null +++ b/services/collaboration/src/routes/index.ts @@ -0,0 +1,7 @@ +import express from 'express'; +import { getHealth } from '../controllers'; +const router = express.Router(); + +router.get('/ht', getHealth); + +export default router; diff --git a/services/collaboration/src/routes/roomRoutes.ts b/services/collaboration/src/routes/roomRoutes.ts new file mode 100644 index 0000000000..e5239c45ec --- /dev/null +++ b/services/collaboration/src/routes/roomRoutes.ts @@ -0,0 +1,34 @@ +import { Router } from 'express'; +import { + getRoomIdsByUserIdController, + getRoomByRoomIdController, + closeRoomController, + updateUserStatusInRoomController, +} from '../controllers/roomController'; + +/** + * Router for room endpoints. + */ +const router = Router(); + +/** + * Get room IDs by user ID (userId is now obtained from the JWT token) + */ +router.get('/user/rooms', getRoomIdsByUserIdController); + +/** + * Get room by room ID + */ +router.get('/:roomId', getRoomByRoomIdController); + +/** + * Close room by room ID + */ +router.patch('/:roomId/close', closeRoomController); + +/** + * Update user isForfeit status in a room + */ +router.patch('/:roomId/user/isForfeit', updateUserStatusInRoomController); + +export default router; diff --git a/services/collaboration/src/services/mongodbService.ts b/services/collaboration/src/services/mongodbService.ts new file mode 100644 index 0000000000..41da9a08f1 --- /dev/null +++ b/services/collaboration/src/services/mongodbService.ts @@ -0,0 +1,205 @@ +import { MongoClient, Db, ObjectId, WithId } from 'mongodb'; +import { MongodbPersistence } from 'y-mongodb-provider'; +import * as Y from 'yjs'; +import config from '../config'; +import { Question } from '../controllers/roomController'; +import { Room } from '../controllers/types'; + +let roomDb: Db | null = null; +let yjsDb: Db | null = null; +/** Yjs MongoDB persistence provider for Yjs documents */ +export let mdb!: MongodbPersistence; + +/** + * Connect to the room database + */ +const connectToRoomDB = async (): Promise => { + try { + if (!roomDb) { + const client = new MongoClient(config.COLLAB_DB_URI); + await client.connect(); + roomDb = client.db('collaboration-service'); + console.log('Connected to the collaboration-service (room) database'); + } + return roomDb; + } catch (error) { + console.error('Failed to connect to the Room database:', error); + throw error; + } +}; + +/** + * Connect to the YJS database + */ +const connectToYJSDB = async (): Promise => { + try { + if (!yjsDb) { + mdb = new MongodbPersistence(config.YJS_DB_URI, { + flushSize: 100, + multipleCollections: true, + }); + + const client = new MongoClient(config.YJS_DB_URI); + await client.connect(); + yjsDb = client.db('yjs-documents'); + console.log('Connected to the YJS database'); + } + return yjsDb; + } catch (error) { + console.error('Failed to connect to the YJS database:', error); + throw error; + } +}; + +/** + * Start MongoDB connection for rooms and Yjs + */ +export const startMongoDB = async (): Promise => { + try { + await connectToRoomDB(); + await connectToYJSDB(); + console.log('Connected to both Room and YJS MongoDB databases'); + } catch (error) { + console.error('MongoDB connection failed:', error); + throw error; + } +}; + +/** + * Save room data in the MongoDB rooms database and create a Yjs document + * @param roomData + * @returns roomId + */ +export const createRoomInDB = async (user1: any, user2: any, question: Question): Promise => { + try { + const db = await connectToRoomDB(); + const result = await db.collection('rooms').insertOne({ + users: [ + { ...user1, isForfeit: false }, + { ...user2, isForfeit: false }, + ], + question, + createdAt: new Date(), + room_status: true, + }); + return result.insertedId.toString(); + } catch (error) { + console.error('Error creating room in DB:', error); + throw error; + } +}; + +/** + * Find a room by roomId and userId + * @param roomId + * @param userId + * @returns + */ +export const findRoomById = async (roomId: string, userId: string): Promise | null> => { + try { + const db = await connectToRoomDB(); + return await db.collection('rooms').findOne({ _id: new ObjectId(roomId) }); + } catch (error) { + console.error(`Error finding room by room ID ${roomId} and user ID ${userId}:`, error); + throw error; + } +}; + +/** + * Create and bind a Yjs document using the room_id as the document name + * @param roomId + * @returns + */ +export const createYjsDocument = async (roomId: string) => { + try { + const yjsDoc = await mdb.getYDoc(roomId); + console.log(`Yjs document created for room: ${roomId}`); + const initialSync = Y.encodeStateAsUpdate(yjsDoc); + await mdb.storeUpdate(roomId, initialSync); + + return yjsDoc; + } catch (error) { + console.error(`Failed to create Yjs document for room ${roomId}:`, error); + throw error; + } +}; + +/** + * Delete the Yjs document (collection) for a given room ID + * @param roomId + */ +export const deleteYjsDocument = async (roomId: string) => { + try { + const db = await connectToYJSDB(); + await db.collection(roomId).drop(); + console.log(`Yjs document collection for room ${roomId} deleted`); + } catch (error) { + console.error(`Failed to delete Yjs document for room ${roomId}:`, error); + throw error; + } +}; + +/** + * Find rooms by user ID where room_status is true + * @param userId + */ +export const findRoomsByUserId = async (userId: string): Promise[]> => { + try { + const db = await connectToRoomDB(); + console.log(`Querying for rooms with user ID: ${userId}`); + const rooms = await db + .collection('rooms') + .find({ + users: { $elemMatch: { id: userId } }, + room_status: true, + }) + .toArray(); + console.log('Rooms found:', rooms); + return rooms; + } catch (error) { + console.error(`Error querying rooms for user ID ${userId}:`, error); + throw error; + } +}; + +/** + * Set the room status to false (close the room) by room ID + * @param roomId + */ +export const closeRoomById = async (roomId: string) => { + try { + const db = await connectToRoomDB(); + const result = await db + .collection('rooms') + .updateOne({ _id: new ObjectId(roomId) as ObjectId }, { $set: { room_status: false } }); + console.log(`Room status updated to closed for room ID: ${roomId}`); + return result; + } catch (error) { + console.error(`Error closing room with ID ${roomId}:`, error); + throw error; + } +}; + +export const updateRoomUserStatus = async (roomId: string, userId: string, isForfeit: boolean) => { + try { + const db = await connectToRoomDB(); + const result = await db + .collection('rooms') + .findOneAndUpdate( + { _id: new ObjectId(roomId), 'users.id': userId }, + { $set: { 'users.$.isForfeit': isForfeit } }, + { returnDocument: 'after' }, + ); + + if (!result.value) { + console.error(`User with ID ${userId} not found in room ${roomId}`); + return null; + } + + console.log(`User isForfeit status updated successfully for user ID: ${userId} in room ID: ${roomId}`); + return result.value; + } catch (error) { + console.error(`Error updating user isForfeit status for user ID ${userId} in room ${roomId}:`, error); + throw error; + } +}; diff --git a/services/collaboration/src/services/webSocketService.ts b/services/collaboration/src/services/webSocketService.ts new file mode 100644 index 0000000000..547b5e9084 --- /dev/null +++ b/services/collaboration/src/services/webSocketService.ts @@ -0,0 +1,108 @@ +import jwt from 'jsonwebtoken'; +import { IncomingMessage, Server } from 'http'; +import { WebSocketServer, WebSocket } from 'ws'; +import * as Y from 'yjs'; +import { findRoomById, mdb } from './mongodbService'; +import { handleAuthFailed, handleRoomClosed } from '../utils/helper'; +import config from '../config'; +import { RequestUser, userSchema } from '../middleware/request'; + +const { setPersistence, setupWSConnection } = require('../utils/utility.js'); + +const URL_REGEX = /^.*\/([0-9a-f]{24})\?accessToken=([a-zA-Z0-9\-._~%]{1,})$/; + +const authorize = async (ws: WebSocket, request: IncomingMessage): Promise => { + const url = request.url; + const match = url?.match(URL_REGEX); + if (!match) { + handleAuthFailed(ws, 'Authorization failed: Invalid format'); + return false; + } + const roomId = match[1]; + const accessToken = match[2]; + + const user: RequestUser | null = await new Promise(resolve => { + jwt.verify(accessToken, config.JWT_SECRET, async (err, data) => { + const result = userSchema.safeParse(data); + if (err || result.error) { + resolve(null); + return; + } else { + resolve(result.data); + } + }); + }); + if (!user) { + handleAuthFailed(ws, 'Authorization failed: Invalid token'); + return false; + } + + const room = await findRoomById(roomId, user.id); + if (!room) { + handleAuthFailed(ws, 'Authorization failed'); + return false; + } + + if (!room.room_status) { + handleRoomClosed(ws); + return false; + } + + const userInRoom = room.users.find((u: { id: string }) => u.id === user.id); + if (userInRoom?.isForfeit) { + handleAuthFailed(ws, 'Authorization failed: User has forfeited'); + return false; + } + console.log('WebSocket connection established for room:', roomId); + return true; +}; + +/** + * Start and configure the WebSocket server + * @returns {WebSocketServer} The configured WebSocket server instance + */ +export const startWebSocketServer = (server: Server) => { + const wss = new WebSocketServer({ server }); + + wss.on('connection', async (conn: WebSocket, req: IncomingMessage) => { + const isAuthorized = await authorize(conn, req); + if (!isAuthorized) { + return; + } + + try { + setupWSConnection(conn, req); + } catch (error) { + console.error('Failed to set up WebSocket connection:', error); + handleAuthFailed(conn, 'Authorization failed'); + } + }); + + setPersistence({ + bindState: async (docName: string, ydoc: Y.Doc) => { + try { + const persistedYdoc = await mdb.getYDoc(docName); + console.log(`Loaded persisted document for ${docName}`); + + const newUpdates = Y.encodeStateAsUpdate(ydoc); + mdb.storeUpdate(docName, newUpdates); + + Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(persistedYdoc)); + + ydoc.on('update', async update => { + await mdb.storeUpdate(docName, update); + }); + } catch (error) { + console.error(`Error loading document ${docName}:`, error); + } + }, + writeState: async (docName: string, ydoc: Y.Doc) => { + return new Promise(resolve => { + resolve(true); + }); + }, + }); + + console.log('WebSocket server initialized'); + return wss; +}; diff --git a/services/collaboration/src/types/event.ts b/services/collaboration/src/types/event.ts new file mode 100644 index 0000000000..b51a1c856c --- /dev/null +++ b/services/collaboration/src/types/event.ts @@ -0,0 +1,50 @@ +import { Types } from 'mongoose'; + +export type IdType = string | Types.ObjectId; + +export enum Difficulty { + Easy = 'Easy', + Medium = 'Medium', + Hard = 'Hard', +} + +export interface Question { + id: number; + description: string; + difficulty: Difficulty; + title: string; + topics?: string[]; +} + +export interface UserWithRequest { + id: Types.ObjectId | string; + username: string; + email: string; + requestId: Types.ObjectId | string; +} + +export interface MatchFoundEvent { + user1: UserWithRequest; + user2: UserWithRequest; + topics: string[]; + difficulty: Difficulty; +} + +export interface QuestionFoundEvent { + user1: UserWithRequest; + user2: UserWithRequest; + question: Question; +} + +export interface CollabCreatedEvent { + requestId1: IdType; + requestId2: IdType; + collabId: IdType; + question: Question; +} + +export interface MatchFailedEvent { + requestId1: IdType; + requestId2: IdType; + reason: string; +} diff --git a/services/collaboration/src/utils/helper.ts b/services/collaboration/src/utils/helper.ts new file mode 100644 index 0000000000..887b9be6fc --- /dev/null +++ b/services/collaboration/src/utils/helper.ts @@ -0,0 +1,57 @@ +import { Response } from 'express'; +import { WebSocket } from 'ws'; + +const WEBSOCKET_AUTH_FAILED = 4000; +const WEBSOCKET_ROOM_CLOSED = 4001; + +/** + * HTTP-specific handlers + */ + +/** + * Handle bad request for HTTP + * @param client + * @param message + */ +export const handleHttpBadRequest = (client: Response, message = 'Bad Request') => { + client.status(400).json({ status: 'Error', message }); +}; + +/** + * Handle not found for HTTP + * @param client + * @param message + */ +export const handleHttpNotFound = (client: Response, message = 'Not Found') => { + client.status(404).json({ status: 'Error', message }); +}; + +/** + * Handle success for HTTP + * @param client + * @param data + */ +export const handleHttpSuccess = (client: Response, data: string | object = 'Success') => { + client.status(200).json({ status: 'Success', data }); +}; + +/** + * Handle internal server error for HTTP + * @param client + * @param message + */ +export const handleHttpServerError = (client: Response, message = 'Internal Server Error') => { + client.status(500).json({ status: 'Error', message }); +}; + +/** + * WS-specific handlers + */ + +export const handleAuthFailed = (ws: WebSocket, message: string) => { + ws.close(WEBSOCKET_AUTH_FAILED, message); +}; + +export const handleRoomClosed = (ws: WebSocket, message = 'Room closed') => { + ws.close(WEBSOCKET_ROOM_CLOSED, message); +}; diff --git a/services/collaboration/src/utils/utility.js b/services/collaboration/src/utils/utility.js new file mode 100644 index 0000000000..05ef7de3e4 --- /dev/null +++ b/services/collaboration/src/utils/utility.js @@ -0,0 +1,264 @@ +/** + * Adapted from: https://github.com/yjs/y-websocket/blob/master/bin/utils.cjs + */ + +const Y = require('yjs'); +const syncProtocol = require('y-protocols/sync'); +const awarenessProtocol = require('y-protocols/awareness'); + +const encoding = require('lib0/encoding'); +const decoding = require('lib0/decoding'); +const map = require('lib0/map'); + +const wsReadyStateConnecting = 0; +const wsReadyStateOpen = 1; +const wsReadyStateClosing = 2; +const wsReadyStateClosed = 3; + +const gcEnabled = process.env.GC !== 'false' && process.env.GC !== '0'; + +let persistence = null; + +/** + * Set persistence + * @param persistence_ + */ +const setPersistence = (persistence_) => { + persistence = persistence_; +}; + +/** + * Get persistence + * @returns {null} + */ +const getPersistence = () => persistence; + +/** + * Map of shared documents + * @type {Map} + */ +const docs = new Map(); + +const messageSync = 0; +const messageAwareness = 1; + +/** + * Send message to all connections except the sender + * @param update + * @param origin + * @param doc + */ +const updateHandler = (update, origin, doc) => { + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageSync); + syncProtocol.writeUpdate(encoder, update); + const message = encoding.toUint8Array(encoder); + doc.conns.forEach((_, conn) => send(doc, conn, message)); +}; + +/** + * Send message to all connections + */ +class WSSharedDoc extends Y.Doc { + constructor(name) { + super({ gc: gcEnabled }); + this.name = name; + this.conns = new Map(); + this.awareness = new awarenessProtocol.Awareness(this); + this.awareness.setLocalState(null); + + const awarenessChangeHandler = ({ added, updated, removed }, conn) => { + const changedClients = added.concat(updated, removed); + if (conn !== null) { + const connControlledIDs = this.conns.get(conn); + if (connControlledIDs !== undefined) { + added.forEach((clientID) => { + connControlledIDs.add(clientID); + }); + removed.forEach((clientID) => { + connControlledIDs.delete(clientID); + }); + } + } + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageAwareness); + encoding.writeVarUint8Array( + encoder, + awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients), + ); + const buff = encoding.toUint8Array(encoder); + this.conns.forEach((_, c) => { + send(this, c, buff); + }); + }; + this.awareness.on('update', awarenessChangeHandler); + this.on('update', updateHandler); + } +} + +/** + * Get shared document by name + * @param docname + * @param gc + * @returns {*} + */ +const getYDoc = (docname, gc = true) => + map.setIfUndefined(docs, docname, () => { + const doc = new WSSharedDoc(docname); + doc.gc = gc; + if (persistence !== null) { + persistence.bindState(docname, doc); + } + docs.set(docname, doc); + return doc; + }); + +/** + * Message listener + * @param conn + * @param doc + * @param message + */ +const messageListener = (conn, doc, message) => { + try { + const encoder = encoding.createEncoder(); + const decoder = decoding.createDecoder(message); + const messageType = decoding.readVarUint(decoder); + switch (messageType) { + case messageSync: + encoding.writeVarUint(encoder, messageSync); + syncProtocol.readSyncMessage(decoder, encoder, doc, conn); + + if (encoding.length(encoder) > 1) { + send(doc, conn, encoding.toUint8Array(encoder)); + } + break; + case messageAwareness: { + awarenessProtocol.applyAwarenessUpdate( + doc.awareness, + decoding.readVarUint8Array(decoder), + conn, + ); + break; + } + } + } catch (err) { + console.error(err); + doc.emit('error', [err]); + } +}; + +/** + * Close connection + * @param doc + * @param conn + */ +const closeConn = (doc, conn) => { + if (doc.conns.has(conn)) { + const controlledIds = doc.conns.get(conn); + doc.conns.delete(conn); + if (controlledIds) { + awarenessProtocol.removeAwarenessStates(doc.awareness, Array.from(controlledIds), null); + } + if (doc.conns.size === 0 && persistence !== null) { + persistence.writeState(doc.name, doc).then(() => { + doc.destroy(); + }); + docs.delete(doc.name); + } + } + conn.close(); +}; + +/** + * Send message + * @param doc + * @param conn + * @param m + */ +const send = (doc, conn, m) => { + if (conn.readyState !== wsReadyStateConnecting && conn.readyState !== wsReadyStateOpen) { + closeConn(doc, conn); + } + try { + conn.send(m, (err) => { + err != null && closeConn(doc, conn); + }); + } catch (e) { + closeConn(doc, conn); + } +}; + +const pingTimeout = 30000; + +/** + * Setup WS connection + * @param conn + * @param req + * @param docName + * @param gc + */ +const setupWSConnection = ( + conn, + req, + { docName = req.url.slice(1).split('?')[0], gc = true } = {}, +) => { + conn.binaryType = 'arraybuffer'; + const doc = getYDoc(docName, gc); + doc.conns.set(conn, new Set()); + conn.on('message', (message) => messageListener(conn, doc, new Uint8Array(message))); + + let pongReceived = true; + const pingInterval = setInterval(() => { + if (!pongReceived) { + if (doc.conns.has(conn)) { + closeConn(doc, conn); + } + clearInterval(pingInterval); + } else if (doc.conns.has(conn)) { + pongReceived = false; + try { + conn.ping(); + } catch (e) { + closeConn(doc, conn); + clearInterval(pingInterval); + } + } + }, pingTimeout); + conn.on('close', () => { + closeConn(doc, conn); + clearInterval(pingInterval); + }); + conn.on('pong', () => { + pongReceived = true; + }); + + { + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageSync); + syncProtocol.writeSyncStep1(encoder, doc); + send(doc, conn, encoding.toUint8Array(encoder)); + const awarenessStates = doc.awareness.getStates(); + if (awarenessStates.size > 0) { + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageAwareness); + encoding.writeVarUint8Array( + encoder, + awarenessProtocol.encodeAwarenessUpdate(doc.awareness, Array.from(awarenessStates.keys())), + ); + send(doc, conn, encoding.toUint8Array(encoder)); + } + } +}; + +/** + * Export + * @type {{getYDoc: (function(*, boolean=): *), docs: Map<*, *>, getPersistence: (function(): null), setupWSConnection: setupWSConnection, setPersistence: setPersistence}} + */ +module.exports = { + setPersistence, + getPersistence, + docs, + getYDoc, + setupWSConnection, +}; \ No newline at end of file diff --git a/services/collaboration/tsconfig.json b/services/collaboration/tsconfig.json new file mode 100644 index 0000000000..9dc9ec853d --- /dev/null +++ b/services/collaboration/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "CommonJS", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "moduleResolution": "node", + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "checkJs": false + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/services/match/.env.sample b/services/match/.env.sample index 86bc9196be..c88c405e05 100644 --- a/services/match/.env.sample +++ b/services/match/.env.sample @@ -4,4 +4,7 @@ DB_CLOUD_URI= DB_LOCAL_URI=mongodb://match-db:27017/match DB_USERNAME=user DB_PASSWORD=password -PORT=8083 \ No newline at end of file +BROKER_URL=amqp://broker:5672 +JWT_SECRET=you-can-replace-this-with-your-own-secret +PORT=8083 +NODE_ENV=development \ No newline at end of file diff --git a/services/match/README.md b/services/match/README.md index f352e82881..9222006b42 100644 --- a/services/match/README.md +++ b/services/match/README.md @@ -71,46 +71,6 @@ docker compose down -v --- -### Update Match Request - -- This endpoint updates the validity of the existing match request to 1 minute. -- **HTTP Method**: `PUT` -- **Endpoint**: http://localhost:8083/match/request/{requestId} -- **Parameters** - - `requestId` (Required) - The request ID of the match request. - - Example: `http://localhost:8083/match/request/6706b5d706ecde0138ca27a9` -- **Headers** - - `Authorization: Bearer ` (Required) - - The endpoint requires the user to include a JWT (JSON Web Token) in the HTTP Request Header for authentication and authorization. This token is generated during the authentication process (i.e., login) and contains information about the user's identity. The server verifies this token to ensure that the client is authorized to access the data. -- **Responses** - - | Response Code | Explanation | - |-----------------------------|-----------------------------------------------------------------| - | 200 (OK) | The match request is updated successfully. | - | 404 (Not Found) | The match request with the specified `requestId` was not found. | - | 500 (Internal Server Error) | Unexpected error in the database or server. | - - ```json - { - "status": "Success", - "message": "Match request updated successfully", - "data": { - "userId": "6713d1778986bf54b29bd0f8", - "username": "user123", - "topics": [ - "Algorithms", - "Arrays" - ], - "difficulty": "Hard", - "_id": "6714d1806da8e6d033ac2be1", - "createdAt": "2024-10-20T09:46:40.877Z", - "updatedAt": "2024-10-20T09:49:57.332Z" - } - } - ``` - ---- - ### Delete Match Request - This endpoint deletes the match request. @@ -359,4 +319,23 @@ docker compose down -v "requestId2": "67144180cda8e610333e4b12", "collabId": "676e7c9a028e8780b3a73a58", } + ``` + +--- + +### Match Failed Consumer + +- This consumer marks the match as failed. +- **Queue**: `MATCH_FAILED` - This message is emitted when a match fails due to unexpected errors. +- **Data Consumed** + - `requestId1` - The first request ID associated with the match failure. + - `requestId2` - The second request ID associated with the match failure. + - `reason` - The error encountered. + + ```json + { + "requestId1": "6714d1806da8e6d033ac2be1", + "requestId2": "67144180cda8e610333e4b12", + "reason": "Failed to create room", + } ``` \ No newline at end of file diff --git a/services/match/package-lock.json b/services/match/package-lock.json index 5eb089a86e..286f4156bd 100644 --- a/services/match/package-lock.json +++ b/services/match/package-lock.json @@ -10,22 +10,22 @@ "license": "ISC", "dependencies": { "amqplib": "^0.10.4", - "axios": "^1.7.7", "body-parser": "^1.20.3", "cors": "^2.8.5", "express": "^4.21.0", - "joi": "^17.13.3", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.7.0", - "morgan": "^1.10.0" + "morgan": "^1.10.0", + "zod": "^3.23.8", + "zod-validation-error": "^3.4.0" }, "devDependencies": { "@eslint/js": "^9.10.0", "@types/amqplib": "^0.10.5", - "@types/axios": "^0.9.36", "@types/cors": "^2.8.17", "@types/eslint__js": "^8.42.3", "@types/express": "^4.17.21", - "@types/joi": "^17.2.2", + "@types/jsonwebtoken": "^9.0.7", "@types/mongoose": "^5.11.96", "@types/morgan": "^1.9.9", "@types/node": "^22.5.4", @@ -258,21 +258,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -451,27 +436,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", - "license": "BSD-3-Clause" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "license": "BSD-3-Clause" - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -517,13 +481,6 @@ "@types/node": "*" } }, - "node_modules/@types/axios": { - "version": "0.9.36", - "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.9.36.tgz", - "integrity": "sha512-NLOpedx9o+rxo/X5ChbdiX6mS1atE4WHmEEIcR9NLenRVa5HoVjAvjafwU3FPTqnZEstpoqCaW7fagqSoTDNeg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -616,16 +573,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/joi": { - "version": "17.2.2", - "resolved": "https://registry.npmjs.org/@types/joi/-/joi-17.2.2.tgz", - "integrity": "sha512-vPvPwxn0Y4pQyqkEcMCJYxXCMYcrHqdfFX4SpF4zcqYioYexmDyxtM3OK+m/ZwGBS8/dooJ0il9qCwAdd6KFtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "joi": "*" - } - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -633,6 +580,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -1184,23 +1141,6 @@ "node": ">=8" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1296,6 +1236,12 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-more-ints": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", @@ -1425,18 +1371,6 @@ "dev": true, "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", @@ -1564,15 +1498,6 @@ "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==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1635,6 +1560,15 @@ "node": ">=6.0.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2114,40 +2048,6 @@ "dev": true, "license": "ISC" }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2563,19 +2463,6 @@ "dev": true, "license": "ISC" }, - "node_modules/joi": { - "version": "17.13.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", - "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2610,6 +2497,55 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", @@ -2666,6 +2602,42 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2673,6 +2645,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/loglevel": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", @@ -3773,12 +3751,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -3998,7 +3970,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4669,6 +4640,27 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz", + "integrity": "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.18.0" + } } } } diff --git a/services/match/package.json b/services/match/package.json index 9cc2dbc358..9a19897aec 100644 --- a/services/match/package.json +++ b/services/match/package.json @@ -14,22 +14,22 @@ "description": "", "dependencies": { "amqplib": "^0.10.4", - "axios": "^1.7.7", "body-parser": "^1.20.3", "cors": "^2.8.5", "express": "^4.21.0", - "joi": "^17.13.3", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.7.0", - "morgan": "^1.10.0" + "morgan": "^1.10.0", + "zod": "^3.23.8", + "zod-validation-error": "^3.4.0" }, "devDependencies": { "@eslint/js": "^9.10.0", "@types/amqplib": "^0.10.5", - "@types/axios": "^0.9.36", "@types/cors": "^2.8.17", "@types/eslint__js": "^8.42.3", "@types/express": "^4.17.21", - "@types/joi": "^17.2.2", + "@types/jsonwebtoken": "^9.0.7", "@types/mongoose": "^5.11.96", "@types/morgan": "^1.9.9", "@types/node": "^22.5.4", diff --git a/services/match/src/app.ts b/services/match/src/app.ts index fb15a8c8b7..0106e46f5f 100644 --- a/services/match/src/app.ts +++ b/services/match/src/app.ts @@ -5,6 +5,7 @@ import router from './routes'; import matchRequestRouter from './routes/matchRequestRoutes'; import bodyParser from 'body-parser'; import { verifyAccessToken } from './middleware/jwt'; +import config from './config'; const app: Express = express(); @@ -15,7 +16,7 @@ app.use(bodyParser.json()); app.use( cors({ - origin: process.env.CORS_ORIGIN ?? true, + origin: config.CORS_ORIGIN, methods: ['GET', 'POST', 'DELETE', 'PUT', 'PATCH'], allowedHeaders: ['Origin', 'X-Request-With', 'Content-Type', 'Accept', 'Authorization'], }), diff --git a/services/match/src/config.ts b/services/match/src/config.ts new file mode 100644 index 0000000000..ecc3747819 --- /dev/null +++ b/services/match/src/config.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; + +const envSchema = z + .object({ + DB_USERNAME: z.string().trim().min(1), + DB_PASSWORD: z.string().trim().min(1), + DB_CLOUD_URI: z.string().trim().optional(), + DB_LOCAL_URI: z.string().trim().optional(), + BROKER_URL: z.string().url(), + NODE_ENV: z.enum(['development', 'production']).default('development'), + CORS_ORIGIN: z.union([z.string().url(), z.literal('*')]).default('*'), + JWT_SECRET: z.string().trim().min(32), + PORT: z.coerce.number().min(1024).default(8083), + }) + .superRefine((data, ctx) => { + const isUrl = z.string().url(); + const cloudRes = isUrl.safeParse(data.DB_CLOUD_URI); + const localRes = isUrl.safeParse(data.DB_LOCAL_URI); + if (data.NODE_ENV === 'production') { + cloudRes.error?.issues.forEach(i => ctx.addIssue({ ...i, path: ['DB_CLOUD_URI'] })); + } else if (data.NODE_ENV === 'development') { + localRes.error?.issues.forEach(i => ctx.addIssue({ ...i, path: ['DB_LOCAL_URI'] })); + } + }); + +const result = envSchema.safeParse(process.env); +if (!result.success) { + console.error('There is an error with the environment variables:', result.error.issues); + process.exit(1); +} + +const { NODE_ENV, DB_CLOUD_URI, DB_LOCAL_URI } = result.data; +const DB_URI = (NODE_ENV === 'production' ? DB_CLOUD_URI : DB_LOCAL_URI) as string; +const config = { ...result.data, DB_URI }; + +export default config; diff --git a/services/match/src/controllers/matchRequestController.ts b/services/match/src/controllers/matchRequestController.ts index 4600dc6940..9efc2b6c4d 100644 --- a/services/match/src/controllers/matchRequestController.ts +++ b/services/match/src/controllers/matchRequestController.ts @@ -4,12 +4,12 @@ import { isValidObjectId } from 'mongoose'; import { createMatchRequestSchema } from '../validation/matchRequestValidation'; import { createMatchRequest as _createMatchRequest, - findMatchRequestAndUpdate, findMatchRequestAndDelete, findMatchRequest, } from '../models/repository'; import { produceMatchUpdatedRequest } from '../events/producer'; import { getStatus } from '../models/matchRequestModel'; +import { fromError } from 'zod-validation-error'; /** * Creates a match request. @@ -17,13 +17,15 @@ import { getStatus } from '../models/matchRequestModel'; * @param res */ export const createMatchRequest = async (req: Request, res: Response) => { - const { error, value } = createMatchRequestSchema.validate(req.body); - if (error) { - return handleBadRequest(res, error.message); + const result = createMatchRequestSchema.safeParse(req.body); + + if (!result.success) { + const formattedError = fromError(result.error).toString(); + return handleBadRequest(res, formattedError); } const { id: userId, username } = req.user; - const { topics, difficulty } = value; + const { topics, difficulty } = result.data; try { const matchRequest = await _createMatchRequest(userId, username, topics, difficulty); await produceMatchUpdatedRequest(matchRequest.id, userId, username, topics, difficulty); @@ -34,33 +36,6 @@ export const createMatchRequest = async (req: Request, res: Response) => { } }; -/** - * Updates a match request. - * @param req - * @param res - */ -export const updateMatchRequest = async (req: Request, res: Response) => { - const id = req.params.id; - const { id: userId, username } = req.user; - try { - const matchRequest = await findMatchRequestAndUpdate(id, userId); - if (!matchRequest) { - return handleNotFound(res, `Request ${id} not found`); - } - await produceMatchUpdatedRequest( - matchRequest.id, - userId, - username, - matchRequest.topics, - matchRequest.difficulty, - ); - handleSuccess(res, 201, 'Match request updated successfully', matchRequest); - } catch (error) { - console.error('Error in updateMatchRequest:', error); - handleInternalError(res, 'Failed to update match request'); - } -}; - /** * Deletes a match request. * @param req diff --git a/services/match/src/events/broker.ts b/services/match/src/events/broker.ts index 21e1bd2802..a1078e2a6c 100644 --- a/services/match/src/events/broker.ts +++ b/services/match/src/events/broker.ts @@ -1,6 +1,5 @@ import client, { Channel, Connection } from 'amqplib'; - -// TODO: Add authentication +import config from '../config'; /** * Adapated from @@ -17,12 +16,12 @@ class MessageBroker { } try { - this.connection = await client.connect('amqp://match-broker'); + this.connection = await client.connect(config.BROKER_URL); console.log('Connected to RabbitMQ'); this.channel = await this.connection.createChannel(); this.connected = true; } catch (error) { - console.error('Failed to connect to RabbitMQ: ', error); + console.error('Failed to connect to RabbitMQ:', error); throw error; } } @@ -35,7 +34,7 @@ class MessageBroker { this.channel.sendToQueue(queue, Buffer.from(JSON.stringify(message))); } catch (error) { - console.error('Failed to produce message: ', error); + console.error('Failed to produce message:', error); throw error; } } @@ -51,7 +50,7 @@ class MessageBroker { this.channel.consume( queue, msg => { - if (!msg) return console.error('Invalid message from queue ', queue); + if (!msg) return console.error('Invalid message from queue', queue); onMessage(JSON.parse(msg.content.toString()) as T); this.channel.ack(msg); @@ -59,7 +58,7 @@ class MessageBroker { { noAck: false }, ); } catch (error) { - console.error('Failed to consume message: ', error); + console.error('Failed to consume message:', error); throw error; } } diff --git a/services/match/src/events/consumer.ts b/services/match/src/events/consumer.ts index 5e3173f1bd..cbd2798ca6 100644 --- a/services/match/src/events/consumer.ts +++ b/services/match/src/events/consumer.ts @@ -1,9 +1,10 @@ import { findAndAssignCollab, + findAndMarkError, findMatchRequestAndAssignPair, findMatchRequestByIdAndAssignPair, } from '../models/repository'; -import { CollabCreatedEvent, MatchUpdatedEvent } from '../types/event'; +import { CollabCreatedEvent, MatchFailedEvent, MatchUpdatedEvent } from '../types/event'; import { logQueueStatus } from '../utils/logger'; import messageBroker from './broker'; import { produceMatchFound } from './producer'; @@ -43,10 +44,18 @@ async function consumeMatchUpdated(msg: MatchUpdatedEvent) { async function consumeCollabCreated(msg: CollabCreatedEvent) { const { requestId1, requestId2, collabId } = msg; + console.log(msg); await findAndAssignCollab(requestId1, requestId2, collabId); } +async function consumeMatchFailed(msg: MatchFailedEvent) { + console.log('Processing MatchFailedEvent:', msg); + const { requestId1, requestId2 } = msg; + await findAndMarkError(requestId1, requestId2); +} + export async function initializeConsumers() { messageBroker.consume(Queues.MATCH_REQUEST_UPDATED, consumeMatchUpdated); messageBroker.consume(Queues.COLLAB_CREATED, consumeCollabCreated); + messageBroker.consume(Queues.MATCH_FAILED, consumeMatchFailed); } diff --git a/services/match/src/events/producer.ts b/services/match/src/events/producer.ts index 080ea8cc2f..e217ef8da7 100644 --- a/services/match/src/events/producer.ts +++ b/services/match/src/events/producer.ts @@ -1,11 +1,12 @@ import { Difficulty } from '../models/matchRequestModel'; import { MatchUpdatedEvent, MatchFoundEvent } from '../types/event'; +import { IdType } from '../types/request'; import messageBroker from './broker'; import { Queues } from './queues'; export async function produceMatchUpdatedRequest( - requestId: string, - userId: string, + requestId: IdType, + userId: IdType, username: string, topics: string[], difficulty: Difficulty, diff --git a/services/match/src/events/queues.ts b/services/match/src/events/queues.ts index fdbb36d6b7..87a52f366f 100644 --- a/services/match/src/events/queues.ts +++ b/services/match/src/events/queues.ts @@ -1,5 +1,7 @@ export enum Queues { MATCH_REQUEST_UPDATED = 'MATCH_REQUEST_UPDATED', MATCH_FOUND = 'MATCH_FOUND', + QUESTION_FOUND = 'QUESTION_FOUND', COLLAB_CREATED = 'COLLAB_CREATED', + MATCH_FAILED = 'MATCH_FAILED', } diff --git a/services/match/src/index.ts b/services/match/src/index.ts index 60fed0b15a..f9e96ffd64 100644 --- a/services/match/src/index.ts +++ b/services/match/src/index.ts @@ -1,9 +1,10 @@ import app from './app'; +import config from './config'; import messageBroker from './events/broker'; import { initializeConsumers } from './events/consumer'; import { connectToDB } from './models/repository'; -const port = process.env.PORT || 8083; +const port = config.PORT; connectToDB() .then(() => console.log('MongoDB connected successfully')) diff --git a/services/match/src/middleware/jwt.ts b/services/match/src/middleware/jwt.ts index 371c9c31e2..660766b68d 100644 --- a/services/match/src/middleware/jwt.ts +++ b/services/match/src/middleware/jwt.ts @@ -1,7 +1,8 @@ +import jwt from 'jsonwebtoken'; import { NextFunction, Request, Response } from 'express'; -import { handleInternalError, handleUnauthorized } from '../utils/responses'; -import { VerifyTokenResponse } from '../types/response'; -import axios from 'axios'; +import { handleUnauthorized } from '../utils/responses'; +import config from '../config'; +import { userSchema } from '../types/request'; export async function verifyAccessToken(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers['authorization']; @@ -9,20 +10,17 @@ export async function verifyAccessToken(req: Request, res: Response, next: NextF handleUnauthorized(res, 'Authenticated failed'); return; } - try { - const response = await axios.get('http://user:8082/auth/verify-token', { - headers: { authorization: authHeader }, - }); - req.user = response.data.data; - next(); - } catch (error: any) { - if (error?.response?.status == 401) { - handleUnauthorized(res); + + const token = authHeader.split(' ')[1]; + + jwt.verify(token, config.JWT_SECRET, async (err, user) => { + const result = userSchema.safeParse(user); + if (err || result.error) { + handleUnauthorized(res, 'Authentication failed'); return; } - console.error(error); - handleInternalError(res); - return; - } + req.user = result.data; + next(); + }); } diff --git a/services/match/src/models/matchRequestModel.ts b/services/match/src/models/matchRequestModel.ts index 54c8ed1de4..14d3c6c531 100644 --- a/services/match/src/models/matchRequestModel.ts +++ b/services/match/src/models/matchRequestModel.ts @@ -11,6 +11,7 @@ export enum MatchRequestStatus { PENDING = 'PENDING', TIME_OUT = 'TIME_OUT', MATCH_FOUND = 'MATCH_FOUND', + MATCH_FAILED = 'MATCH_FAILED', COLLAB_CREATED = 'COLLAB_CREATED', } @@ -24,6 +25,7 @@ export interface MatchRequest { updatedAt: Date; pairId: Types.ObjectId; collabId: Types.ObjectId; + hasError: boolean; } const matchRequestSchema = new Schema( @@ -53,6 +55,10 @@ const matchRequestSchema = new Schema( type: Schema.Types.ObjectId, required: false, }, + hasError: { + type: Boolean, + default: false, + }, }, { versionKey: false, timestamps: true }, ); @@ -60,11 +66,15 @@ const matchRequestSchema = new Schema( export const MatchRequestModel = model('MatchRequest', matchRequestSchema); export function getStatus(matchRequest: MatchRequest): MatchRequestStatus { - return matchRequest.collabId - ? MatchRequestStatus.COLLAB_CREATED - : matchRequest.pairId - ? MatchRequestStatus.MATCH_FOUND - : matchRequest.updatedAt >= oneMinuteAgo() - ? MatchRequestStatus.PENDING - : MatchRequestStatus.TIME_OUT; + if (matchRequest.hasError) { + return MatchRequestStatus.MATCH_FAILED; + } else if (matchRequest.collabId) { + return MatchRequestStatus.COLLAB_CREATED; + } else if (matchRequest.pairId) { + return MatchRequestStatus.MATCH_FOUND; + } else if (matchRequest.updatedAt >= oneMinuteAgo()) { + return MatchRequestStatus.PENDING; + } else { + return MatchRequestStatus.TIME_OUT; + } } diff --git a/services/match/src/models/repository.ts b/services/match/src/models/repository.ts index a0baee367d..6c3df0e002 100644 --- a/services/match/src/models/repository.ts +++ b/services/match/src/models/repository.ts @@ -1,24 +1,14 @@ -import mongoose, { Types } from 'mongoose'; +import mongoose from 'mongoose'; import { Difficulty, MatchRequestModel } from './matchRequestModel'; import { oneMinuteAgo } from '../utils/date'; - -type IdType = string | Types.ObjectId; +import config from '../config'; +import { IdType } from '../types/request'; export async function connectToDB() { - const mongoURI = process.env.NODE_ENV === 'production' ? process.env.DB_CLOUD_URI : process.env.DB_LOCAL_URI; - - console.log('MongoDB URI:', mongoURI); - - if (!mongoURI) { - throw new Error('MongoDB URI not specified'); - } else if (!process.env.DB_USERNAME || !process.env.DB_PASSWORD) { - throw Error('MongoDB credentials not specified'); - } - - await mongoose.connect(mongoURI, { + await mongoose.connect(config.DB_URI, { authSource: 'admin', - user: process.env.DB_USERNAME, - pass: process.env.DB_PASSWORD, + user: config.DB_USERNAME, + pass: config.DB_PASSWORD, }); } @@ -26,14 +16,6 @@ export async function createMatchRequest(userId: IdType, username: string, topic return await new MatchRequestModel({ userId, username, topics, difficulty }).save(); } -export async function findMatchRequestAndUpdate(id: IdType, userId: IdType) { - return await MatchRequestModel.findOneAndUpdate( - { _id: id, userId, pairId: null }, - { $set: { updatedAt: Date.now() } }, - { new: true }, - ); -} - export async function findMatchRequestAndDelete(id: IdType, userId: IdType) { return await MatchRequestModel.findOneAndDelete({ _id: id, userId, updatedAt: { $gte: oneMinuteAgo() } }); } @@ -73,3 +55,7 @@ export async function findMatchRequestByIdAndAssignPair(id: IdType, pairId: IdTy export async function findAndAssignCollab(requestId1: IdType, requestId2: IdType, collabId: IdType) { await MatchRequestModel.updateMany({ _id: { $in: [requestId1, requestId2] } }, { $set: { collabId } }); } + +export async function findAndMarkError(requestId1: IdType, requestId2: IdType) { + await MatchRequestModel.updateMany({ _id: { $in: [requestId1, requestId2] } }, { $set: { hasError: true } }); +} diff --git a/services/match/src/routes/matchRequestRoutes.ts b/services/match/src/routes/matchRequestRoutes.ts index 543b1a003e..b2ab52c584 100644 --- a/services/match/src/routes/matchRequestRoutes.ts +++ b/services/match/src/routes/matchRequestRoutes.ts @@ -1,14 +1,8 @@ import express from 'express'; -import { - createMatchRequest, - deleteMatchRequest, - retrieveMatchRequest, - updateMatchRequest, -} from '../controllers/matchRequestController'; +import { createMatchRequest, deleteMatchRequest, retrieveMatchRequest } from '../controllers/matchRequestController'; const router = express.Router(); router.post('', createMatchRequest); -router.put('/:id', updateMatchRequest); router.delete('/:id', deleteMatchRequest); router.get('/:id', retrieveMatchRequest); diff --git a/services/match/src/types/event.ts b/services/match/src/types/event.ts index 5c230103f6..5319cea581 100644 --- a/services/match/src/types/event.ts +++ b/services/match/src/types/event.ts @@ -32,3 +32,9 @@ export interface CollabCreatedEvent { requestId2: Types.ObjectId | string; collabId: Types.ObjectId | string; } + +export interface MatchFailedEvent { + requestId1: Types.ObjectId | string; + requestId2: Types.ObjectId | string; + reason: string; +} diff --git a/services/match/src/types/express.d.ts b/services/match/src/types/express.d.ts index ca510382a8..92949f41ce 100644 --- a/services/match/src/types/express.d.ts +++ b/services/match/src/types/express.d.ts @@ -1,6 +1,6 @@ // https://blog.logrocket.com/extend-express-request-object-typescript/ -import { RequestUser } from '../custom'; +import { RequestUser } from './request'; export {}; diff --git a/services/match/src/types/request.ts b/services/match/src/types/request.ts index 6644aae8b7..4f4a1a509b 100644 --- a/services/match/src/types/request.ts +++ b/services/match/src/types/request.ts @@ -1,8 +1,21 @@ import { Types } from 'mongoose'; +import { z } from 'zod'; + +export type IdType = string | Types.ObjectId; + +enum Role { + Admin = 'admin', + User = 'user', +} export interface RequestUser { - id: Types.ObjectId | string; + id: IdType; username: string; - email: string; - isAdmin: boolean; + role: Role; } + +export const userSchema = z.object({ + id: z.string(), + username: z.string(), + role: z.nativeEnum(Role), +}); diff --git a/services/match/src/types/response.ts b/services/match/src/types/response.ts deleted file mode 100644 index d5a63921ce..0000000000 --- a/services/match/src/types/response.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { MatchRequest, MatchRequestStatus } from '../models/matchRequestModel'; -import { RequestUser } from './request'; - -export interface BaseResponse { - status: string; - message: string; -} - -export interface VerifyTokenResponse extends BaseResponse { - data: RequestUser; -} - -export interface MatchRequestWithStatus extends MatchRequest { - status: MatchRequestStatus; -} diff --git a/services/match/src/utils/logger.ts b/services/match/src/utils/logger.ts index aadd5bb638..1e015b758c 100644 --- a/services/match/src/utils/logger.ts +++ b/services/match/src/utils/logger.ts @@ -10,5 +10,5 @@ export async function logQueueStatus(): Promise { updatedAt: r.updatedAt, })); - console.log('Current Queue Status: ', queueStatus); + console.log('Current Queue Status:', queueStatus); } diff --git a/services/match/src/validation/matchRequestValidation.ts b/services/match/src/validation/matchRequestValidation.ts index 9c3aa4d767..7450b64816 100644 --- a/services/match/src/validation/matchRequestValidation.ts +++ b/services/match/src/validation/matchRequestValidation.ts @@ -1,9 +1,7 @@ -import Joi from 'joi'; +import { z } from 'zod'; import { Difficulty } from '../models/matchRequestModel'; -export const createMatchRequestSchema = Joi.object({ - topics: Joi.array().items(Joi.string()).min(1).required(), - difficulty: Joi.string() - .valid(...Object.values(Difficulty)) - .required(), +export const createMatchRequestSchema = z.object({ + topics: z.array(z.string().min(1)).min(1), + difficulty: z.nativeEnum(Difficulty), }); diff --git a/services/question/.env.sample b/services/question/.env.sample index 5613b8f4a7..7e9a506d59 100644 --- a/services/question/.env.sample +++ b/services/question/.env.sample @@ -4,6 +4,8 @@ DB_CLOUD_URI= DB_LOCAL_URI=mongodb://question-db:27017/question DB_USERNAME=user DB_PASSWORD=password +BROKER_URL=amqp://broker:5672 +JWT_SECRET=you-can-replace-this-with-your-own-secret CORS_ORIGIN=* PORT=8081 NODE_ENV=development \ No newline at end of file diff --git a/services/question/README.md b/services/question/README.md index 4ab0a987fb..557b507eb7 100644 --- a/services/question/README.md +++ b/services/question/README.md @@ -21,7 +21,11 @@ docker compose up -d docker compose down -v ``` -## Get Questions +--- + +## Endpoints + +### Get Questions This endpoint allows the retrieval of all the questions in the database. If filter by (optional) parameters, questions that matches with parameters will be returned; if no parameters are provided, all questions will be returned. @@ -29,21 +33,21 @@ that matches with parameters will be returned; if no parameters are provided, al - **HTTP Method**: `GET` - **Endpoint**: `/questions` -### Parameters: +#### Parameters: - `title` (Optional) - Filter by question title. - `description` (Optional) - Filter by question description. - `topics` (Optional) - Filter by topics associated with the questions. - `difficulty` (Optional) - Filter by question difficulty. -### Responses: +#### Responses: | Response Code | Explanation | |-----------------------------|-----------------------------------------------------------------------------------------------------------------| | 200 (OK) | Success, all questions are returned. If no questions match the optional parameters, an empty array is returned. | | 500 (Internal Server Error) | Unexpected error in the database or server. | -### Command Line Example: +#### Command Line Example: ``` Retrieve all Questions: @@ -68,12 +72,12 @@ Retrieve Questions by Title, Description, Topics, and Difficulty: curl -X GET "http://localhost:8081/questions?title=Reverse%20a%20String&description=string&topics=Algorithms&difficulty=Easy" ``` -### Parameter Format Details: +#### Parameter Format Details: The `topics` parameter must be passed as a comma-separated string in `GET` request because there is limitation with URL encoding and readability concerns. -### Example of Response Body for Success: +#### Example of Response Body for Success: ```json { @@ -108,18 +112,18 @@ encoding and readability concerns. --- -## Get Question by ID +### Get Question by ID This endpoint allows the retrieval of the question by using the question ID. - **HTTP Method**: `GET` - **Endpoint**: `/questions/{id}` -### Parameters: +#### Parameters: - `id` (Required) - The ID of the question to retrieve. -### Responses: +#### Responses: | Response Code | Explanation | |-----------------------------|----------------------------------------------------------| @@ -127,14 +131,14 @@ This endpoint allows the retrieval of the question by using the question ID. | 404 (Not Found) | Question with the specified `id` not found. | | 500 (Internal Server Error) | Unexpected error in the database or server. | -### Command Line Example: +#### Command Line Example: ``` Retrieve Question by ID: curl -X GET http://localhost:8081/questions/1 ``` -### Example of Response Body for Success: +#### Example of Response Body for Success: ```json { @@ -156,22 +160,22 @@ curl -X GET http://localhost:8081/questions/1 --- -## Get Question by Parameters (Random) +### Get Question by Parameters (Random) This endpoint allows the retrieval of random questions that matches the parameters provided. - **HTTP Method**: `GET` - **Endpoint**: `/questions/search` -### Parameters: +#### Parameters: - `limit` (Optional) - The number of questions to be returned. If not provided, default limit is 1. - `topics` (Required) - The topic of the question. - `difficulty` (Required) - The difficulty of the question. -### Responses: +#### Responses: -### Responses: +#### Responses: | Response Code | Explanation | |-----------------------------|-------------------------------------------------------------------------------------------------------------| @@ -179,7 +183,7 @@ This endpoint allows the retrieval of random questions that matches the paramete | 400 (Bad Request) | The request is missing required parameters or the parameters are invalid. | | 500 (Internal Server Error) | Unexpected error in the database or server. | -### Command Line Example: +#### Command Line Example: ``` Retrieve Random Question by Topics and Difficulty: @@ -189,7 +193,7 @@ Retrieve Random Question by Topics, Difficulty, and Limit: curl -X GET "http://localhost:8081/questions/search?topics=Algorithms,Data%20Structures&difficulty=Easy&limit=5" ``` -### Example of Response Body for Success: +#### Example of Response Body for Success: ```json { @@ -234,28 +238,28 @@ curl -X GET "http://localhost:8081/questions/search?topics=Algorithms,Data%20Str --- -## Get Topics +### Get Topics This endpoint retrieves all unique topics in the database - **HTTP Method**: `GET` - **Endpoint**: `/questions/topics` -### Responses: +#### Responses: | Response Code | Explanation | |-----------------------------|---------------------------------------------------------------------| | 200 (OK) | Success, all topics are returned. | | 500 (Internal Server Error) | The server encountered an error and could not complete the request. | -### Command Line Example: +#### Command Line Example: ``` Retrieve Topics: curl -X GET http://localhost:8081/questions/topics ``` -### Example of Response Body for Success: +#### Example of Response Body for Success: ```json { @@ -276,7 +280,7 @@ curl -X GET http://localhost:8081/questions/topics --- -## Add Question +### Add Question This endpoint allows the addition of a new question. The `id` is now automatically generated by the system to ensure uniqueness. @@ -284,14 +288,14 @@ uniqueness. - **HTTP Method**: `POST` - **Endpoint**: `/questions` -### Request Body: +#### Request Body: - `title` (Required) - The title of the question. - `description` (Required) - A description of the question. - `topics` (Required) - The topics associated with the question. - `difficulty` (Required) - The difficulty level of the question. -### Responses: +#### Responses: | Response Code | Explanation | |-----------------------------|---------------------------------------------------------------------| @@ -299,14 +303,14 @@ uniqueness. | 400 (Bad Request) | Required fields are missing or invalid, or question already exists. | | 500 (Internal Server Error) | Unexpected error in the database or server. | -### Command Line Example: +#### Command Line Example: ``` Add Question: curl -X POST http://localhost:8081/questions -H "Content-Type: application/json" -d "{\"title\": \"New Question\", \"description\": \"This is a description for a new question.\", \"topics\": [\"Data Structures\", \"Algorithms\"], \"difficulty\": \"Medium\"}" ``` -### Example of Response Body for Success: +#### Example of Response Body for Success: ```json { @@ -325,25 +329,25 @@ curl -X POST http://localhost:8081/questions -H "Content-Type: application/json" --- -## Update Question +### Update Question This endpoint allows updating an existing question. Only the title, description, topics, and difficulty can be updated. - **HTTP Method**: `PUT` - **Endpoint**: `/questions/{id}` -### Request Parameters: +#### Request Parameters: - `id` (Required) - The ID of the question to update. -### Request Body: +#### Request Body: - `title` (Optional) - New title for the question. - `description` (Optional) - New description for the question. - `topics` (Optional) - New topics for the question. - `difficulty` (Optional) - New difficulty level for the question. -### Responses: +#### Responses: | Response Code | Explanation | |-----------------------------|------------------------------------------------------------------------------------| @@ -352,14 +356,14 @@ This endpoint allows updating an existing question. Only the title, description, | 404 (Not Found) | Question with the specified `id` not found. | | 500 (Internal Server Error) | Unexpected error in the database or server. | -### Command Line Example: +#### Command Line Example: ``` Update Question: curl -X PUT http://localhost:8081/questions/21 -H "Content-Type: application/json" -d "{\"title\": \"Updated Question Title\", \"description\": \"This is the updated description.\", \"topics\": [\"Updated Topic\"], \"difficulty\": \"Hard\"}" ``` -### Example of Response Body for Success: +#### Example of Response Body for Success: ```json { @@ -378,18 +382,18 @@ curl -X PUT http://localhost:8081/questions/21 -H "Content-Type: application/jso --- -## Delete Question +### Delete Question This endpoint allows the deletion of a question by the question ID. - **HTTP Method**: `DELETE` - **Endpoint**: `/questions/{id}` -### Parameters: +#### Parameters: - `id` (Required) - The ID of the question to delete. -### Responses: +#### Responses: | Response Code | Explanation | |-----------------------------|------------------------------------------------| @@ -397,14 +401,14 @@ This endpoint allows the deletion of a question by the question ID. | 404 (Not Found) | Question with the specified `id` not found. | | 500 (Internal Server Error) | Unexpected error in the database or server. | -### Command Line Example: +#### Command Line Example: ``` Delete Question: curl -X DELETE http://localhost:8081/questions/21 ``` -### Example of Response Body for Success: +#### Example of Response Body for Success: ```json { @@ -423,18 +427,18 @@ curl -X DELETE http://localhost:8081/questions/21 --- -## Delete Questions +### Delete Questions This endpoint allows the deletion of multiple questions by their question IDs. - **HTTP Method**: `POST` - **Endpoint**: `/questions/delete` -### Parameters: +#### Parameters: - `ids` (Required) - An array of integers representing the IDs of the questions to delete, e.g. `[1, 2, 3]`. -### Responses: +#### Responses: | Response Code | Explanation | |-----------------------------|------------------------------------------------------| @@ -443,13 +447,14 @@ This endpoint allows the deletion of multiple questions by their question IDs. | 404 (Not Found) | A question with the specified id not found. | | 500 (Internal Server Error) | Unexpected error in the database or server. | -### Command Line Example: +#### Command Line Example: ``` +Delete Questions: curl -X POST http://localhost:8081/questions/delete -H "Content-Type: application/json" -d '{"ids": [21, 22]}' ``` -### Example of Response Body for Success: +#### Example of Response Body for Success: ```json { @@ -460,3 +465,88 @@ curl -X POST http://localhost:8081/questions/delete -H "Content-Type: applicatio ``` --- + +## Producers + +### Question Found Producer + +- This producer emits a message when a question has been successfully found for a match. +- **Queue**: `QUESTION_FOUND` +- **Data Produced** + - `user1` - The first user associated with the successful match. + - `user2` - The second user associated with the successful match. + - `question` - The question assigned to the successful match. + + ```json + { + "user1": { + "id": "6713d1778986bf54b29bd0f8", + "username": "user123", + "requestId": "6714d1806da8e6d033ac2be1", + }, + "user2": { + "id": "6713d17f8986bf54b29bd0fe", + "username": "userabc", + "requestId": "6714dab233a91c7f7c9b9b15", + }, + "question": { + "_id": "66f77e7bf9530832bd839239", + "id": 21, + "title": "Reverse Integer", + "description": "Given a signed 32-bit integer x, return x with its digits reversed.", + "topics": ["Math"], + "difficulty": "Medium" + } + } + ``` + +--- + +### Match Failed Producer + +- This producer emits a message when a question could not be found for a match. +- **Queue**: `MATCH_FAILED` +- **Data Produced** + - `requestId1` - The first request ID associated with the match failure. + - `requestId2` - The second request ID associated with the match failure. + - `reason` - The error encountered. + + ```json + { + "requestId1": "6714d1806da8e6d033ac2be1", + "requestId2": "67144180cda8e610333e4b12", + "reason": "No questions were found", + } + ``` + +--- + +## Consumers + +### Match Found Consumer + +- This consumer attempts to find and assign a compatible question. +- Upon successfully finding a question, it produces a `QUESTION_FOUND` message. +- **Queue**: `MATCH_FOUND` - This message is emitted when a match is found between two match requests.s +- **Data Consumed** + - `user1` - The first user associated with the match request. + - `user2` - The second user associated with the match request. + - `topics` - The topics in common between the two requests. + - `difficulty` - The difficulty of the match request. + + ```json + { + "user1": { + "id": "6713d1778986bf54b29bd0f8", + "username": "user123", + "requestId": "6714d1806da8e6d033ac2be1", + }, + "user2": { + "id": "6714d1806da8e6d033ac2be1", + "username": "userabc", + "requestId": "6713d1778986bf54b29bd0f8", + }, + "topics": [ "Algorithms", "Arrays" ], + "difficulty": "Hard" + } + ``` diff --git a/services/question/package-lock.json b/services/question/package-lock.json index 9c6f712439..5fa3d89b89 100644 --- a/services/question/package-lock.json +++ b/services/question/package-lock.json @@ -9,17 +9,22 @@ "version": "0.0.0", "license": "ISC", "dependencies": { + "amqplib": "^0.10.4", "body-parser": "^1.20.3", "cors": "^2.8.5", "express": "^4.21.0", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.6.2", - "morgan": "^1.10.0" + "morgan": "^1.10.0", + "zod": "^3.23.8" }, "devDependencies": { "@eslint/js": "^9.10.0", + "@types/amqplib": "^0.10.5", "@types/cors": "^2.8.17", "@types/eslint__js": "^8.42.3", "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.7", "@types/morgan": "^1.9.9", "@types/node": "^22.5.4", "@typescript-eslint/eslint-plugin": "^8.5.0", @@ -34,6 +39,49 @@ "typescript-eslint": "^8.5.0" } }, + "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/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -421,6 +469,16 @@ "dev": true, "license": "MIT" }, + "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/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -520,6 +578,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -976,6 +1044,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-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1141,6 +1224,18 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/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", @@ -1317,6 +1412,12 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "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", @@ -1447,6 +1548,15 @@ "node": ">=6.0.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2328,6 +2438,12 @@ "node": ">=8" } }, + "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", @@ -2369,6 +2485,55 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", @@ -2425,6 +2590,42 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2432,6 +2633,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/loglevel": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", @@ -3563,6 +3770,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", @@ -3615,6 +3828,18 @@ "dev": true, "license": "MIT" }, + "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/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -3635,6 +3860,12 @@ "dev": true, "license": "MIT" }, + "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-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3727,7 +3958,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3901,6 +4131,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/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -4182,6 +4418,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", @@ -4382,6 +4628,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/services/question/package.json b/services/question/package.json index f7a57cd3d3..d05d67f61b 100644 --- a/services/question/package.json +++ b/services/question/package.json @@ -4,8 +4,8 @@ "main": "index.js", "scripts": { "build": "npx tsc", - "start": "npm run build && node dist/index.js", - "dev": "nodemon src/index.ts", + "start": "npm run build && node ./dist/index.js", + "dev": "nodemon --files ./src/index.ts", "lint": "npx eslint .", "lint:fix": "npx eslint . --fix" }, @@ -13,17 +13,22 @@ "license": "ISC", "description": "", "dependencies": { + "amqplib": "^0.10.4", "body-parser": "^1.20.3", "cors": "^2.8.5", "express": "^4.21.0", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.6.2", - "morgan": "^1.10.0" + "morgan": "^1.10.0", + "zod": "^3.23.8" }, "devDependencies": { "@eslint/js": "^9.10.0", + "@types/amqplib": "^0.10.5", "@types/cors": "^2.8.17", "@types/eslint__js": "^8.42.3", "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.7", "@types/morgan": "^1.9.9", "@types/node": "^22.5.4", "@typescript-eslint/eslint-plugin": "^8.5.0", diff --git a/services/question/src/app.ts b/services/question/src/app.ts index 53a5b4acc0..6bc6da6bcf 100644 --- a/services/question/src/app.ts +++ b/services/question/src/app.ts @@ -4,6 +4,7 @@ import cors from 'cors'; import router from './routes'; import questionRouter from './routes/questionRoutes'; import bodyParser from 'body-parser'; +import config from './config'; const app: Express = express(); @@ -14,7 +15,7 @@ app.use(bodyParser.json()); app.use( cors({ - origin: process.env.CORS_ORIGIN ?? true, + origin: config.CORS_ORIGIN, methods: ['GET', 'POST', 'DELETE', 'PUT', 'PATCH'], allowedHeaders: ['Origin', 'X-Request-With', 'Content-Type', 'Accept', 'Authorization'], }), diff --git a/services/question/src/config.ts b/services/question/src/config.ts new file mode 100644 index 0000000000..83e9cee4e3 --- /dev/null +++ b/services/question/src/config.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; + +const envSchema = z + .object({ + DB_USERNAME: z.string().trim().min(1), + DB_PASSWORD: z.string().trim().min(1), + DB_CLOUD_URI: z.string().trim().optional(), + DB_LOCAL_URI: z.string().trim().optional(), + BROKER_URL: z.string().url(), + NODE_ENV: z.enum(['development', 'production']).default('development'), + CORS_ORIGIN: z.union([z.string().url(), z.literal('*')]).default('*'), + JWT_SECRET: z.string().trim().min(32), + PORT: z.coerce.number().min(1024).default(8081), + }) + .superRefine((data, ctx) => { + const isUrl = z.string().url(); + const cloudRes = isUrl.safeParse(data.DB_CLOUD_URI); + const localRes = isUrl.safeParse(data.DB_LOCAL_URI); + if (data.NODE_ENV === 'production') { + cloudRes.error?.issues.forEach(i => ctx.addIssue({ ...i, path: ['DB_CLOUD_URI'] })); + } else if (data.NODE_ENV === 'development') { + localRes.error?.issues.forEach(i => ctx.addIssue({ ...i, path: ['DB_LOCAL_URI'] })); + } + }); + +const result = envSchema.safeParse(process.env); +if (!result.success) { + console.error('There is an error with the environment variables:', result.error.issues); + process.exit(1); +} + +const { NODE_ENV, DB_CLOUD_URI, DB_LOCAL_URI } = result.data; +const DB_URI = (NODE_ENV === 'production' ? DB_CLOUD_URI : DB_LOCAL_URI) as string; +const config = { ...result.data, DB_URI }; + +export default config; diff --git a/services/question/src/events/broker.ts b/services/question/src/events/broker.ts new file mode 100644 index 0000000000..a1078e2a6c --- /dev/null +++ b/services/question/src/events/broker.ts @@ -0,0 +1,68 @@ +import client, { Channel, Connection } from 'amqplib'; +import config from '../config'; + +/** + * Adapated from + * https://hassanfouad.medium.com/using-rabbitmq-with-nodejs-and-typescript-8b33d56a62cc + */ +class MessageBroker { + connection!: Connection; + channel!: Channel; + private connected = false; + + async connect(): Promise { + if (this.connection && this.channel) { + return; + } + + try { + this.connection = await client.connect(config.BROKER_URL); + console.log('Connected to RabbitMQ'); + this.channel = await this.connection.createChannel(); + this.connected = true; + } catch (error) { + console.error('Failed to connect to RabbitMQ:', error); + throw error; + } + } + + async produce(queue: string, message: any): Promise { + try { + if (!this.connected) { + await this.connect(); + } + + this.channel.sendToQueue(queue, Buffer.from(JSON.stringify(message))); + } catch (error) { + console.error('Failed to produce message:', error); + throw error; + } + } + + async consume(queue: string, onMessage: (message: T) => void): Promise { + try { + if (!this.connected) { + await this.connect(); + } + + await this.channel.assertQueue(queue, { durable: true }); + + this.channel.consume( + queue, + msg => { + if (!msg) return console.error('Invalid message from queue', queue); + + onMessage(JSON.parse(msg.content.toString()) as T); + this.channel.ack(msg); + }, + { noAck: false }, + ); + } catch (error) { + console.error('Failed to consume message:', error); + throw error; + } + } +} + +const messageBroker = new MessageBroker(); +export default messageBroker; diff --git a/services/question/src/events/consumer.ts b/services/question/src/events/consumer.ts new file mode 100644 index 0000000000..7a092c5811 --- /dev/null +++ b/services/question/src/events/consumer.ts @@ -0,0 +1,24 @@ +import { Question } from '../models/questionModel'; +import { MatchFoundEvent } from '../types/event'; +import messageBroker from './broker'; +import { produceMatchFailedEvent, produceQuestionFoundEvent } from './producer'; +import { Queues } from './queues'; + +async function consumeMatchFound(msg: MatchFoundEvent) { + console.log('Attempting to find questions:', msg); + + const { user1, user2, topics, difficulty } = msg; + const questions = await Question.find({ topics: { $in: topics }, difficulty }).exec(); + if (!questions.length) { + console.log('Failed to find questions:', msg); + await produceMatchFailedEvent(user1.requestId, user2.requestId); + return; + } + const randQuestion = questions[Math.floor(Math.random() * questions.length)]; + console.log('Questions found:', msg, randQuestion); + await produceQuestionFoundEvent(user1, user2, randQuestion); +} + +export async function initializeConsumers() { + messageBroker.consume(Queues.MATCH_FOUND, consumeMatchFound); +} diff --git a/services/question/src/events/producer.ts b/services/question/src/events/producer.ts new file mode 100644 index 0000000000..9d5d9b13c9 --- /dev/null +++ b/services/question/src/events/producer.ts @@ -0,0 +1,19 @@ +import { IdType, MatchFailedEvent, Question, QuestionFoundEvent, UserWithRequest } from '../types/event'; +import messageBroker from './broker'; +import { Queues } from './queues'; + +const QUESTION_FOUND_ERROR = 'No questions were found'; + +export async function produceQuestionFoundEvent(user1: UserWithRequest, user2: UserWithRequest, question: Question) { + const message: QuestionFoundEvent = { + user1, + user2, + question, + }; + await messageBroker.produce(Queues.QUESTION_FOUND, message); +} + +export async function produceMatchFailedEvent(requestId1: IdType, requestId2: IdType) { + const message: MatchFailedEvent = { requestId1, requestId2, reason: QUESTION_FOUND_ERROR }; + await messageBroker.produce(Queues.MATCH_FAILED, message); +} diff --git a/services/question/src/events/queues.ts b/services/question/src/events/queues.ts new file mode 100644 index 0000000000..87a52f366f --- /dev/null +++ b/services/question/src/events/queues.ts @@ -0,0 +1,7 @@ +export enum Queues { + MATCH_REQUEST_UPDATED = 'MATCH_REQUEST_UPDATED', + MATCH_FOUND = 'MATCH_FOUND', + QUESTION_FOUND = 'QUESTION_FOUND', + COLLAB_CREATED = 'COLLAB_CREATED', + MATCH_FAILED = 'MATCH_FAILED', +} diff --git a/services/question/src/index.ts b/services/question/src/index.ts index 4802d8777b..0271622d1c 100644 --- a/services/question/src/index.ts +++ b/services/question/src/index.ts @@ -1,9 +1,12 @@ import app from './app'; +import config from './config'; +import messageBroker from './events/broker'; +import { initializeConsumers } from './events/consumer'; import { connectToDB, upsertManyQuestions } from './models'; import { getDemoQuestions } from './utils/data'; import { initializeCounter } from './utils/sequence'; -const port = process.env.PORT || 8081; +const port = config.PORT; connectToDB() .then(async () => { @@ -19,6 +22,8 @@ connectToDB() console.log('Question ID initialized successfully'); app.listen(port, () => console.log(`Question service is listening on port ${port}.`)); }) + .then(async () => await messageBroker.connect()) + .then(async () => await initializeConsumers()) .catch(error => { console.error('Failed to start server'); console.error(error); diff --git a/services/question/src/middleware/jwt.ts b/services/question/src/middleware/jwt.ts new file mode 100644 index 0000000000..dec2175628 --- /dev/null +++ b/services/question/src/middleware/jwt.ts @@ -0,0 +1,34 @@ +import jwt from 'jsonwebtoken'; +import { NextFunction, Request, Response } from 'express'; +import config from '../config'; +import { Role, userSchema } from '../types/request'; +import { handleForbidden, handleUnauthorized } from '../utils/helpers'; + +export async function verifyAccessToken(req: Request, res: Response, next: NextFunction) { + const authHeader = req.headers['authorization']; + if (!authHeader) { + handleUnauthorized(res, 'Authenticated failed'); + return; + } + + const token = authHeader.split(' ')[1]; + + jwt.verify(token, config.JWT_SECRET, async (err, user) => { + const result = userSchema.safeParse(user); + if (err || result.error) { + handleUnauthorized(res, 'Authentication failed'); + return; + } + + req.user = result.data; + next(); + }); +} + +export function verifyIsAdmin(req: Request, res: Response, next: NextFunction) { + if (req.user.role === Role.Admin) { + next(); + return; + } + handleForbidden(res, 'Not authorized to access this resource'); +} diff --git a/services/question/src/models/index.ts b/services/question/src/models/index.ts index 90596e07ce..2d54e72f47 100644 --- a/services/question/src/models/index.ts +++ b/services/question/src/models/index.ts @@ -1,21 +1,12 @@ import mongoose from 'mongoose'; import { IQuestion, Question } from './questionModel'; +import config from '../config'; export async function connectToDB() { - const mongoURI = process.env.NODE_ENV === 'production' ? process.env.DB_CLOUD_URI : process.env.DB_LOCAL_URI; - - console.log('MongoDB URI:', mongoURI); - - if (!mongoURI) { - throw new Error('MongoDB URI not specified'); - } else if (!process.env.DB_USERNAME || !process.env.DB_PASSWORD) { - throw Error('MongoDB credentials not specified'); - } - - await mongoose.connect(mongoURI, { + await mongoose.connect(config.DB_URI, { authSource: 'admin', - user: process.env.DB_USERNAME, - pass: process.env.DB_PASSWORD, + user: config.DB_USERNAME, + pass: config.DB_PASSWORD, }); } diff --git a/services/question/src/routes/questionRoutes.ts b/services/question/src/routes/questionRoutes.ts index c48e61860b..2fcdb6343f 100644 --- a/services/question/src/routes/questionRoutes.ts +++ b/services/question/src/routes/questionRoutes.ts @@ -9,6 +9,7 @@ import { updateQuestion, deleteQuestions, } from '../controllers/questionController'; +import { verifyAccessToken, verifyIsAdmin } from '../middleware/jwt'; /** * Router for question endpoints. @@ -27,21 +28,21 @@ questionRouter.get('/:id', getQuestionById); /** * Add a new question to the database. */ -questionRouter.post('/', addQuestion); +questionRouter.post('/', verifyAccessToken, verifyIsAdmin, addQuestion); /** * Update a question in the database. */ -questionRouter.put('/:id', updateQuestion); +questionRouter.put('/:id', verifyAccessToken, verifyIsAdmin, updateQuestion); /** * Delete a question from the database. */ -questionRouter.delete('/:id', deleteQuestion); +questionRouter.delete('/:id', verifyAccessToken, verifyIsAdmin, deleteQuestion); /** * Delete questions from the database. */ -questionRouter.post('/delete', deleteQuestions); +questionRouter.post('/delete', verifyAccessToken, verifyIsAdmin, deleteQuestions); export default questionRouter; diff --git a/services/question/src/types/event.ts b/services/question/src/types/event.ts new file mode 100644 index 0000000000..52e6434a82 --- /dev/null +++ b/services/question/src/types/event.ts @@ -0,0 +1,40 @@ +import { Types } from 'mongoose'; + +export type IdType = string | Types.ObjectId; + +export enum Difficulty { + Easy = 'Easy', + Medium = 'Medium', + Hard = 'Hard', +} +export interface Question { + id: number; + description: string; + difficulty: Difficulty; + title: string; + topics?: string[]; +} +export interface UserWithRequest { + id: Types.ObjectId | string; + username: string; + email: string; + requestId: Types.ObjectId | string; +} +export interface MatchFoundEvent { + user1: UserWithRequest; + user2: UserWithRequest; + topics: string[]; + difficulty: Difficulty; +} + +export interface QuestionFoundEvent { + user1: UserWithRequest; + user2: UserWithRequest; + question: Question; +} + +export interface MatchFailedEvent { + requestId1: IdType; + requestId2: IdType; + reason: string; +} diff --git a/services/question/src/types/express.d.ts b/services/question/src/types/express.d.ts new file mode 100644 index 0000000000..92949f41ce --- /dev/null +++ b/services/question/src/types/express.d.ts @@ -0,0 +1,13 @@ +// https://blog.logrocket.com/extend-express-request-object-typescript/ + +import { RequestUser } from './request'; + +export {}; + +declare global { + namespace Express { + export interface Request { + user: RequestUser; + } + } +} diff --git a/services/question/src/types/request.ts b/services/question/src/types/request.ts new file mode 100644 index 0000000000..0fa4ce321e --- /dev/null +++ b/services/question/src/types/request.ts @@ -0,0 +1,21 @@ +import { Types } from 'mongoose'; +import { z } from 'zod'; + +export type IdType = string | Types.ObjectId; + +export enum Role { + Admin = 'admin', + User = 'user', +} + +export interface RequestUser { + id: IdType; + username: string; + role: Role; +} + +export const userSchema = z.object({ + id: z.string(), + username: z.string(), + role: z.nativeEnum(Role), +}); diff --git a/services/question/src/utils/helpers.ts b/services/question/src/utils/helpers.ts index 58b1dfcfce..b2f08f9a68 100644 --- a/services/question/src/utils/helpers.ts +++ b/services/question/src/utils/helpers.ts @@ -24,6 +24,30 @@ export const handleBadRequest = (res: Response, message = 'Bad Request') => { }); }; +/** + * Handles unauthorized requests and sends a 401 response with a custom message. + * @param res + * @param message + */ +export const handleUnauthorized = (res: Response, message = 'Unauthorized Request') => { + res.status(401).json({ + status: 'Error', + message, + }); +}; + +/** + * Handles forbidden requests and sends a 403 response with a custom message. + * @param res + * @param message + */ +export const handleForbidden = (res: Response, message = 'Forbidden Request') => { + res.status(403).json({ + status: 'Error', + message, + }); +}; + /** * Handles not found errors and sends a 404 response with a custom message. * @param res diff --git a/services/user/package-lock.json b/services/user/package-lock.json index e416db5e6c..509c3a52f4 100644 --- a/services/user/package-lock.json +++ b/services/user/package-lock.json @@ -16,7 +16,8 @@ "express": "^4.21.0", "http": "^0.0.1-security", "jsonwebtoken": "^9.0.2", - "mongoose": "^8.5.4" + "mongoose": "^8.5.4", + "zod": "^3.23.8" }, "devDependencies": { "@eslint/js": "^9.11.1", @@ -5115,6 +5116,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/services/user/package.json b/services/user/package.json index 55112d18b9..36a14097a2 100644 --- a/services/user/package.json +++ b/services/user/package.json @@ -45,6 +45,7 @@ "express": "^4.21.0", "http": "^0.0.1-security", "jsonwebtoken": "^9.0.2", - "mongoose": "^8.5.4" + "mongoose": "^8.5.4", + "zod": "^3.23.8" } } diff --git a/services/user/src/config.ts b/services/user/src/config.ts new file mode 100644 index 0000000000..7c8e2191e1 --- /dev/null +++ b/services/user/src/config.ts @@ -0,0 +1,34 @@ +import { z } from 'zod'; + +const envSchema = z + .object({ + DB_USERNAME: z.string().trim().min(1), + DB_PASSWORD: z.string().trim().min(1), + DB_CLOUD_URI: z.string().trim().optional(), + DB_LOCAL_URI: z.string().trim().optional(), + NODE_ENV: z.enum(['development', 'production']).default('development'), + JWT_SECRET: z.string().trim().min(32), + PORT: z.coerce.number().min(1024).default(8082), + }) + .superRefine((data, ctx) => { + const isUrl = z.string().url(); + const cloudRes = isUrl.safeParse(data.DB_CLOUD_URI); + const localRes = isUrl.safeParse(data.DB_LOCAL_URI); + if (data.NODE_ENV === 'production') { + cloudRes.error?.issues.forEach(i => ctx.addIssue({ ...i, path: ['DB_CLOUD_URI'] })); + } else if (data.NODE_ENV === 'development') { + localRes.error?.issues.forEach(i => ctx.addIssue({ ...i, path: ['DB_LOCAL_URI'] })); + } + }); + +const result = envSchema.safeParse(process.env); +if (!result.success) { + console.error('There is an error with the environment variables:', result.error.issues); + process.exit(1); +} + +const { NODE_ENV, DB_CLOUD_URI, DB_LOCAL_URI } = result.data; +const DB_URI = (NODE_ENV === 'production' ? DB_CLOUD_URI : DB_LOCAL_URI) as string; +const config = { ...result.data, DB_URI }; + +export default config; diff --git a/services/user/src/controller/auth-controller.ts b/services/user/src/controller/auth-controller.ts index 641061dcaa..98d06e1b98 100644 --- a/services/user/src/controller/auth-controller.ts +++ b/services/user/src/controller/auth-controller.ts @@ -4,6 +4,8 @@ import { findUserByUsername as _findUserByUsername } from '../model/repository'; import { formatUserResponse } from './user-controller'; import { Request, Response } from 'express'; import { handleBadRequest, handleInternalError, handleSuccess, handleUnauthorized } from '../utils/helper'; +import config from '../config'; +import { Role } from '../model/user-model'; export async function handleLogin(req: Request, res: Response) { const { username, password } = req.body; @@ -25,16 +27,16 @@ export async function handleLogin(req: Request, res: Response) { return; } - if (!process.env.JWT_SECRET) { - handleInternalError(res, 'JWT secret not specified'); - return; - } + const role = user.isAdmin ? Role.Admin : Role.User; + console.log({ id: user.id, username: user.username, role }); const accessToken = jwt.sign( { id: user.id, + username: user.username, + role, }, - process.env.JWT_SECRET, + config.JWT_SECRET, { expiresIn: '1d', }, diff --git a/services/user/src/middleware/basic-access-control.ts b/services/user/src/middleware/basic-access-control.ts index a9ea0e9702..11fc3a7c8f 100644 --- a/services/user/src/middleware/basic-access-control.ts +++ b/services/user/src/middleware/basic-access-control.ts @@ -1,7 +1,8 @@ import jwt from 'jsonwebtoken'; import { findUserById as _findUserById } from '../model/repository'; import { NextFunction, Request, Response } from 'express'; -import { handleForbidden, handleInternalError, handleUnauthorized } from '../utils/helper'; +import { handleForbidden, handleUnauthorized } from '../utils/helper'; +import config from '../config'; export function verifyAccessToken(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers['authorization']; @@ -13,12 +14,7 @@ export function verifyAccessToken(req: Request, res: Response, next: NextFunctio // request auth header: `Authorization: Bearer + ` const token = authHeader.split(' ')[1]; - if (!process.env.JWT_SECRET) { - handleInternalError(res, 'JWT Secret not provided'); - return; - } - - jwt.verify(token, process.env.JWT_SECRET, async (err, user) => { + jwt.verify(token, config.JWT_SECRET, async (err, user) => { if (err || typeof user !== 'object' || !user?.id) { handleUnauthorized(res, 'Authentication failed'); return; diff --git a/services/user/src/model/repository.ts b/services/user/src/model/repository.ts index 8559d1c227..c86f8b5cfa 100644 --- a/services/user/src/model/repository.ts +++ b/services/user/src/model/repository.ts @@ -1,18 +1,13 @@ import UserModel from './user-model'; import 'dotenv/config'; import { connect } from 'mongoose'; +import config from '../config'; export async function connectToDB() { - const mongoUri = process.env.NODE_ENV === 'production' ? process.env.DB_CLOUD_URI : process.env.DB_LOCAL_URI; - - if (!mongoUri) { - throw new Error('MongoDB URI not specified'); - } - - await connect(mongoUri, { + await connect(config.DB_URI, { authSource: 'admin', - user: process.env.DB_USERNAME, - pass: process.env.DB_PASSWORD, + user: config.DB_USERNAME, + pass: config.DB_PASSWORD, }); } diff --git a/services/user/src/model/user-model.ts b/services/user/src/model/user-model.ts index 9c22ba20db..0860017ce2 100644 --- a/services/user/src/model/user-model.ts +++ b/services/user/src/model/user-model.ts @@ -2,6 +2,11 @@ import mongoose, { Types } from 'mongoose'; const Schema = mongoose.Schema; +export enum Role { + Admin = 'admin', + User = 'user', +} + export interface User { id: Types.ObjectId; username: string; diff --git a/services/user/src/server.ts b/services/user/src/server.ts index e85572cc4c..474653552a 100644 --- a/services/user/src/server.ts +++ b/services/user/src/server.ts @@ -2,8 +2,9 @@ import http from 'http'; import index from './index'; import 'dotenv/config'; import { connectToDB } from './model/repository'; +import config from './config'; -const port = process.env.PORT || 8082; +const port = config.PORT; const server = http.createServer(index);