From 39ea2ffef31fa374c6ca0b64c07d1ce3f944f2bf Mon Sep 17 00:00:00 2001 From: Jessica Mulein Date: Sun, 29 Sep 2024 00:09:13 +0000 Subject: [PATCH] new theme, start ripping out auth0, bring in improvements from CurseFund --- .devcontainer/.env.example | 1 + .devcontainer/Dockerfile | 22 + .devcontainer/devcontainer.json | 91 +- .devcontainer/docker-compose.yml | 10 +- .eslintrc.json | 15 +- .github/dependabot.yml | 12 + .github/workflows/ci.yml | 64 + .github/workflows/qodana_code_quality.yml | 28 + chili-and-cilantro-api/.env.example | 12 +- chili-and-cilantro-api/src/application.ts | 199 + .../src/controllers/api/game.ts | 429 ++ .../src/controllers/api/user.ts | 18 +- .../src/controllers/base.ts | 112 +- chili-and-cilantro-api/src/environment.ts | 14 +- .../src/interfaces/bid-ingredient.ts | 6 + .../src/interfaces/environment.ts | 31 +- .../src/interfaces/game-action.ts | 9 + .../src/interfaces/game-chef.ts | 9 + chili-and-cilantro-api/src/main.ts | 52 +- chili-and-cilantro-api/src/middlewares.ts | 64 + .../src/middlewares/authenticate-token.ts | 21 +- chili-and-cilantro-api/src/routers/api.ts | 20 + chili-and-cilantro-api/src/routers/app.ts | 86 + chili-and-cilantro-api/src/routers/base.ts | 11 + chili-and-cilantro-api/src/routes/api.ts | 8 - chili-and-cilantro-api/src/routes/games.ts | 293 -- chili-and-cilantro-api/src/routes/users.ts | 44 - chili-and-cilantro-api/src/services/action.ts | 62 +- chili-and-cilantro-api/src/services/chef.ts | 45 +- chili-and-cilantro-api/src/services/game.ts | 151 +- chili-and-cilantro-api/src/services/jwt.ts | 8 +- chili-and-cilantro-api/src/services/player.ts | 33 +- .../src/services/request-user.ts | 6 +- chili-and-cilantro-api/src/services/user.ts | 71 +- .../src/services/utility.ts | 4 +- chili-and-cilantro-api/src/setup-database.ts | 18 - .../src/setup-middlewares.ts | 54 - chili-and-cilantro-api/src/setup-routes.ts | 13 - .../src/setup-static-react-app.ts | 15 - chili-and-cilantro-api/src/types/express.d.ts | 6 +- .../test/fixtures/action.ts | 314 +- chili-and-cilantro-api/test/fixtures/chef.ts | 27 +- .../test/fixtures/database.ts | 49 + chili-and-cilantro-api/test/fixtures/game.ts | 33 +- .../test/fixtures/jwksClient.ts | 20 - .../test/fixtures/mocked-model.ts | 11 + .../test/fixtures/objectId.ts | 6 + chili-and-cilantro-api/test/fixtures/user.ts | 30 +- chili-and-cilantro-api/test/fixtures/utils.ts | 19 +- .../test/unit/actionService.test.ts | 5 +- .../test/unit/chefService.test.ts | 15 +- .../test/unit/gameService.createGame.test.ts | 12 +- .../unit/gameService.joinGameAsync.test.ts | 30 +- .../test/unit/jwtService.test.ts | 573 --- .../test/unit/userService.test.ts | 4 +- chili-and-cilantro-api/tsconfig.spec.json | 1 + chili-and-cilantro-lib/package.json | 9 +- chili-and-cilantro-lib/src/index.ts | 2 + .../src/lib/action-strings.ts | 18 + chili-and-cilantro-lib/src/lib/constants.ts | 11 +- .../src/lib/errors/invalid-action.ts | 6 +- .../src/lib/errors/invalid-game-name.ts | 2 +- .../src/lib/errors/invalid-game-password.ts | 2 +- .../src/lib/errors/invalid-message.ts | 2 +- .../lib/errors/invalid-user-display-name.ts | 2 +- .../src/lib/errors/out-of-ingredient.ts | 2 +- .../src/lib/interfaces/bid.ts | 4 +- .../src/lib/interfaces/card.ts | 3 +- .../src/lib/interfaces/create-user-basics.ts | 1 - .../src/lib/interfaces/documents/action.ts | 8 +- .../documents/actions/create-game.ts | 10 +- .../interfaces/documents/actions/end-game.ts | 10 +- .../interfaces/documents/actions/end-round.ts | 6 +- .../documents/actions/expire-game.ts | 10 +- .../interfaces/documents/actions/flip-card.ts | 10 +- .../interfaces/documents/actions/join-game.ts | 10 +- .../interfaces/documents/actions/make-bid.ts | 10 +- .../interfaces/documents/actions/message.ts | 10 +- .../lib/interfaces/documents/actions/pass.ts | 10 +- .../documents/actions/place-card.ts | 10 +- .../interfaces/documents/actions/quit-game.ts | 10 +- .../documents/actions/start-bidding.ts | 10 +- .../documents/actions/start-game.ts | 10 +- .../documents/actions/start-new-round.ts | 10 +- .../src/lib/interfaces/documents/base.ts | 6 +- .../src/lib/interfaces/documents/card.ts | 9 +- .../src/lib/interfaces/documents/chef.ts | 5 +- .../lib/interfaces/documents/email-token.ts | 9 +- .../src/lib/interfaces/documents/game.ts | 5 +- .../src/lib/interfaces/documents/user.ts | 5 +- .../src/lib/interfaces/has-id.ts | 6 +- .../src/lib/interfaces/has-soft-deleter.ts | 4 +- .../src/lib/interfaces/models/action.ts | 16 +- .../interfaces/models/actions/create-game.ts | 8 +- .../interfaces/models/actions/details/base.ts | 1 + .../models/actions/details/create-game.ts | 4 +- .../models/actions/details/end-game.ts | 4 +- .../models/actions/details/end-round.ts | 4 +- .../models/actions/details/expire-game.ts | 4 +- .../models/actions/details/flip-card.ts | 9 +- .../models/actions/details/join-game.ts | 4 +- .../models/actions/details/make-bid.ts | 4 +- .../models/actions/details/message.ts | 4 +- .../interfaces/models/actions/details/pass.ts | 4 +- .../models/actions/details/place-card.ts | 3 +- .../models/actions/details/quit-game.ts | 3 +- .../models/actions/details/start-bidding.ts | 4 +- .../models/actions/details/start-game.ts | 4 +- .../models/actions/details/start-new-round.ts | 4 +- .../lib/interfaces/models/actions/end-game.ts | 5 +- .../interfaces/models/actions/end-round.ts | 5 +- .../interfaces/models/actions/expire-game.ts | 8 +- .../interfaces/models/actions/flip-card.ts | 5 +- .../interfaces/models/actions/join-game.ts | 5 +- .../lib/interfaces/models/actions/make-bid.ts | 5 +- .../lib/interfaces/models/actions/message.ts | 5 +- .../src/lib/interfaces/models/actions/pass.ts | 5 +- .../interfaces/models/actions/place-card.ts | 5 +- .../interfaces/models/actions/quit-game.ts | 5 +- .../models/actions/start-bidding.ts | 8 +- .../interfaces/models/actions/start-game.ts | 5 +- .../models/actions/start-new-round.ts | 8 +- .../src/lib/interfaces/models/chef.ts | 8 +- .../src/lib/interfaces/models/email-token.ts | 6 +- .../src/lib/interfaces/models/game.ts | 22 +- .../src/lib/interfaces/objects/action.ts | 8 +- .../interfaces/objects/actions/create-game.ts | 2 +- .../src/lib/shared-types.ts | 53 + .../src/lib/turn-action-strings.ts | 8 + chili-and-cilantro-lib/tsconfig.spec.json | 3 +- chili-and-cilantro-lib/yarn.lock | 756 +++- chili-and-cilantro-node-lib/package.json | 14 +- chili-and-cilantro-node-lib/src/index.ts | 7 + .../src/lib/action-schema-map.ts | 4 +- .../src/lib/discriminators/action.ts | 43 +- .../src/lib/errors/missing-validated-data.ts | 14 + .../src/lib/interfaces/application.ts | 9 + .../src/lib/interfaces/schema-data.ts | 15 + .../src/lib/interfaces/schema-model-data.ts | 18 +- .../src/lib/models/action.ts | 21 +- .../src/lib/models/chef.ts | 21 +- .../src/lib/models/email-token.ts | 15 +- .../src/lib/models/game.ts | 21 +- .../src/lib/models/user.ts | 13 +- chili-and-cilantro-node-lib/src/lib/schema.ts | 81 +- .../src/lib/schemas/action.ts | 40 +- .../src/lib/schemas/actions/create-game.ts | 2 + .../src/lib/schemas/actions/end-game.ts | 2 + .../src/lib/schemas/actions/end-round.ts | 2 + .../src/lib/schemas/actions/expire-game.ts | 2 + .../src/lib/schemas/actions/flip-card.ts | 2 + .../src/lib/schemas/actions/join-game.ts | 2 + .../src/lib/schemas/actions/make-bid.ts | 2 + .../src/lib/schemas/actions/message.ts | 2 + .../src/lib/schemas/actions/pass.ts | 2 + .../src/lib/schemas/actions/place-card.ts | 2 + .../src/lib/schemas/actions/quit-game.ts | 2 + .../src/lib/schemas/actions/start-bidding.ts | 2 + .../src/lib/schemas/actions/start-game.ts | 2 + .../lib/schemas/actions/start-new-round.ts | 2 + .../src/lib/schemas/chef.ts | 5 +- .../src/lib/schemas/email-token.ts | 5 +- .../src/lib/schemas/game.ts | 14 +- .../src/lib/schemas/user.ts | 5 +- chili-and-cilantro-node-lib/src/lib/utils.ts | 127 + .../src/types/shared-types.ts | 18 + .../src/types/types.d.ts | 12 + chili-and-cilantro-node-lib/tsconfig.lib.json | 2 +- chili-and-cilantro-node-lib/yarn.lock | 1192 ++++- chili-and-cilantro-react/.eslintrc.json | 24 +- chili-and-cilantro-react/project.json | 2 +- chili-and-cilantro-react/src/app/app.tsx | 145 +- .../src/app/auth-provider.tsx | 201 + .../src/{ => app}/components/api-access.tsx | 24 +- .../src/app/components/auth.scss | 74 + .../app/components/change-password-page.tsx | 136 + .../src/app/components/dashboard-page.scss | 52 + .../src/app/components/dashboard-page.tsx | 82 + .../app/components/forgot-password-page.tsx | 175 + .../src/{ => app}/components/game.tsx | 41 +- .../src/app/components/login-page.tsx | 193 + .../src/app/components/player-disc.scss | 71 + .../src/app/components/player-disc.tsx | 119 + .../src/app/components/private-route.tsx | 28 + .../src/app/components/register-page.tsx | 155 + .../src/app/components/splash-page.scss | 102 + .../src/app/components/splash-page.tsx | 50 + .../src/app/components/top-menu.scss | 143 + .../src/app/components/top-menu.tsx | 143 + .../src/app/components/verify-email-page.scss | 38 + .../src/app/components/verify-email-page.tsx | 72 + .../src/app/menu-context.tsx | 87 + .../src/app/nx-welcome.tsx | 845 ---- .../src/app/services/api.ts | 9 + .../src/app/services/auth-service.ts | 114 + .../src/app/services/authenticated-api.ts | 16 + .../src/assets/fonts/Dosmilcatorce.woff2 | Bin 0 -> 20228 bytes .../src/assets/fonts/McFoodPoisoning1.woff2 | Bin 0 -> 30076 bytes .../src/assets/fonts/McFoodPoisoning2.woff2 | Bin 0 -> 30248 bytes .../src/assets/fonts/McFoodPoisoning3.woff2 | Bin 0 -> 30960 bytes .../src/assets/fonts/McFoodPoisoning4.woff2 | Bin 0 -> 30340 bytes .../src/assets/fonts/McFoodPoisoning5.woff2 | Bin 0 -> 29472 bytes .../src/assets/fonts/McFoodPoisoning6.woff2 | Bin 0 -> 29984 bytes ...fairDisplay-Italic-VariableFont_wght.woff2 | Bin 0 -> 104256 bytes .../PlayfairDisplay-VariableFont_wght.woff2 | Bin 0 -> 106340 bytes .../src/assets/fonts/kingsbridge rg.woff2 | Bin 0 -> 38592 bytes .../assets/images/Chili-and-Cilantro-logo.png | Bin 0 -> 509765 bytes .../src/assets/images/Chili-and-Cilantro.png | Bin 0 -> 872107 bytes .../src/assets/images/chili.png | Bin 0 -> 344297 bytes .../src/assets/images/cilantro.png | Bin 0 -> 491405 bytes .../components/authentication-required.tsx | 23 - .../src/components/callback.tsx | 68 - .../src/components/login-button.tsx | 9 - .../src/components/login-link.scss | 9 - .../src/components/login-link.tsx | 14 - .../src/components/logout-link.scss | 9 - .../src/components/logout-link.tsx | 14 - .../src/components/not-found.tsx | 29 - .../src/components/page-loader.tsx | 11 - .../src/environments/environment.prod.ts | 6 - .../src/environments/environment.ts | 6 - chili-and-cilantro-react/src/index.html | 4 +- .../src/interfaces/environment.ts | 6 - chili-and-cilantro-react/src/main.tsx | 44 +- .../src/pages/splash-page.tsx | 36 + .../src/pages/user-profile.tsx | 26 - chili-and-cilantro-react/src/styles.css | 1 - chili-and-cilantro-react/src/styles.scss | 211 + chili-and-cilantro-react/src/theme.tsx | 46 + .../src/types/images.d.ts | 19 + chili-and-cilantro-react/tsconfig.app.json | 8 +- chili-and-cilantro-react/tsconfig.json | 11 +- docs/Developers.md | 20 +- npm-install-globals.sh | 5 + package.json | 52 +- qodana.yaml | 10 + reset.sh | 7 + yarn.lock | 3967 ++++++++++------- 238 files changed, 9561 insertions(+), 4592 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/qodana_code_quality.yml create mode 100644 chili-and-cilantro-api/src/application.ts create mode 100644 chili-and-cilantro-api/src/controllers/api/game.ts create mode 100644 chili-and-cilantro-api/src/interfaces/bid-ingredient.ts create mode 100644 chili-and-cilantro-api/src/interfaces/game-action.ts create mode 100644 chili-and-cilantro-api/src/interfaces/game-chef.ts create mode 100644 chili-and-cilantro-api/src/middlewares.ts create mode 100644 chili-and-cilantro-api/src/routers/api.ts create mode 100644 chili-and-cilantro-api/src/routers/app.ts create mode 100644 chili-and-cilantro-api/src/routers/base.ts delete mode 100644 chili-and-cilantro-api/src/routes/api.ts delete mode 100644 chili-and-cilantro-api/src/routes/games.ts delete mode 100644 chili-and-cilantro-api/src/routes/users.ts delete mode 100644 chili-and-cilantro-api/src/setup-database.ts delete mode 100644 chili-and-cilantro-api/src/setup-middlewares.ts delete mode 100644 chili-and-cilantro-api/src/setup-routes.ts delete mode 100644 chili-and-cilantro-api/src/setup-static-react-app.ts create mode 100644 chili-and-cilantro-api/test/fixtures/database.ts delete mode 100644 chili-and-cilantro-api/test/fixtures/jwksClient.ts create mode 100644 chili-and-cilantro-api/test/fixtures/mocked-model.ts create mode 100644 chili-and-cilantro-api/test/fixtures/objectId.ts delete mode 100644 chili-and-cilantro-api/test/unit/jwtService.test.ts create mode 100644 chili-and-cilantro-lib/src/lib/action-strings.ts create mode 100644 chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/base.ts create mode 100644 chili-and-cilantro-lib/src/lib/shared-types.ts create mode 100644 chili-and-cilantro-lib/src/lib/turn-action-strings.ts create mode 100644 chili-and-cilantro-node-lib/src/lib/errors/missing-validated-data.ts create mode 100644 chili-and-cilantro-node-lib/src/lib/interfaces/application.ts create mode 100644 chili-and-cilantro-node-lib/src/lib/interfaces/schema-data.ts create mode 100644 chili-and-cilantro-node-lib/src/lib/utils.ts create mode 100644 chili-and-cilantro-node-lib/src/types/shared-types.ts create mode 100644 chili-and-cilantro-node-lib/src/types/types.d.ts create mode 100644 chili-and-cilantro-react/src/app/auth-provider.tsx rename chili-and-cilantro-react/src/{ => app}/components/api-access.tsx (65%) create mode 100644 chili-and-cilantro-react/src/app/components/auth.scss create mode 100644 chili-and-cilantro-react/src/app/components/change-password-page.tsx create mode 100644 chili-and-cilantro-react/src/app/components/dashboard-page.scss create mode 100644 chili-and-cilantro-react/src/app/components/dashboard-page.tsx create mode 100644 chili-and-cilantro-react/src/app/components/forgot-password-page.tsx rename chili-and-cilantro-react/src/{ => app}/components/game.tsx (75%) create mode 100644 chili-and-cilantro-react/src/app/components/login-page.tsx create mode 100644 chili-and-cilantro-react/src/app/components/player-disc.scss create mode 100644 chili-and-cilantro-react/src/app/components/player-disc.tsx create mode 100644 chili-and-cilantro-react/src/app/components/private-route.tsx create mode 100644 chili-and-cilantro-react/src/app/components/register-page.tsx create mode 100644 chili-and-cilantro-react/src/app/components/splash-page.scss create mode 100644 chili-and-cilantro-react/src/app/components/splash-page.tsx create mode 100644 chili-and-cilantro-react/src/app/components/top-menu.scss create mode 100644 chili-and-cilantro-react/src/app/components/top-menu.tsx create mode 100644 chili-and-cilantro-react/src/app/components/verify-email-page.scss create mode 100644 chili-and-cilantro-react/src/app/components/verify-email-page.tsx create mode 100644 chili-and-cilantro-react/src/app/menu-context.tsx delete mode 100644 chili-and-cilantro-react/src/app/nx-welcome.tsx create mode 100644 chili-and-cilantro-react/src/app/services/api.ts create mode 100644 chili-and-cilantro-react/src/app/services/auth-service.ts create mode 100644 chili-and-cilantro-react/src/app/services/authenticated-api.ts create mode 100644 chili-and-cilantro-react/src/assets/fonts/Dosmilcatorce.woff2 create mode 100644 chili-and-cilantro-react/src/assets/fonts/McFoodPoisoning1.woff2 create mode 100644 chili-and-cilantro-react/src/assets/fonts/McFoodPoisoning2.woff2 create mode 100644 chili-and-cilantro-react/src/assets/fonts/McFoodPoisoning3.woff2 create mode 100644 chili-and-cilantro-react/src/assets/fonts/McFoodPoisoning4.woff2 create mode 100644 chili-and-cilantro-react/src/assets/fonts/McFoodPoisoning5.woff2 create mode 100644 chili-and-cilantro-react/src/assets/fonts/McFoodPoisoning6.woff2 create mode 100644 chili-and-cilantro-react/src/assets/fonts/PlayfairDisplay-Italic-VariableFont_wght.woff2 create mode 100644 chili-and-cilantro-react/src/assets/fonts/PlayfairDisplay-VariableFont_wght.woff2 create mode 100644 chili-and-cilantro-react/src/assets/fonts/kingsbridge rg.woff2 create mode 100644 chili-and-cilantro-react/src/assets/images/Chili-and-Cilantro-logo.png create mode 100644 chili-and-cilantro-react/src/assets/images/Chili-and-Cilantro.png create mode 100644 chili-and-cilantro-react/src/assets/images/chili.png create mode 100644 chili-and-cilantro-react/src/assets/images/cilantro.png delete mode 100644 chili-and-cilantro-react/src/components/authentication-required.tsx delete mode 100644 chili-and-cilantro-react/src/components/callback.tsx delete mode 100644 chili-and-cilantro-react/src/components/login-button.tsx delete mode 100644 chili-and-cilantro-react/src/components/login-link.scss delete mode 100644 chili-and-cilantro-react/src/components/login-link.tsx delete mode 100644 chili-and-cilantro-react/src/components/logout-link.scss delete mode 100644 chili-and-cilantro-react/src/components/logout-link.tsx delete mode 100644 chili-and-cilantro-react/src/components/not-found.tsx delete mode 100644 chili-and-cilantro-react/src/components/page-loader.tsx create mode 100644 chili-and-cilantro-react/src/pages/splash-page.tsx delete mode 100644 chili-and-cilantro-react/src/pages/user-profile.tsx delete mode 100644 chili-and-cilantro-react/src/styles.css create mode 100644 chili-and-cilantro-react/src/styles.scss create mode 100644 chili-and-cilantro-react/src/theme.tsx create mode 100644 chili-and-cilantro-react/src/types/images.d.ts create mode 100644 npm-install-globals.sh create mode 100644 qodana.yaml create mode 100755 reset.sh diff --git a/.devcontainer/.env.example b/.devcontainer/.env.example index dc159a4..d18f5d5 100644 --- a/.devcontainer/.env.example +++ b/.devcontainer/.env.example @@ -1,5 +1,6 @@ MONGO_DB_USERNAME=chilicilantro MONGO_DB_PASSWORD=db!Passw0rd FONTAWESOME_KEY=XXXXXXXXXXXXXX +GRAPHITE_KEY=XXXXXXXXXXXXXX COMPOSE_PROJECT_NAME=chili-and-cilantro_devcontainer \ No newline at end of file diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..5eb6797 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,22 @@ +FROM mcr.microsoft.com/devcontainers/javascript-node:1-22-bookworm + +# Install MongoDB command line tools - though mongo-database-tools not available on arm64 +ARG MONGO_TOOLS_VERSION=6.0 +RUN . /etc/os-release \ + && curl -sSL "https://www.mongodb.org/static/pgp/server-${MONGO_TOOLS_VERSION}.asc" | gpg --dearmor > /usr/share/keyrings/mongodb-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/mongodb-archive-keyring.gpg] http://repo.mongodb.org/apt/debian ${VERSION_CODENAME}/mongodb-org/${MONGO_TOOLS_VERSION} main" | tee /etc/apt/sources.list.d/mongodb-org-${MONGO_TOOLS_VERSION}.list \ + && apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get install -y mongodb-mongosh \ + && if [ "$(dpkg --print-architecture)" = "amd64" ]; then apt-get install -y mongodb-database-tools; fi \ + && apt-get clean -y && rm -rf /var/lib/apt/lists/* + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment if you want to install an additional version of node using nvm +# ARG EXTRA_NODE_VERSION=10 +# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" + +# [Optional] Uncomment if you want to install more global node modules +# RUN su node -c "npm install -g " \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index dc4867d..611bc0c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,53 +1,54 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node { - "name": "Node.js & TypeScript", - // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "dockerComposeFile": "docker-compose.yml", - "service": "app", - "workspaceFolder": "/workspace", + "name": "Node.js & TypeScript", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspace", - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - "features": { - "ghcr.io/devcontainers/features/common-utils:2": {}, - "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, - }, + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + "features": { + "ghcr.io/devcontainers/features/common-utils:2": {}, + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {} + }, - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - "forwardPorts": [27017], + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + "forwardPorts": [27017], - // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "./setup-nvm.sh && ./fontawesome-npmrc.sh && ./do-yarn.sh && git config --global --add safe.directory /workspace", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "./setup-nvm.sh && ./fontawesome-npmrc.sh && ./do-yarn.sh && git config --global --add safe.directory /workspace", - // Configure tool-specific properties. - // "customizations": {}, - "customizations": { - "vscode": { - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "mongodb.mongodb-vscode", - "rangav.vscode-thunder-client", - "github.vscode-github-actions", - "eamodio.gitlens", - "ms-azuretools.vscode-docker", - "esbenp.prettier-vscode", - "ms-vscode-remote.remote-containers", - "firsttris.vscode-jest-runner", - "ms-playwright.playwright", - "nrwl.angular-console", - "TabNine.tabnine-vscode", - "GitHub.copilot", - "GitHub.copilot-chat" - ], - // Set *default* container specific settings.json values on container create. - "settings": { - "terminal.integrated.shell.linux": "/usr/bin/zsh" - }, - }, - }, + // Configure tool-specific properties. + // "customizations": {}, + "customizations": { + "vscode": { + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "mongodb.mongodb-vscode", + "rangav.vscode-thunder-client", + "github.vscode-github-actions", + "eamodio.gitlens", + "ms-azuretools.vscode-docker", + "esbenp.prettier-vscode", + "ms-vscode-remote.remote-containers", + "firsttris.vscode-jest-runner", + "ms-playwright.playwright", + "nrwl.angular-console", + "GitHub.copilot", + "GitHub.copilot-chat", + "Codeium.codeium", + "bruno-api-client.bruno" + ], + // Set *default* container specific settings.json values on container create. + "settings": { + "terminal.integrated.shell.linux": "/usr/bin/zsh" + } + } + }, - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - "remoteUser": "root" -} \ No newline at end of file + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + "remoteUser": "root" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 1952396..f3db00b 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,10 +1,12 @@ version: '3' services: app: - image: mcr.microsoft.com/devcontainers/typescript-node:0-20-bullseye + build: + context: . + dockerfile: Dockerfile volumes: - ..:/workspace:cached - command: /bin/sh -c "while sleep 1000; do :; done" + command: sleep infinity environment: NODE_ENV: development FONTAWESOME_KEY: ${FONTAWESOME_KEY} @@ -14,8 +16,8 @@ services: - node_app mongo: - restart: always - image: mongo:6.0.7 + image: mongo:latest + restart: unless-stopped environment: - MONGO_INITDB_ROOT_USERNAME=${MONGO_DB_USERNAME} - MONGO_INITDB_ROOT_PASSWORD=${MONGO_DB_PASSWORD} diff --git a/.eslintrc.json b/.eslintrc.json index 8050e5a..ea96001 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,12 +1,12 @@ { "root": true, "ignorePatterns": ["**/*.test.ts", "**/build/**", "**/dist/**"], - "plugins": ["@nrwl/nx", "prettier"], + "plugins": ["@nx", "prettier"], "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], "rules": { - "@nrwl/nx/enforce-module-boundaries": [ + "@nx/enforce-module-boundaries": [ "error", { "enforceBuildableLibDependency": true, @@ -18,19 +18,24 @@ } ] } - ] + ], + "@typescript-eslint/no-empty-function": [ + "error", + { "allow": ["arrowFunctions", "functions", "methods"] } + ], + "@nx/dependency-checks": "error" } }, { "files": ["*.ts", "*.tsx"], - "extends": ["plugin:@nrwl/nx/typescript", "plugin:prettier/recommended"], + "extends": ["plugin:@nx/typescript", "plugin:prettier/recommended"], "rules": { "prettier/prettier": "error" } }, { "files": ["*.js", "*.jsx"], - "extends": ["plugin:@nrwl/nx/javascript", "plugin:prettier/recommended"], + "extends": ["plugin:@nx/javascript", "plugin:prettier/recommended"], "rules": { "prettier/prettier": "error" } diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..3cddc75 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for more information: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://containers.dev/guide/dependabot + +version: 2 +updates: + - package-ecosystem: 'devcontainers' + directory: '/' + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f36a937 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +permissions: + actions: read + contents: read + +jobs: + main: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Clean workspace + run: | + rm -rf node_modules + rm -rf dist + rm -rf .cache + + - uses: actions/setup-node@v4 + with: + node-version: 22.9.0 + cache: 'yarn' + + - name: Clear Yarn cache + run: yarn cache clean + + - name: Setup FontAwesome + run: | + ./fontawesome-npmrc.sh + env: + FONTAWESOME_KEY: ${{ secrets.FONTAWESOME_KEY }} + + - name: Install dependencies + run: | + ./do-yarn.sh --frozen-lockfile --ignore-scripts + + - uses: nrwl/nx-set-shas@v4 + + # Run lint, test, build, and e2e tasks explicitly + - name: Run Lint + run: npx nx run-many --target=lint --all --skip-nx-cache --parallel=3 + + - name: Run Tests + run: npx nx run-many --target=test --all --skip-nx-cache --parallel=3 + + - name: Run Build + run: npx nx run-many --target=build --all --skip-nx-cache --parallel=3 + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Run E2E Tests + run: npx nx run-many --target=e2e --all --skip-nx-cache --parallel=3 + env: + CI: true + timeout-minutes: 10 diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml new file mode 100644 index 0000000..4c8b4f4 --- /dev/null +++ b/.github/workflows/qodana_code_quality.yml @@ -0,0 +1,28 @@ +name: Qodana +on: + workflow_dispatch: + pull_request: + push: + branches: # Specify your branches here + - main # The 'main' branch + - 'releases/*' # The release branches + +jobs: + qodana: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + checks: write + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit + fetch-depth: 0 # a full history is required for pull request analysis + - name: 'Qodana Scan' + uses: JetBrains/qodana-action@v2024.2 + with: + pr-mode: false + env: + QODANA_TOKEN: ${{ secrets.QODANA_TOKEN_57397973 }} + QODANA_ENDPOINT: 'https://qodana.cloud' \ No newline at end of file diff --git a/chili-and-cilantro-api/.env.example b/chili-and-cilantro-api/.env.example index d326be5..5f0bbf7 100644 --- a/chili-and-cilantro-api/.env.example +++ b/chili-and-cilantro-api/.env.example @@ -1,13 +1,11 @@ MONGO_URI=mongodb://chilicilantro:db!Passw0rd@mongo:27017/chilicilantro?authSource=admin +SENDGRID_API_KEY=SG-XXXXX +REACT_DIST_DIR=/workspace/dist/chili-and-cilantro-api +SERVER_HOST=0.0.0.0 +PORT=3000 # SITE_URL=http://localhost:3000 +EMAIL_SENDER=noreply@chilicilantro.com SSL_ENABLED=false # -AUTH0_DATABASE=Username-Password-Authentication -AUTH0_DOMAIN=your-project-name.us.auth0.com -AUTH0_CLIENT_ID= -AUTH0_CLIENT_SECRET= -AUTH0_SCOPE=create:users -AUTH0_AUDIENCE=http://localhost:3000/ -# COMPOSE_PROJECT_NAME=your-project-name_devcontainer \ No newline at end of file diff --git a/chili-and-cilantro-api/src/application.ts b/chili-and-cilantro-api/src/application.ts new file mode 100644 index 0000000..90a6556 --- /dev/null +++ b/chili-and-cilantro-api/src/application.ts @@ -0,0 +1,199 @@ +import { + GetModelFunction, + IBaseDocument, +} from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { + getSchemaMap, + IApplication, + SchemaMap, +} from '@chili-and-cilantro/chili-and-cilantro-node-lib'; +import express, { Application, Request, Response } from 'express'; +import { Server } from 'http'; +import mongoose, { connect, connection, Model } from 'mongoose'; +import { environment } from './environment'; +import { Middlewares } from './middlewares'; +import { ApiRouter } from './routers/api'; +import { AppRouter } from './routers/app'; + +/** + * Application class + */ +export class App implements IApplication { + private static instance: App | null = null; + /** + * Express application instance + */ + public readonly expressApp: Application; + /** + * Mongoose database instance + */ + private _db?: typeof mongoose; + /** + * Schema map for all models + */ + private _schemaMap: SchemaMap | undefined; + public get schemaMap(): SchemaMap { + if (!this._schemaMap) { + throw new Error('schemaMap is not loaded yet. call start() first'); + } + return this._schemaMap; + } + /** + * Flag indicating whether the application is ready to handle requests + */ + private _ready: boolean; + + /** + * HTTP server instance + */ + private server: Server | null = null; + private _appRouter: AppRouter | undefined; + public get appRouter(): AppRouter { + if (!this._appRouter) { + throw new Error('appRouter is not loaded yet. call start() first'); + } + return this._appRouter; + } + private _apiRouter: ApiRouter | undefined; + public get apiRouter(): ApiRouter { + if (!this._apiRouter) { + throw new Error('apiRouter is not loaded yet. call start() first'); + } + return this._apiRouter; + } + + public static getInstance(): App { + if (!App.instance) { + App.instance = new App(); + } + return App.instance; + } + + /** + * Get the connected MongoDB database instance + */ + public get db(): typeof mongoose { + if (!this._db) { + throw new Error('db is not connected yet. call start() first'); + } + return this._db; + } + + /** + * Get whether the application is ready to handle requests + */ + public get ready(): boolean { + return this._ready; + } + constructor() { + if (App.instance) { + throw new Error('App instance already exists, use getInstance()'); + } + this._ready = false; + this.expressApp = express(); + this.server = null; + } + + /** + * Start the application + */ + public async start(mongoUri?: string, debug = true): Promise { + const mongo_uri = mongoUri ?? environment.mongo.uri; + try { + if (this._ready) { + console.error( + 'Failed to start the application:', + 'Application is already running', + ); + process.exit(1); + } + if (debug) console.log('[ connecting ] MongoDB', mongo_uri); + this._db = await connect(mongo_uri); + connection.on('error', (err) => { + console.error('MongoDB connection error:', err); + }); + + connection.on('disconnected', () => { + if (debug) console.log('MongoDB disconnected'); + }); + + await new Promise((resolve) => { + if (connection.readyState === 1) { + resolve(); + } else { + connection.once('connected', resolve); + } + }); + if (debug) console.log('[ connected ] MongoDB'); + + if (debug) console.log('[ loading ] Schemas'); + this._schemaMap = getSchemaMap(this.db.connection); + + if (debug) { + Object.values(this._schemaMap).forEach((schema) => { + console.log(`[ loaded ] schema '${schema.name}'`); + }); + } + + // init all middlewares and routes + Middlewares.init(this.expressApp); + this._apiRouter = new ApiRouter(this.getModel, this.db.connection); + this._appRouter = new AppRouter(this._apiRouter); + this._appRouter.init(this.expressApp, debug); + // if none of the above handle the request, pass it to error handler + this.expressApp.use((err: Error, req: Request, res: Response) => { + console.error('Unhandled error:', err); + res.status(500).send('Internal Server Error'); + }); + + this.server = this.expressApp.listen( + environment.developer.port, + environment.developer.host, + () => { + this._ready = true; + if (debug) + console.log( + `[ ready ] http://${environment.developer.host}:${environment.developer.port}`, + ); + }, + ); + } catch (err) { + console.error('Failed to start the application:', err); + process.exit(1); + } + } + + /** + * Stop the application + */ + public async stop(debug = true): Promise { + if (this.server) { + if (debug) console.log('[ stopping ] Application server'); + await new Promise((resolve, reject) => { + this.server!.close((err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + this.server = null; + } + + if (this.db) { + await this.db.disconnect(); + this._db = undefined; + } + + this._ready = false; + if (debug) + console.log('[ stopped ] Application server and database connections'); + } + + public getModel: GetModelFunction = >( + modelName: string, + ): Model => { + return this.db.connection.model(modelName); + }; +} diff --git a/chili-and-cilantro-api/src/controllers/api/game.ts b/chili-and-cilantro-api/src/controllers/api/game.ts new file mode 100644 index 0000000..3a89081 --- /dev/null +++ b/chili-and-cilantro-api/src/controllers/api/game.ts @@ -0,0 +1,429 @@ +import { + CardType, + GetModelFunction, + IUserDocument, + ModelName, + TurnAction, + UserNotFoundError, + ValidationError, +} from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { RouteConfig } from 'chili-and-cilantro-api/src/interfaces/route-config'; +import { Request, Response } from 'express'; +import { InvalidTokenError } from 'express-oauth2-jwt-bearer'; +import { body } from 'express-validator'; +import { Connection } from 'mongoose'; +import { ActionService } from '../../services/action'; +import { ChefService } from '../../services/chef'; +import { GameService } from '../../services/game'; +import { PlayerService } from '../../services/player'; +import { BaseController } from '../base'; + +export class GameController extends BaseController { + private readonly actionService; + private readonly chefService; + private readonly playerService; + private readonly gameService; + + constructor(getModel: GetModelFunction, connection: Connection) { + super(getModel); + this.actionService = new ActionService(getModel, connection); + this.chefService = new ChefService(getModel); + this.playerService = new PlayerService(getModel); + this.gameService = new GameService( + getModel, + this.actionService, + this.chefService, + this.playerService, + ); + } + + protected getRoutes(): RouteConfig[] { + return [ + { + method: 'post', + path: '/create', + handler: this.createGame, + useAuthentication: true, + validation: [ + body('name').isString().trim().notEmpty(), + body('userName').isString().trim().notEmpty(), + body('password').optional().isString().trim(), + body('maxChefs').isInt({ min: 2, max: 8 }), + ], + }, + { + method: 'post', + path: '/:code/join', + handler: this.joinGame, + useAuthentication: true, + validation: [ + body('userName').isString().trim().notEmpty(), + body('password').optional().isString().trim(), + ], + }, + { + method: 'post', + path: '/:code/message', + handler: this.sendMessage, + useAuthentication: true, + validation: [body('message').isString().trim().notEmpty()], + }, + { + method: 'get', + path: '/:code/history', + handler: this.getGameHistory, + useAuthentication: true, + }, + { + method: 'post', + path: '/:code/start', + handler: this.startGame, + useAuthentication: true, + }, + { + method: 'get', + path: '/:code/action', + handler: this.getAvailableActions, + useAuthentication: true, + }, + { + method: 'post', + path: '/:code/action', + handler: this.performTurnAction, + useAuthentication: true, + validation: [ + body('action').isString().isIn(Object.values(TurnAction)), + body('cardType').optional().isString().isIn(Object.values(CardType)), + ], + }, + ]; + } + + /** + * Creates a new game. + * @param req + * @param res + * @returns + */ + private async createGame(req: Request, res: Response) { + const UserModel = this.getModel(ModelName.User); + try { + if (!req.user) { + this.sendApiErrorResponse( + 401, + 'Invalid token', + new InvalidTokenError(), + res, + ); + return; + } + const user = await UserModel.findById(req.user.id); + if (!user) { + this.sendApiErrorResponse( + 500, + 'User not found', + new UserNotFoundError(), + res, + ); + return; + } + const { name, userName, password, maxChefs } = req.body; + const sanitizedName = (name as string)?.trim(); + const sanitizedUserName = (userName as string)?.trim(); + const sanitizedPassword = (password as string)?.trim(); + const sanitizedMaxChefs = parseInt(maxChefs, 10); + + const { game, chef } = await this.gameService.performCreateGameAsync( + user, + sanitizedUserName, + sanitizedName, + sanitizedPassword, + sanitizedMaxChefs, + ); + res.send({ game, chef }); + } catch (error) { + if (error instanceof ValidationError) { + res.status(400).json(error); + } else { + this.sendApiErrorResponse(500, 'An error occurred', error, res); + } + } + } + + /** + * Joins a game + * @param req + * @param res + * @returns + */ + private async joinGame(req: Request, res: Response) { + const UserModel = this.getModel(ModelName.User); + try { + if (!req.user) { + this.sendApiErrorResponse( + 401, + 'Invalid token', + new InvalidTokenError(), + res, + ); + return; + } + const user = await UserModel.findById(req.user.id); + if (!user) { + this.sendApiErrorResponse( + 500, + 'User not found', + new UserNotFoundError(), + res, + ); + return; + } + const { userName, password } = req.body; + const gameCode = req.params.code; + const sanitizedUserName = (userName as string)?.trim(); + const sanitizedPassword = (password as string)?.trim(); + + const { game, chef } = await this.gameService.performJoinGameAsync( + gameCode, + sanitizedPassword, + user, + sanitizedUserName, + ); + res.send({ game, chef }); + } catch (error) { + if (error instanceof ValidationError) { + res.status(400).json(error); + } else { + res.status(500).json(error); + } + } + } + + /** + * Send a message to game chat + * @param req + * @param res + * @returns + */ + private async sendMessage(req: Request, res: Response) { + const UserModel = this.getModel(ModelName.User); + try { + if (!req.user) { + this.sendApiErrorResponse( + 401, + 'Invalid token', + new InvalidTokenError(), + res, + ); + return; + } + const user = await UserModel.findById(req.user.id); + if (!user) { + this.sendApiErrorResponse( + 500, + 'User not found', + new UserNotFoundError(), + res, + ); + return; + } + const { message } = req.body; + const gameCode = req.params.code; + const sanitizedMessage = (message as string)?.trim(); + const messageAction = await this.gameService.performSendMessageAsync( + gameCode, + user, + sanitizedMessage, + ); + res.status(200).json(messageAction); + } catch (e) { + if (e instanceof ValidationError) { + res.status(400).json(e); + } else { + this.sendApiErrorResponse(500, 'An error occurred', e, res); + } + } + } + + /** + * Gets the history of the game + * @param req + * @param res + * @returns + */ + private async getGameHistory(req: Request, res: Response) { + const UserModel = this.getModel(ModelName.User); + try { + if (!req.user) { + this.sendApiErrorResponse( + 401, + 'Invalid token', + new InvalidTokenError(), + res, + ); + return; + } + const user = await UserModel.findById(req.user.id); + if (!user) { + this.sendApiErrorResponse( + 500, + 'User not found', + new UserNotFoundError(), + res, + ); + return; + } + const gameCode = req.params.code; + const game = await this.gameService.getGameByCodeOrThrowAsync( + gameCode, + true, + ); + const actions = await this.actionService.getGameHistoryAsync(game); + res.status(200).json(actions); + } catch (e) { + if (e instanceof ValidationError) { + res.status(400).json(e); + } else { + this.sendApiErrorResponse(500, 'An error occurred', e, res); + } + } + } + + /** + * Starts a game + * @param req + * @param res + * @returns + */ + private async startGame(req: Request, res: Response) { + const UserModel = this.getModel(ModelName.User); + try { + if (!req.user) { + this.sendApiErrorResponse( + 401, + 'Invalid token', + new InvalidTokenError(), + res, + ); + return; + } + const user = await UserModel.findById(req.user.id); + if (!user) { + this.sendApiErrorResponse( + 500, + 'User not found', + new UserNotFoundError(), + res, + ); + return; + } + const gameCode = req.params.code; + const { game, action } = await this.gameService.performStartGameAsync( + gameCode, + user._id, + ); + res.status(200).json({ game, action }); + } catch (e) { + if (e instanceof ValidationError) { + res.status(400).json(e); + } else { + this.sendApiErrorResponse(500, 'An error occurred', e, res); + } + } + } + + /** + * Gets the available actions for the current turn + * @param req + * @param res + * @returns + */ + private async getAvailableActions(req: Request, res: Response) { + const UserModel = this.getModel(ModelName.User); + try { + if (!req.user) { + this.sendApiErrorResponse( + 401, + 'Invalid token', + new InvalidTokenError(), + res, + ); + return; + } + const user = await UserModel.findById(req.user.id); + if (!user) { + this.sendApiErrorResponse( + 500, + 'User not found', + new UserNotFoundError(), + res, + ); + return; + } + const gameCode = req.params.code; + const game = await this.gameService.getGameByCodeOrThrowAsync( + gameCode, + true, + ); + const chef = await this.chefService.getGameChefOrThrowAsync(game, user); + const actions = this.gameService.availableTurnActions(game, chef); + res.status(200).json(actions); + } catch (e) { + if (e instanceof ValidationError) { + res.status(400).json(e); + } else { + this.sendApiErrorResponse(500, 'An error occurred', e, res); + } + } + } + + /** + * Performs a turn action for the specified game + * @param req + * @param res + * @returns + */ + private async performTurnAction(req: Request, res: Response) { + const UserModel = this.getModel(ModelName.User); + try { + if (!req.user) { + this.sendApiErrorResponse( + 401, + 'Invalid token', + new InvalidTokenError(), + res, + ); + return; + } + const user = await UserModel.findById(req.user.id); + if (!user) { + this.sendApiErrorResponse( + 500, + 'User not found', + new UserNotFoundError(), + res, + ); + return; + } + const gameCode = req.params.code; + const { action, ingredient, bid } = req.body; + const actionArgs = { + ...(ingredient ? { ingredient: ingredient as CardType } : {}), + ...(bid ? { bid: bid as number } : {}), + }; + const { game, chef } = await this.gameService.performTurnActionAsync( + gameCode, + user, + action as TurnAction, + actionArgs, + ); + res.status(200).json({ game, chef }); + } catch (e) { + if (e instanceof ValidationError) { + res.status(400).json(e); + } else { + this.sendApiErrorResponse(500, 'An error occurred', e, res); + } + } + } +} diff --git a/chili-and-cilantro-api/src/controllers/api/user.ts b/chili-and-cilantro-api/src/controllers/api/user.ts index cadd70b..eeb46f0 100644 --- a/chili-and-cilantro-api/src/controllers/api/user.ts +++ b/chili-and-cilantro-api/src/controllers/api/user.ts @@ -3,14 +3,17 @@ import { constants, EmailTokenExpiredError, EmailTokenUsedOrInvalidError, + GetModelFunction, IApiMessageResponse, + ICreateUserBasics, InvalidCredentialsError, InvalidPasswordError, IRequestUser, ITokenResponse, + IUserDocument, IUserResponse, + ModelName, } from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { UserModel } from '@chili-and-cilantro/chili-and-cilantro-node-lib'; import { Request, Response } from 'express'; import { body, query } from 'express-validator'; import { MongooseValidationError } from '../../errors/mongoose-validation-error'; @@ -28,10 +31,10 @@ export class UserController extends BaseController { private jwtService: JwtService; private userService: UserService; - constructor() { - super(); + constructor(getModel: GetModelFunction) { + super(getModel); this.jwtService = new JwtService(); - this.userService = new UserService(); + this.userService = new UserService(getModel); } protected getRoutes(): RouteConfig[] { @@ -248,6 +251,7 @@ export class UserController extends BaseController { * @returns */ private async refreshToken(req: Request, res: Response) { + const UserModel = this.getModel(ModelName.User); try { const token = findAuthToken(req.headers); if (!token) { @@ -297,9 +301,8 @@ export class UserController extends BaseController { { username: username.trim(), email: email.trim(), - languages: ['en'], timezone: timezone, - }, + } as ICreateUserBasics, password, ); this.sendApiMessageResponse( @@ -393,6 +396,7 @@ export class UserController extends BaseController { * @param res */ public async resendVerification(req: Request, res: Response): Promise { + const UserModel = this.getModel(ModelName.User); try { const { username, email } = req.body; @@ -543,5 +547,3 @@ export class UserController extends BaseController { } } } - -export default new UserController().router; diff --git a/chili-and-cilantro-api/src/controllers/base.ts b/chili-and-cilantro-api/src/controllers/base.ts index af83289..7fefc38 100644 --- a/chili-and-cilantro-api/src/controllers/base.ts +++ b/chili-and-cilantro-api/src/controllers/base.ts @@ -1,21 +1,32 @@ import { + GetModelFunction, IApiErrorResponse, IApiExpressValidationErrorResponse, IApiMessageResponse, IApiMongoValidationErrorResponse, IMongoErrors, + IRequestUser, } from '@chili-and-cilantro/chili-and-cilantro-lib'; import { NextFunction, Request, Response, Router } from 'express'; -import { ValidationError, validationResult } from 'express-validator'; +import { + matchedData, + ValidationChain, + ValidationError, + validationResult, +} from 'express-validator'; import { RouteConfig } from '../interfaces/route-config'; import { authenticateToken } from '../middlewares/authenticate-token'; export abstract class BaseController { public readonly router: Router; + private activeRequest: Request | null = null; + private activeResponse: Response | null = null; + public readonly getModel: GetModelFunction; - constructor() { + protected constructor(getModel: GetModelFunction) { this.router = Router(); this.initializeRoutes(); + this.getModel = getModel; } /** @@ -38,11 +49,10 @@ export abstract class BaseController { validation = [], } = route; const routeHandlers = [ - ...middleware, ...(useAuthentication ? [ (req: Request, res: Response, next: NextFunction) => - this.authenticateRequest(req, res, next), + this.authenticateRequest(this.getModel, req, res, next), ] : []), ...(validation.length > 0 @@ -52,7 +62,19 @@ export abstract class BaseController { this.validateRequest(req, res, next), ] : []), + ...middleware, (req: Request, res: Response, next: NextFunction) => { + this.activeRequest = req; + this.activeResponse = res; + // if req.user wasn't added above, return an unauthorized response + if (useAuthentication && !req.user) { + this.sendApiMessageResponse( + 401, + { message: 'Unauthorized' } as IApiMessageResponse, + res, + ); + return; + } handler.call(this, req, res, next); }, ]; @@ -125,16 +147,18 @@ export abstract class BaseController { /** * Authenticates the request by checking the token. Also populates the request with the user object. - * @param req - * @param res - * @param next + * @param getModel Function to get models from the database + * @param req The request object + * @param res The response object + * @param next The next function */ protected authenticateRequest( + getModel: GetModelFunction, req: Request, res: Response, next: NextFunction, ): void { - authenticateToken(req, res, (err) => { + authenticateToken(getModel, req, res, (err) => { if (err || !req.user) { this.sendApiMessageResponse( 401, @@ -149,21 +173,87 @@ export abstract class BaseController { /** * Validates the request using the express-validator library. - * @param req - * @param res - * @param next + * @param req The request object + * @param res The response object + * @param next The next function + * @param validationArray An array of express validation chains to apply to the request. * @returns */ protected validateRequest( req: Request, res: Response, next: NextFunction, + validationArray: ValidationChain[] = [], ): void { const errors = validationResult(req); if (!errors.isEmpty()) { this.sendApiExpressValidationErrorResponse(400, errors.array(), res); return; } + // Create an object with only the validated fields + const validatedBody = matchedData(req, { + locations: ['body'], // Only match data from request body + includeOptionals: false, // Exclude fields that weren't validated + }); + + validationArray.forEach((validation: ValidationChain) => { + const fieldChains = validation.builder.build().fields; + + fieldChains.forEach((field: string) => { + const hasBooleanValidator = validation.builder + .build() + .stack.some((item: any) => { + return ( + item.validator && + typeof item.validator === 'function' && + item.validator.name === 'isBoolean' && + !item.negated + ); + }); + + if (hasBooleanValidator && !(field in validatedBody)) { + validatedBody[field] = false; + } + }); + }); + + // Attach the validated fields to the request object + req.validatedBody = validatedBody; + next(); } + + public get user(): IRequestUser { + if (!this.activeRequest) { + throw new Error('No active request'); + } + if (!this.activeRequest.user) { + throw new Error('No user on request'); + } + return this.activeRequest.user; + } + + public get validatedBody(): Record { + if (!this.activeRequest) { + throw new Error('No active request'); + } + if (!this.activeRequest.validatedBody) { + throw new Error('No validated body on request'); + } + return this.activeRequest.validatedBody; + } + + public get req(): Request { + if (!this.activeRequest) { + throw new Error('No active request'); + } + return this.activeRequest; + } + + public get res(): Response { + if (!this.activeResponse) { + throw new Error('No active response'); + } + return this.activeResponse; + } } diff --git a/chili-and-cilantro-api/src/environment.ts b/chili-and-cilantro-api/src/environment.ts index 4b21f5e..4b4bb7d 100644 --- a/chili-and-cilantro-api/src/environment.ts +++ b/chili-and-cilantro-api/src/environment.ts @@ -1,7 +1,11 @@ -import 'dotenv/config'; -import { join } from 'path'; +import { constants } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { config } from 'dotenv'; +import { join, resolve } from 'path'; import { IEnvironment } from './interfaces/environment'; +// Load .env file from the root directory +config({ path: resolve(__dirname, '../../.env') }); + /** * Finds the path to the dist folder using the current filename */ @@ -17,7 +21,8 @@ const host = process.env.SERVER_HOST ?? 'localhost'; const port = Number(process.env.PORT ?? 3000); const production = process.env.NODE_ENV === 'production'; const sslEnabled = process.env.SSL_ENABLED === 'true'; -const reactDir = relativeToDist('chili-and-cilantro-react'); +const reactDistDir = + process.env['REACT_DIST_DIR'] ?? relativeToDist('chili-and-cilantro-react'); function getSiteUrl() { const proto = sslEnabled ? 'https' : 'http'; @@ -35,8 +40,9 @@ export const environment: IEnvironment = { siteUrl: process.env.SITE_URL ?? getSiteUrl(), jwtSecret: process.env.JWT_SECRET ?? 'Ch1l!&C1l@ntr0', sendgridKey: process.env.SENDGRID_API_KEY ?? '', + emailSender: process.env.EMAIL_SENDER ?? constants.EMAIL_FROM, developer: { - reactDir: reactDir, + reactDistDir: reactDistDir, host: host, port: port, sslEnabled: sslEnabled, diff --git a/chili-and-cilantro-api/src/interfaces/bid-ingredient.ts b/chili-and-cilantro-api/src/interfaces/bid-ingredient.ts new file mode 100644 index 0000000..a86db69 --- /dev/null +++ b/chili-and-cilantro-api/src/interfaces/bid-ingredient.ts @@ -0,0 +1,6 @@ +import { CardType } from '@chili-and-cilantro/chili-and-cilantro-lib'; + +export interface IBidIngredient { + bid?: number; + ingredient?: CardType; +} diff --git a/chili-and-cilantro-api/src/interfaces/environment.ts b/chili-and-cilantro-api/src/interfaces/environment.ts index 5b100ed..4f089e9 100644 --- a/chili-and-cilantro-api/src/interfaces/environment.ts +++ b/chili-and-cilantro-api/src/interfaces/environment.ts @@ -1,10 +1,11 @@ export interface IEnvironment { production: boolean; siteUrl: string; + emailSender: string; jwtSecret: string; sendgridKey: string; developer: { - reactDir: string; + reactDistDir: string; host: string; port: number; sslEnabled: boolean; @@ -21,14 +22,34 @@ export interface IEnvironment { export function validateEnvironment( environment: IEnvironment, - then: (environment: IEnvironment) => void, + then: () => void, ) { // ensure all required environment variables are set - if (!environment.cookies.secret) { - throw new Error('EXPRESS_SESSION_SECRET is not set'); + if (!environment.developer.host) { + throw new Error('HOST is not set'); + } + if (!environment.developer.port) { + throw new Error('PORT is not set'); + } + if (!environment.siteUrl) { + throw new Error('SITE_URL is not set'); + } + if (!environment.jwtSecret) { + throw new Error('JWT_SECRET is not set'); } if (!environment.mongo.uri) { throw new Error('MONGO_URI is not set'); } - then(environment); + if (!environment.sendgridKey) { + throw new Error('SENDGRID_API_KEY is not set'); + } + if (!environment.sendgridKey.startsWith('SG')) { + throw new Error( + `SENDGRID_API_KEY does not start with "SG": ${environment.sendgridKey}`, + ); + } + if (!environment.emailSender) { + throw new Error('EMAIL_SENDER is not set'); + } + then(); } diff --git a/chili-and-cilantro-api/src/interfaces/game-action.ts b/chili-and-cilantro-api/src/interfaces/game-action.ts new file mode 100644 index 0000000..1c71bfa --- /dev/null +++ b/chili-and-cilantro-api/src/interfaces/game-action.ts @@ -0,0 +1,9 @@ +import { + IGameDocument, + IStartGameActionDocument, +} from '@chili-and-cilantro/chili-and-cilantro-lib'; + +export interface IGameAction { + game: IGameDocument; + action: IStartGameActionDocument; +} diff --git a/chili-and-cilantro-api/src/interfaces/game-chef.ts b/chili-and-cilantro-api/src/interfaces/game-chef.ts new file mode 100644 index 0000000..fb2b0b2 --- /dev/null +++ b/chili-and-cilantro-api/src/interfaces/game-chef.ts @@ -0,0 +1,9 @@ +import { + IChefDocument, + IGameDocument, +} from '@chili-and-cilantro/chili-and-cilantro-lib'; + +export interface IGameChef { + game: IGameDocument; + chef: IChefDocument; +} diff --git a/chili-and-cilantro-api/src/main.ts b/chili-and-cilantro-api/src/main.ts index 67bc006..65f8b38 100644 --- a/chili-and-cilantro-api/src/main.ts +++ b/chili-and-cilantro-api/src/main.ts @@ -1,51 +1,9 @@ -/* eslint-disable @typescript-eslint/no-namespace */ -/* eslint-disable @typescript-eslint/no-empty-interface */ -import express, { Application } from 'express'; -import fs from 'fs'; -import { Server, createServer } from 'http'; -import https from 'https'; +import { App } from './application'; import { environment } from './environment'; -import { setupDatabase } from './setup-database'; -import { setupMiddlewares } from './setup-middlewares'; -import { setupRoutes } from './setup-routes'; -import { setupStaticReactApp } from './setup-static-react-app'; +import { validateEnvironment } from './interfaces/environment'; -declare global { - namespace Express { - interface User {} - } -} -export const app: Application = express(); -export let server: https.Server | Server; +const app: App = App.getInstance(); -async function configureApplication(app: Application): Promise { - await setupDatabase(); - await setupMiddlewares(app); - await setupRoutes(app); - await setupStaticReactApp(app); -} - -configureApplication(app).then(async () => { - if (environment.developer.sslEnabled) { - const path = - (process.env.NX_WORKSPACE_ROOT ?? '.') + - '/apps/chili-and-cilantro-api/localdev/'; - const httpsOptions = { - key: fs.readFileSync(path + 'cert.key'), - cert: fs.readFileSync(path + 'cert.pem'), - }; - server = https.createServer(httpsOptions, app); - server.listen(environment.developer.port, () => { - console.log( - `[ ready ] https://${environment.developer.host}:${environment.developer.port}`, - ); - }); - } else { - server = createServer(app); - server.listen(environment.developer.port, () => { - console.log( - `[ ready ] http://${environment.developer.host}:${environment.developer.port}`, - ); - }); - } +validateEnvironment(environment, async () => { + await app.start(); }); diff --git a/chili-and-cilantro-api/src/middlewares.ts b/chili-and-cilantro-api/src/middlewares.ts new file mode 100644 index 0000000..cf8c9bf --- /dev/null +++ b/chili-and-cilantro-api/src/middlewares.ts @@ -0,0 +1,64 @@ +import { constants } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import cors from 'cors'; +import { Application, json, urlencoded } from 'express'; +import helmet from 'helmet'; + +export class Middlewares { + private static readonly corsWhitelist = [ + 'http://localhost:3000', + 'https://localhost:3000', + `http://${constants.SITE_DOMAIN}`, + `https://${constants.SITE_DOMAIN}`, + ]; + private static readonly corsOptionsDelegate = ( + req: cors.CorsRequest, + callback: ( + error: Error | null, + options: cors.CorsOptions | undefined, + ) => void, + ) => { + const corsOptions: cors.CorsOptions = { + origin: (origin, cb) => { + if (!origin || Middlewares.corsWhitelist.indexOf(origin) !== -1) { + cb(null, true); + } else { + cb(new Error('Not allowed by CORS')); + } + }, + methods: ['GET', 'POST', 'PUT', 'DELETE'], + allowedHeaders: ['Authorization', 'Content-Type'], + maxAge: 86400, + }; + callback(null, corsOptions); + }; + public static init(app: Application): void { + // Helmet helps you secure your Express apps by setting various HTTP headers + app.use( + helmet({ + hsts: { + maxAge: 31536000, + }, + contentSecurityPolicy: { + useDefaults: false, + directives: { + defaultSrc: ["'self'"], + imgSrc: ["'self'"], + connectSrc: ["'self'"], + scriptSrc: ["'self'", "'unsafe-inline'"], + styleSrc: ["'self'", "'unsafe-inline'"], + frameSrc: ["'self'"], + }, + }, + frameguard: { + action: 'deny', + }, + }), + ); + // Enable CORS + app.use(cors(Middlewares.corsOptionsDelegate)); + // Parse incoming requests with JSON payloads + app.use(json()); + // Parse incoming requests with urlencoded payloads + app.use(urlencoded({ extended: true })); + } +} diff --git a/chili-and-cilantro-api/src/middlewares/authenticate-token.ts b/chili-and-cilantro-api/src/middlewares/authenticate-token.ts index 6e9f340..52dce7d 100644 --- a/chili-and-cilantro-api/src/middlewares/authenticate-token.ts +++ b/chili-and-cilantro-api/src/middlewares/authenticate-token.ts @@ -1,13 +1,20 @@ import { AccountStatusTypeEnum, + GetModelFunction, ITokenUser, + IUserDocument, + ModelName, } from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { UserModel } from '@chili-and-cilantro/chili-and-cilantro-node-lib'; import { NextFunction, Request, Response } from 'express'; import { IncomingHttpHeaders } from 'http'; import { JwtService } from '../services/jwt'; import { RequestUserService } from '../services/request-user'; +/** + * Find the auth token in the headers + * @param headers The headers + * @returns The auth token + */ export function findAuthToken(headers: IncomingHttpHeaders): string | null { const authHeader = headers['Authorization'] || headers['authorization']; if (authHeader && typeof authHeader === 'string') { @@ -19,11 +26,21 @@ export function findAuthToken(headers: IncomingHttpHeaders): string | null { return null; } +/** + * Middleware to authenticate a token + * @param getModel Function to get a model + * @param req The request + * @param res The response + * @param next The next function + * @returns The response + */ export function authenticateToken( + getModel: GetModelFunction, req: Request, res: Response, next: NextFunction, -) { +): Response { + const UserModel = getModel(ModelName.User); const token = findAuthToken(req.headers); if (token == null) { return res.status(401).send('No token provided'); diff --git a/chili-and-cilantro-api/src/routers/api.ts b/chili-and-cilantro-api/src/routers/api.ts new file mode 100644 index 0000000..4659df9 --- /dev/null +++ b/chili-and-cilantro-api/src/routers/api.ts @@ -0,0 +1,20 @@ +import { GetModelFunction } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { Connection } from 'mongoose'; +import { GameController } from '../controllers/api/game'; +import { UserController } from '../controllers/api/user'; +import { BaseRouter } from './base'; + +/** + * Router for the API + */ +export class ApiRouter extends BaseRouter { + private readonly userController: UserController; + private readonly gameController: GameController; + constructor(getModel: GetModelFunction, connnection: Connection) { + super(getModel); + this.userController = new UserController(getModel); + this.gameController = new GameController(getModel, connnection); + this.router.use('/user', this.userController.router); + this.router.use('/game', this.gameController.router); + } +} diff --git a/chili-and-cilantro-api/src/routers/app.ts b/chili-and-cilantro-api/src/routers/app.ts new file mode 100644 index 0000000..0347a8e --- /dev/null +++ b/chili-and-cilantro-api/src/routers/app.ts @@ -0,0 +1,86 @@ +import { Application, static as expressStatic } from 'express'; +import { existsSync } from 'fs'; +import { join } from 'path'; +import { ApiRouter } from './api'; + +/** + * Application router + * Sets up the API and static file serving + */ +export class AppRouter { + private static readonly distPath = join( + __dirname, + '..', + '..', + '..', + '..', + 'chili-and-cilantro-react', + ); + private static readonly indexPath = join(AppRouter.distPath, 'index.html'); + private readonly apiRouter: ApiRouter; + + constructor(apiRouter: ApiRouter) { + this.apiRouter = apiRouter; + } + + /** + * Initialize the application router + * @param app The application + * @param debugRoutes Whether to log routes to the console + */ + public init(app: Application, debugRoutes: boolean) { + if (!AppRouter.distPath.includes('/dist/')) { + throw new Error( + `App does not appear to be running within dist: ${AppRouter.distPath}`, + ); + } + if (!existsSync(AppRouter.indexPath)) { + throw new Error(`Index file not found: ${AppRouter.indexPath}`); + } + + if (debugRoutes) { + app.use((req, res, next) => { + console.log(`Serving route: ${req.method} ${req.url}`); + next(); + }); + } + + app.use('/api', this.apiRouter.router); + + // Serve static files from the React app build directory + // app.use(express.static(path.join(__dirname, '..', '..', '..', 'chili-and-cilantro-react'))); + const serveStaticWithLogging = expressStatic(AppRouter.distPath); + app.use((req, res, next) => { + if (debugRoutes) { + console.log(`Trying to serve static for ${req.url}`); + } + serveStaticWithLogging(req, res, (err) => { + if (err) { + console.error('Error serving static file:', err); + next(err); + return; + } + next(); + }); + }); + + // The "catchall" handler: for any request that doesn't + // match one above, send back React's index.html file. + // app.get('*', (req, res) => { + // res.sendFile(path.join(__dirname,'..', '..', '..', 'chili-and-cilantro-react', 'index.html')); + // }); + app.get('*', (req, res) => { + if (debugRoutes) { + console.log(`Attempting to serve: ${AppRouter.indexPath}`); + } + res.sendFile(AppRouter.indexPath, (err) => { + if (err) { + console.error('Error sending file:', err); + if (!res.headersSent) { + res.status(500).send('Error serving the page'); + } + } + }); + }); + } +} diff --git a/chili-and-cilantro-api/src/routers/base.ts b/chili-and-cilantro-api/src/routers/base.ts new file mode 100644 index 0000000..094dda4 --- /dev/null +++ b/chili-and-cilantro-api/src/routers/base.ts @@ -0,0 +1,11 @@ +import { GetModelFunction } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { Router } from 'express'; + +export abstract class BaseRouter { + public readonly router: Router; + public readonly getModel: GetModelFunction; + protected constructor(getModel: GetModelFunction) { + this.router = Router(); + this.getModel = getModel; + } +} diff --git a/chili-and-cilantro-api/src/routes/api.ts b/chili-and-cilantro-api/src/routes/api.ts deleted file mode 100644 index 7bcd009..0000000 --- a/chili-and-cilantro-api/src/routes/api.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Router } from 'express'; -import { gamesRouter } from './games'; -import { usersRouter } from './users'; - -export const apiRouter = Router(); - -apiRouter.use('/games', gamesRouter); -apiRouter.use('/users', usersRouter); diff --git a/chili-and-cilantro-api/src/routes/games.ts b/chili-and-cilantro-api/src/routes/games.ts deleted file mode 100644 index d980daf..0000000 --- a/chili-and-cilantro-api/src/routes/games.ts +++ /dev/null @@ -1,293 +0,0 @@ -import { - CardType, - TurnAction, -} from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { Request, Response, Router } from 'express'; -import { ValidationError } from '../errors/validation-error'; -import { ActionService } from '../services/action'; -import { ChefService } from '../services/chef'; -import { GameService } from '../services/game'; -import { JwtService } from '../services/jwt'; -import { PlayerService } from '../services/player'; -import { UserService } from '../services/user'; - -export const gamesRouter = Router(); - -gamesRouter.post( - '/create', - validateAccessToken, - async (req: Request, res: Response) => { - try { - const userService = new UserService(); - const jwtService = new JwtService(userService); - const token = req.headers.authorization?.split(' ')[1]; - const user = await jwtService.getUserFromValidatedTokenAsync(token); - if (!user) { - return res.status(401).json({ message: 'User not found' }); - } - const { name, userName, password, maxChefs } = req.body; - const sanitizedName = (name as string)?.trim(); - const sanitizedUserName = (userName as string)?.trim(); - const sanitizedPassword = (password as string)?.trim(); - const sanitizedMaxChefs = parseInt(maxChefs, 10); - - const actionService = new ActionService(); - const chefService = new ChefService(); - const playerService = new PlayerService(); - const gameService = new GameService( - actionService, - chefService, - playerService, - ); - const { game, chef } = await gameService.performCreateGameAsync( - user, - sanitizedUserName, - sanitizedName, - sanitizedPassword, - sanitizedMaxChefs, - ); - res.send({ game, chef }); - } catch (error) { - if (error instanceof ValidationError) { - res.status(400).json(error); - } else { - res.status(500).json(error); - } - } - }, -); - -gamesRouter.post( - '/:code/join', - validateAccessToken, - async (req: Request, res: Response) => { - try { - const userService = new UserService(); - const jwtService = new JwtService(userService); - const token = req.headers.authorization?.split(' ')[1]; - const user = await jwtService.getUserFromValidatedTokenAsync(token); - if (!user) { - return res.status(401).json({ message: 'User not found' }); - } - const { userName, password } = req.body; - const gameCode = req.params.code; - const sanitizedUserName = (userName as string)?.trim(); - const sanitizedPassword = (password as string)?.trim(); - - const actionService = new ActionService(); - const chefService = new ChefService(); - const playerService = new PlayerService(); - const gameService = new GameService( - actionService, - chefService, - playerService, - ); - const { game, chef } = await gameService.performJoinGameAsync( - gameCode, - sanitizedPassword, - user, - sanitizedUserName, - ); - res.send({ game, chef }); - } catch (error) { - if (error instanceof ValidationError) { - res.status(400).json(error); - } else { - res.status(500).json(error); - } - } - }, -); - -gamesRouter.post( - '/:code/message', - validateAccessToken, - async (req: Request, res: Response) => { - try { - const userService = new UserService(); - const jwtService = new JwtService(userService); - const token = req.headers.authorization?.split(' ')[1]; - const user = await jwtService.getUserFromValidatedTokenAsync(token); - if (!user) { - return res.status(401).json({ message: 'User not found' }); - } - const { message } = req.body; - const gameCode = req.params.code; - const sanitizedMessage = (message as string)?.trim(); - const actionService = new ActionService(); - const chefService = new ChefService(); - const playerService = new PlayerService(); - const gameService = new GameService( - actionService, - chefService, - playerService, - ); - const messageAction = await gameService.performSendMessageAsync( - gameCode, - user, - sanitizedMessage, - ); - res.status(200).json(messageAction); - } catch (e) { - if (e instanceof ValidationError) { - res.status(400).json(e); - } else { - res.status(500).json(e); - } - } - }, -); - -gamesRouter.get( - '/:code/history', - validateAccessToken, - async (req: Request, res: Response) => { - try { - const userService = new UserService(); - const jwtService = new JwtService(userService); - const token = req.headers.authorization?.split(' ')[1]; - const user = await jwtService.getUserFromValidatedTokenAsync(token); - if (!user) { - return res.status(401).json({ message: 'User not found' }); - } - const gameCode = req.params.code; - const actionService = new ActionService(); - const chefService = new ChefService(); - const playerService = new PlayerService(); - const gameService = new GameService( - actionService, - chefService, - playerService, - ); - const game = await gameService.getGameByCodeOrThrowAsync(gameCode, true); - const actions = await actionService.getGameHistoryAsync(game); - res.status(200).json(actions); - } catch (e) { - if (e instanceof ValidationError) { - res.status(400).json(e); - } else { - res.status(500).json(e); - } - } - }, -); - -gamesRouter.post( - '/:code/start', - validateAccessToken, - async (req: Request, res: Response) => { - try { - const userService = new UserService(); - const jwtService = new JwtService(userService); - const token = req.headers.authorization?.split(' ')[1]; - const user = await jwtService.getUserFromValidatedTokenAsync(token); - if (!user) { - return res.status(401).json({ message: 'User not found' }); - } - const gameCode = req.params.code; - const actionService = new ActionService(); - const chefService = new ChefService(); - const playerService = new PlayerService(); - const gameService = new GameService( - actionService, - chefService, - playerService, - ); - const { game, action } = await gameService.performStartGameAsync( - gameCode, - user._id, - ); - res.status(200).json({ game, action }); - } catch (e) { - if (e instanceof ValidationError) { - res.status(400).json(e); - } else { - res.status(500).json(e); - } - } - }, -); - -/** - * Gets the available actions for the specified game and current user - */ -gamesRouter.get( - '/:code/action', - validateAccessToken, - async (req: Request, res: Response) => { - try { - const userService = new UserService(); - const jwtService = new JwtService(userService); - const token = req.headers.authorization?.split(' ')[1]; - const user = await jwtService.getUserFromValidatedTokenAsync(token); - if (!user) { - return res.status(401).json({ message: 'User not found' }); - } - const gameCode = req.params.code; - const actionService = new ActionService(); - const chefService = new ChefService(); - const playerService = new PlayerService(); - const gameService = new GameService( - actionService, - chefService, - playerService, - ); - const game = await gameService.getGameByCodeOrThrowAsync(gameCode, true); - const chef = await chefService.getGameChefOrThrowAsync(game, user); - const actions = await gameService.availableTurnActions(game, chef); - res.status(200).json(actions); - } catch (e) { - if (e instanceof ValidationError) { - res.status(400).json(e); - } else { - res.status(500).json(e); - } - } - }, -); - -/** - * Performs a turn action for the specified game - */ -gamesRouter.post( - '/:code/action', - validateAccessToken, - async (req: Request, res: Response) => { - try { - const userService = new UserService(); - const jwtService = new JwtService(userService); - const token = req.headers.authorization?.split(' ')[1]; - const user = await jwtService.getUserFromValidatedTokenAsync(token); - if (!user) { - return res.status(401).json({ message: 'User not found' }); - } - const gameCode = req.params.code; - const { action, ingredient, bid } = req.body; - const actionArgs = { - ...(ingredient ? { ingredient: ingredient as CardType } : {}), - ...(bid ? { bid: bid as number } : {}), - }; - const actionService = new ActionService(); - const chefService = new ChefService(); - const playerService = new PlayerService(); - const gameService = new GameService( - actionService, - chefService, - playerService, - ); - const { game, chef } = await gameService.performTurnActionAsync( - gameCode, - user, - action as TurnAction, - actionArgs, - ); - res.status(200).json({ game, chef }); - } catch (e) { - if (e instanceof ValidationError) { - res.status(400).json(e); - } else { - res.status(500).json(e); - } - } - }, -); diff --git a/chili-and-cilantro-api/src/routes/users.ts b/chili-and-cilantro-api/src/routes/users.ts deleted file mode 100644 index 577ea9f..0000000 --- a/chili-and-cilantro-api/src/routes/users.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Request, Response, Router } from 'express'; -import { JwtService } from '../services/jwt'; -import { UserService } from '../services/user'; - -export const usersRouter = Router(); - -usersRouter.post('/register', async (req: Request, res: Response) => { - const { email, username, password } = req.body; - try { - const userService = new UserService(); - await userService.performRegister(email, username, password); - res.status(201).json({ - message: 'User created successfully', - email: email, - username: username, - }); - } catch (error) { - res.status(400).json(error); - } -}); - -usersRouter.post( - '/validate', - validateAccessToken, - async (req: Request, res: Response) => { - try { - const userService = new UserService(); - const jwtService = new JwtService(userService); - jwtService.authenticateUserAsync(req, res, async (user, auth0User) => { - if (auth0User.email_verified && user.email_verified === false) { - user.email_verified = true; - await user.save(); - } - res - .status(200) - .json({ message: 'User validated successfully', user: user }); - }); - } catch (error) { - res.status(400).json(error); - } - }, -); - -export default usersRouter; diff --git a/chili-and-cilantro-api/src/services/action.ts b/chili-and-cilantro-api/src/services/action.ts index c25e604..1c15781 100644 --- a/chili-and-cilantro-api/src/services/action.ts +++ b/chili-and-cilantro-api/src/services/action.ts @@ -2,7 +2,9 @@ import { ActionType, CardType, constants, + GetModelFunction, IAction, + IActionDocument, IChefDocument, ICreateGameAction, ICreateGameActionDocument, @@ -23,6 +25,9 @@ import { IPlaceCardAction, IPlaceCardActionDocument, IPlaceCardDetails, + IQuitGameAction, + IQuitGameActionDocument, + IQuitGameDetails, IStartBiddingAction, IStartBiddingActionDocument, IStartBiddingDetails, @@ -30,26 +35,31 @@ import { IStartGameActionDocument, IStartGameDetails, IUserDocument, + ModelName, } from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { - ActionDiscriminatorsByActionType, - ActionModel, -} from '@chili-and-cilantro/chili-and-cilantro-node-lib'; +import { ActionDiscriminatorsByActionType } from '@chili-and-cilantro/chili-and-cilantro-node-lib'; +import { Connection, Model } from 'mongoose'; export class ActionService { - constructor() {} + private readonly getModel: GetModelFunction; + private readonly actionDiscriminators: { [key in ActionType]: Model }; + + constructor(getModel: GetModelFunction, connection: Connection) { + this.getModel = getModel; + this.actionDiscriminators = ActionDiscriminatorsByActionType(connection); + } public async getGameHistoryAsync(game: IGameDocument): Promise { - const actions = await ActionModel.find({ gameId: game._id }).sort({ + const ActionModel = this.getModel(ModelName.Action); + return ActionModel.find({ gameId: game._id }).sort({ createdAt: 1, }); - return actions; } public async createGameAsync( game: IGameDocument, chef: IChefDocument, user: IUserDocument, ): Promise { - const result = await ActionDiscriminatorsByActionType.CREATE_GAME.create({ + return this.actionDiscriminators.CREATE_GAME.create({ gameId: game._id, chefId: chef._id, userId: user._id, @@ -57,14 +67,13 @@ export class ActionService { details: {} as ICreateGameDetails, round: constants.NONE, } as ICreateGameAction); - return result; } public async joinGameAsync( game: IGameDocument, chef: IChefDocument, user: IUserDocument, ): Promise { - const result = await ActionDiscriminatorsByActionType.JOIN_GAME.create({ + return this.actionDiscriminators.JOIN_GAME.create({ gameId: game._id, chefId: chef._id, userId: user._id, @@ -72,12 +81,11 @@ export class ActionService { details: {} as IJoinGameDetails, round: constants.NONE, } as IJoinGameAction); - return result; } public async startGameAsync( game: IGameDocument, ): Promise { - const result = await ActionDiscriminatorsByActionType.START_GAME.create({ + return this.actionDiscriminators.START_GAME.create({ gameId: game._id, chefId: game.hostChefId, userId: game.hostUserId, @@ -85,12 +93,11 @@ export class ActionService { details: {} as IStartGameDetails, round: game.currentRound, } as IStartGameAction); - return result; } public async expireGameAsync( game: IGameDocument, ): Promise { - const result = ActionDiscriminatorsByActionType.EXPIRE_GAME.create({ + return this.actionDiscriminators.EXPIRE_GAME.create({ gameId: game._id, chefId: game.hostChefId, userId: game.hostUserId, @@ -98,14 +105,13 @@ export class ActionService { details: {} as IExpireGameDetails, round: game.currentRound, } as IExpireGameAction); - return result; } public async sendMessageAsync( game: IGameDocument, chef: IChefDocument, message: string, ): Promise { - const result = await ActionDiscriminatorsByActionType.MESSAGE.create({ + return this.actionDiscriminators.MESSAGE.create({ gameId: game._id, chefId: chef._id, userId: chef.userId, @@ -115,14 +121,13 @@ export class ActionService { } as IMessageDetails, round: game.currentRound, } as IMessageAction); - return result; } public async startBiddingAsync( game: IGameDocument, chef: IChefDocument, bid: number, ): Promise { - const result = await ActionDiscriminatorsByActionType.START_BIDDING.create({ + return this.actionDiscriminators.START_BIDDING.create({ gameId: game._id, chefId: chef._id, userId: chef.userId, @@ -132,13 +137,12 @@ export class ActionService { } as IStartBiddingDetails, round: game.currentRound, } as IStartBiddingAction); - return result; } public async passAsync( game: IGameDocument, chef: IChefDocument, ): Promise { - const result = await ActionDiscriminatorsByActionType.PASS.create({ + return this.actionDiscriminators.PASS.create({ gameId: game._id, chefId: chef._id, userId: chef.userId, @@ -146,7 +150,6 @@ export class ActionService { details: {} as IPassDetails, round: game.currentRound, } as IPassAction); - return result; } public async placeCardAsync( game: IGameDocument, @@ -154,7 +157,7 @@ export class ActionService { cardType: CardType, position: number, ): Promise { - const result = await ActionDiscriminatorsByActionType.PLACE_CARD.create({ + return this.actionDiscriminators.PLACE_CARD.create({ gameId: game._id, chefId: chef._id, userId: chef.userId, @@ -165,6 +168,19 @@ export class ActionService { } as IPlaceCardDetails, round: game.currentRound, } as IPlaceCardAction); - return result; + } + + public async quitGameAsync( + game: IGameDocument, + chef: IChefDocument, + ): Promise { + return this.actionDiscriminators.QUIT_GAME.create({ + gameId: game._id, + chefId: chef._id, + userId: chef.userId, + type: ActionType.QUIT_GAME, + details: {} as IQuitGameDetails, + round: game.currentRound, + } as IQuitGameAction); } } diff --git a/chili-and-cilantro-api/src/services/chef.ts b/chili-and-cilantro-api/src/services/chef.ts index 4b9ff8c..b420c10 100644 --- a/chili-and-cilantro-api/src/services/chef.ts +++ b/chili-and-cilantro-api/src/services/chef.ts @@ -1,18 +1,21 @@ import { ChefState, - IChef, + DefaultIdType, + GetModelFunction, IChefDocument, - IGame, IGameDocument, IUserDocument, + ModelName, + NotInGameError, } from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { ChefModel } from '@chili-and-cilantro/chili-and-cilantro-node-lib'; -import { Document, Types } from 'mongoose'; -import { NotInGameError } from '../errors/not-in-game'; +import { Types } from 'mongoose'; import { UtilityService } from './utility'; export class ChefService { - constructor() {} + public readonly getModel: GetModelFunction; + constructor(getModel: GetModelFunction) { + this.getModel = getModel; + } /** * Creates a new chef in the database @@ -28,9 +31,10 @@ export class ChefService { user: IUserDocument, userName: string, host: boolean, - chefId?: Types.ObjectId, + chefId?: DefaultIdType, ): Promise { - const chef = await ChefModel.create({ + const ChefModel = this.getModel(ModelName.Chef); + return ChefModel.create({ _id: chefId ?? new Types.ObjectId(), gameId: game._id, name: userName, @@ -41,7 +45,6 @@ export class ChefService { state: ChefState.LOBBY, host: host, }); - return chef; } /** @@ -54,9 +57,10 @@ export class ChefService { public async newChefFromExisting( newGame: IGameDocument, existingChef: IChefDocument, - newChefId?: Types.ObjectId, + newChefId?: DefaultIdType, ): Promise { - const newChef = await ChefModel.create({ + const ChefModel = this.getModel(ModelName.Chef); + return ChefModel.create({ _id: newChefId ?? new Types.ObjectId(), gameId: newGame._id, name: existingChef.name, @@ -67,7 +71,6 @@ export class ChefService { state: ChefState.LOBBY, host: existingChef.host, }); - return newChef; } /** @@ -79,7 +82,8 @@ export class ChefService { public async getGameChefOrThrowAsync( game: IGameDocument, user: IUserDocument, - ): Promise { + ): Promise { + const ChefModel = this.getModel(ModelName.Chef); const chef = await ChefModel.findOne({ gameId: game._id, userId: user._id, @@ -96,14 +100,11 @@ export class ChefService { * @returns An array of chef documents */ public async getGameChefsByGameOrIdAsync( - gameOrId: string | IGame, - ): Promise<(IChef & Document)[]> { - // verify that gameOrId is either a string or an IGame by checking whether there's an _id property - const hasId = (obj: any): obj is IGame => { - return obj._id !== undefined; - }; - const gameId = hasId(gameOrId) ? gameOrId._id.toString() : gameOrId; - const chefs = await ChefModel.find({ gameId: gameId }); - return chefs; + gameOrId: string | IGameDocument, + ): Promise { + const ChefModel = this.getModel(ModelName.Chef); + const gameId = + typeof gameOrId === 'string' ? gameOrId : gameOrId._id.toString(); + return ChefModel.find({ gameId: new Types.ObjectId(gameId) }); } } diff --git a/chili-and-cilantro-api/src/services/game.ts b/chili-and-cilantro-api/src/services/game.ts index ee2a896..797d5b5 100644 --- a/chili-and-cilantro-api/src/services/game.ts +++ b/chili-and-cilantro-api/src/services/game.ts @@ -1,38 +1,42 @@ import { + AllCardsPlacedError, + AlreadyJoinedOtherError, CardType, ChefState, constants, + DefaultIdType, + GameFullError, + GameInProgressError, + GamePasswordMismatchError, GamePhase, + GetModelFunction, IBid, IChefDocument, ICreateGameActionDocument, IGameDocument, IMessageActionDocument, - IStartGameActionDocument, + IncorrectGamePhaseError, + InvalidActionError, + InvalidGameError, + InvalidGameNameError, + InvalidGameParameterError, + InvalidGamePasswordError, + InvalidMessageError, + InvalidUserDisplayNameError, IUserDocument, + ModelName, + NotEnoughChefsError, + NotHostError, + OutOfIngredientError, + OutOfOrderError, TurnAction, + UsernameInUseError, } from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { GameModel } from '@chili-and-cilantro/chili-and-cilantro-node-lib'; import { Types } from 'mongoose'; import validator from 'validator'; -import { AllCardsPlacedError } from '../errors/all-cards-placed'; -import { AlreadyJoinedOtherError } from '../errors/already-joined-other'; -import { GameFullError } from '../errors/game-full'; -import { GameInProgressError } from '../errors/game-in-progress'; -import { GamePasswordMismatchError } from '../errors/game-password-mismatch'; -import { IncorrectGamePhaseError } from '../errors/incorrect-game-phase'; -import { InvalidActionError } from '../errors/invalid-action'; -import { InvalidGameError } from '../errors/invalid-game'; -import { InvalidGameNameError } from '../errors/invalid-game-name'; -import { InvalidGameParameterError } from '../errors/invalid-game-parameter'; -import { InvalidGamePasswordError } from '../errors/invalid-game-password'; -import { InvalidMessageError } from '../errors/invalid-message'; -import { InvalidUserDisplayNameError } from '../errors/invalid-user-display-name'; -import { NotEnoughChefsError } from '../errors/not-enough-chefs'; -import { NotHostError } from '../errors/not-host'; -import { OutOfIngredientError } from '../errors/out-of-ingredient'; -import { OutOfOrderError } from '../errors/out-of-order'; -import { UsernameInUseError } from '../errors/username-in-use'; +import { IBidIngredient } from '../interfaces/bid-ingredient'; +import { IGameAction } from '../interfaces/game-action'; +import { IGameChef } from '../interfaces/game-chef'; import { ActionService } from './action'; import { ChefService } from './chef'; import { PlayerService } from './player'; @@ -40,16 +44,19 @@ import { TransactionManager } from './transaction-manager'; import { UtilityService } from './utility'; export class GameService extends TransactionManager { + private readonly getModel: GetModelFunction; private readonly actionService: ActionService; private readonly chefService: ChefService; private readonly playerService: PlayerService; constructor( + getModel: GetModelFunction, actionService: ActionService, chefService: ChefService, playerService: PlayerService, ) { super(); + this.getModel = getModel; this.actionService = actionService; this.chefService = chefService; this.playerService = playerService; @@ -61,6 +68,7 @@ export class GameService extends TransactionManager { * @returns string game code */ public async generateNewGameCodeAsync(): Promise { + const GameModel = this.getModel(ModelName.Game); // find a game code that is not being used by an active game // codes are freed up when currentPhase is GAME_OVER let code = ''; @@ -129,7 +137,6 @@ export class GameService extends TransactionManager { if (maxChefs < constants.MIN_CHEFS || maxChefs > constants.MAX_CHEFS) { throw new InvalidGameParameterError( `Must be between ${constants.MIN_CHEFS} and ${constants.MAX_CHEFS}.`, - 'maxChefs', ); } } @@ -154,6 +161,7 @@ export class GameService extends TransactionManager { chef: IChefDocument; action: ICreateGameActionDocument; }> { + const GameModel = this.getModel(ModelName.Game); const gameId = new Types.ObjectId(); const chefId = new Types.ObjectId(); const gameCode = await this.generateNewGameCodeAsync(); @@ -201,7 +209,7 @@ export class GameService extends TransactionManager { gameName: string, password: string, maxChefs: number, - ): Promise<{ game: IGameDocument; chef: IChefDocument }> { + ): Promise { return this.withTransaction(async (session) => { await this.validateCreateGameOrThrowAsync( user, @@ -217,17 +225,16 @@ export class GameService extends TransactionManager { /** * Joins the player to the specified game and creates a chef object for them * A password is not needed as it is validated earlier. - * @param gameCode - * @param password - * @param user - * @param userName - * @returns + * @param game The game to join + * @param user The user joining + * @param userName The display name for the user + * @returns A tuple of the game and chef objects */ public async joinGameAsync( game: IGameDocument, user: IUserDocument, userName: string, - ): Promise<{ game: IGameDocument; chef: IChefDocument }> { + ): Promise { const chef = await this.chefService.newChefAsync( game, user, @@ -292,7 +299,7 @@ export class GameService extends TransactionManager { password: string, user: IUserDocument, userName: string, - ): Promise<{ game: IGameDocument; chef: IChefDocument }> { + ): Promise { return this.withTransaction(async (session) => { const game = await this.getGameByCodeOrThrowAsync(gameCode, true); await this.validateJoinGameOrThrowAsync(game, user, userName, password); @@ -303,7 +310,8 @@ export class GameService extends TransactionManager { public async createNewGameFromExistingAsync( existingGame: IGameDocument, user: IUserDocument, - ): Promise<{ game: IGameDocument; chef: IChefDocument }> { + ): Promise { + const GameModel = this.getModel(ModelName.Game); const newChefIds = existingGame.chefIds.map(() => new Types.ObjectId()); // find the existing chef id's index @@ -317,7 +325,9 @@ export class GameService extends TransactionManager { await this.chefService.getGameChefsByGameOrIdAsync(existingGame); // Create the new Game document without persisting to the database yet - const newGame = new GameModel({ + const newGameId = new Types.ObjectId(); + const newGame = await GameModel.create({ + _id: newGameId, chefIds: newChefIds, code: existingGame.code, cardsPlaced: 0, @@ -351,15 +361,12 @@ export class GameService extends TransactionManager { // Execute all creations concurrently const chefs = await Promise.all(chefCreations); - // Persist the new game - const savedNewGame = await newGame.save(); - // Create action for game creation - this could be moved to an event or a method to encapsulate the logic const hostChef = chefs.find( (chef) => chef._id.toString() == newHostChefId.toString(), ); - await this.actionService.createGameAsync(savedNewGame, hostChef, user); - return { game: savedNewGame, chef: chefs[hostChefIndex] }; + await this.actionService.createGameAsync(newGame, hostChef, user); + return { game: newGame, chef: chefs[hostChefIndex] }; } /** @@ -377,12 +384,13 @@ export class GameService extends TransactionManager { /** * Creates a new game from a completed game. The new game will have the same code, name, password, and players as the existing game. * @param existingGameId The ID of the existing game + * @param user The user creating the new game * @returns A tuple of the new game and chef objects */ public async performCreateNewGameFromExistingAsync( - existingGameId: Types.ObjectId, + existingGameId: DefaultIdType, user: IUserDocument, - ): Promise<{ game: IGameDocument; chef: IChefDocument }> { + ): Promise { return this.withTransaction(async (session) => { const existingGame = await this.getGameByIdOrThrowAsync( existingGameId, @@ -398,10 +406,7 @@ export class GameService extends TransactionManager { * @param game The game to start * @returns A tuple of the game and the start game action */ - public async startGameAsync(game: IGameDocument): Promise<{ - game: IGameDocument; - action: IStartGameActionDocument; - }> { + public async startGameAsync(game: IGameDocument): Promise { // set the current bid to 0 game.currentBid = 0; // set the current chef to the first player @@ -432,7 +437,7 @@ export class GameService extends TransactionManager { */ public async validateStartGameOrThrowAsync( game: IGameDocument, - userId: Types.ObjectId, + userId: DefaultIdType, ): Promise { if (!(await this.playerService.isGameHostAsync(userId, game._id))) { throw new NotHostError(); @@ -447,16 +452,14 @@ export class GameService extends TransactionManager { /** * Starts the specified game - * @param gameId + * @param gameCode The code of the game to start + * @param userId The ID of the user starting the game * @returns */ public async performStartGameAsync( gameCode: string, - userId: Types.ObjectId, - ): Promise<{ - game: IGameDocument; - action: IStartGameActionDocument; - }> { + userId: DefaultIdType, + ): Promise { return this.withTransaction(async (session) => { const game = await this.getGameByCodeOrThrowAsync(gameCode, true); await this.validateStartGameOrThrowAsync(game, userId); @@ -466,13 +469,15 @@ export class GameService extends TransactionManager { /** * Gets a Game model by game ID - * @param gameId + * @param gameId The ID of the game + * @param active Whether the game must be active * @returns Game model */ public async getGameByIdOrThrowAsync( - gameId: Types.ObjectId, + gameId: DefaultIdType, active = false, ): Promise { + const GameModel = this.getModel(ModelName.Game); const search = active ? { _id: gameId, @@ -488,13 +493,15 @@ export class GameService extends TransactionManager { /** * Gets a Game model by game code - * @param gameCode + * @param gameCode The code of the game + * @param active Whether the game must be active * @returns Game model */ public async getGameByCodeOrThrowAsync( gameCode: string, active = false, ): Promise { + const GameModel = this.getModel(ModelName.Game); // Construct the search criteria const search = active ? { code: gameCode, currentPhase: { $ne: GamePhase.GAME_OVER } } @@ -541,6 +548,7 @@ export class GameService extends TransactionManager { * Finds games not in GAME_OVER phase with a lastModified date older than MAX_GAME_AGE_WITHOUT_ACTIVITY_IN_MINUTES and marks them GAME_OVER */ public async performExpireOldGamesAsync(): Promise { + const GameModel = this.getModel(ModelName.Game); return this.withTransaction(async (session) => { // find games not in GAME_OVER phase with a lastModified date older than MAX_GAME_AGE_WITHOUT_ACTIVITY_IN_MINUTES // cutoffDate is now minus MAX_GAME_AGE_WITHOUT_ACTIVITY_IN_MINUTES @@ -554,7 +562,7 @@ export class GameService extends TransactionManager { lastModified: { $lt: cutoffDate }, }); if (games && games.length > 0) { - this.expireGamesOrThrowAsync(games); + await this.expireGamesOrThrowAsync(games); } }); } @@ -621,8 +629,9 @@ export class GameService extends TransactionManager { /** * Return whether the current chef can bid - * @param gameCode - * @param user + * @param game The game being evaluated + * @param chef The chef being evaluated + * @returns boolean with whether the chef can bid */ public canBid(game: IGameDocument, chef: IChefDocument): boolean { // current phase must be SETUP or BIDDING @@ -748,7 +757,7 @@ export class GameService extends TransactionManager { * @throws Error if currentChef is invalid * @returns ObjectID of the current chef */ - public getGameCurrentChefId(game: IGameDocument): Types.ObjectId { + public getGameCurrentChefId(game: IGameDocument): DefaultIdType { if (game.currentChef < 0 || game.currentChef >= game.turnOrder.length) { throw new Error(`Invalid current chef index: ${game.currentChef}`); } @@ -813,7 +822,7 @@ export class GameService extends TransactionManager { game: IGameDocument, chef: IChefDocument, ingredient: CardType, - ): Promise<{ game: IGameDocument; chef: IChefDocument }> { + ): Promise { // remove one card of the specified type from the chef's hand const ingredientIndex = chef.hand.findIndex( (card) => card.type == ingredient, @@ -836,16 +845,16 @@ export class GameService extends TransactionManager { /** * When the game is in setup phase, the current player can place a card or make a bid. This method places a card. - * @param gameCode - * @param user - * @param ingredient + * @param game The game to place the card in + * @param chef The chef placing the card + * @param ingredient The ingredient to place * @returns A tuple of the updated game and chef objects */ public async performPlaceIngredientAsync( game: IGameDocument, chef: IChefDocument, ingredient: CardType, - ): Promise<{ game: IGameDocument; chef: IChefDocument }> { + ): Promise { this.validatePlaceIngredientOrThrow(game, chef, ingredient); return this.withTransaction(async (session) => { return this.placeIngredientAsync(game, chef, ingredient); @@ -882,7 +891,7 @@ export class GameService extends TransactionManager { game: IGameDocument, chef: IChefDocument, bid: number, - ): Promise<{ game: IGameDocument; chef: IChefDocument }> { + ): Promise { const firstBid = game.currentBid <= 0; // set the current bid game.currentBid = bid; @@ -914,15 +923,15 @@ export class GameService extends TransactionManager { /** * Rather than placing a card, the current player can make a bid. This method makes a bid. * The game phase will move from SETUP to BIDDING - * @param gameCode - * @param user - * @param bid + * @param game The game to bid in + * @param chef The chef making the bid + * @param bid The bid to make */ public async performMakeBidAsync( game: IGameDocument, chef: IChefDocument, bid: number, - ): Promise<{ game: IGameDocument; chef: IChefDocument }> { + ): Promise { this.validateMakeBidOrThrow(game, chef, bid); return this.makeBidAsync(game, chef, bid); } @@ -1009,7 +1018,7 @@ export class GameService extends TransactionManager { public async passAsync( game: IGameDocument, chef: IChefDocument, - ): Promise<{ game: IGameDocument; chef: IChefDocument }> { + ): Promise { // create pass event/action await this.actionService.passAsync(game, chef); // increment the current chef @@ -1036,7 +1045,7 @@ export class GameService extends TransactionManager { public async performPassAsync( game: IGameDocument, chef: IChefDocument, - ): Promise<{ game: IGameDocument; chef: IChefDocument }> { + ): Promise { this.validatePerformPassOrThrow(game, chef); return this.passAsync(game, chef); } @@ -1053,8 +1062,8 @@ export class GameService extends TransactionManager { gameCode: string, user: IUserDocument, action: TurnAction, - value?: { bid?: number; ingredient?: CardType }, - ): Promise<{ game: IGameDocument; chef: IChefDocument }> { + value?: IBidIngredient, + ): Promise { return this.withTransaction(async (session) => { const game = await this.getGameByCodeOrThrowAsync(gameCode, true); if (game.chefIds[game.currentChef].toString() !== user._id.toString()) { @@ -1066,7 +1075,7 @@ export class GameService extends TransactionManager { if (!availableActions.includes(action)) { throw new InvalidActionError(action, value?.bid, value?.ingredient); } - let result: { game: IGameDocument; chef: IChefDocument } = undefined; + let result: IGameChef | undefined = undefined; switch (action) { case TurnAction.Bid: case TurnAction.IncreaseBid: diff --git a/chili-and-cilantro-api/src/services/jwt.ts b/chili-and-cilantro-api/src/services/jwt.ts index 8176a40..965fdfe 100644 --- a/chili-and-cilantro-api/src/services/jwt.ts +++ b/chili-and-cilantro-api/src/services/jwt.ts @@ -26,7 +26,6 @@ export class JwtService { if (!userDoc._id) { throw new Error('User ID is required to sign JWT token'); } - // look for roles the user is a member of (the role contains the user id in the user's roles array) const tokenUser: ITokenUser = { userId: userDoc._id.toString(), }; @@ -43,8 +42,8 @@ export class JwtService { /** * Verify a JWT token and return the user data - * @param token - * @returns + * @param token The JWT token + * @returns The user data */ public async verifyToken(token: string): Promise { try { @@ -55,8 +54,7 @@ export class JwtService { if ( typeof decoded === 'object' && decoded !== null && - 'userId' in decoded && - 'roles' in decoded + 'userId' in decoded ) { return { userId: decoded.userId as string, diff --git a/chili-and-cilantro-api/src/services/player.ts b/chili-and-cilantro-api/src/services/player.ts index 226acb0..8d22e14 100644 --- a/chili-and-cilantro-api/src/services/player.ts +++ b/chili-and-cilantro-api/src/services/player.ts @@ -1,15 +1,18 @@ import { + DefaultIdType, GamePhase, + GetModelFunction, + IGameDocument, IUserDocument, + ModelName, } from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { - GameModel, - Schema, -} from '@chili-and-cilantro/chili-and-cilantro-node-lib'; -import { Types } from 'mongoose'; +import { Schema } from '@chili-and-cilantro/chili-and-cilantro-node-lib'; export class PlayerService { - constructor() {} + private readonly getModel: GetModelFunction; + constructor(getModel: GetModelFunction) { + this.getModel = getModel; + } /** * Returns whether the specified user is the host of the specified game @@ -18,9 +21,10 @@ export class PlayerService { * @returns boolean */ public async isGameHostAsync( - userId: Types.ObjectId, - gameId: Types.ObjectId, + userId: DefaultIdType, + gameId: DefaultIdType, ): Promise { + const GameModel = this.getModel(ModelName.Game); try { const count = await GameModel.countDocuments({ _id: gameId, @@ -36,12 +40,13 @@ export class PlayerService { /** * Returns whether the specified user is in any active game - * @param userId + * @param user The user document * @returns boolean */ public async userIsInAnyActiveGameAsync( user: IUserDocument, ): Promise { + const GameModel = this.getModel(ModelName.Game); try { const result = await GameModel.aggregate([ { @@ -82,15 +87,17 @@ export class PlayerService { /** * Returns whether the user is in the specified game, regardless of game state - * @param userId - * @param gameId + * @param userId The user id + * @param gameId The game id + * @param active Whether the game must be active * @returns boolean */ public async userIsInGameAsync( - userId: Types.ObjectId, - gameId: Types.ObjectId, + userId: DefaultIdType, + gameId: DefaultIdType, active = false, ): Promise { + const GameModel = this.getModel(ModelName.Game); try { const result = await GameModel.aggregate([ { diff --git a/chili-and-cilantro-api/src/services/request-user.ts b/chili-and-cilantro-api/src/services/request-user.ts index 3d44f7d..6d60175 100644 --- a/chili-and-cilantro-api/src/services/request-user.ts +++ b/chili-and-cilantro-api/src/services/request-user.ts @@ -7,21 +7,19 @@ export class RequestUserService { /** * Given a user document and an array of role documents, create the IRequestUser * @param userDoc - * @param roles * @returns */ public static makeRequestUser(userDoc: IUserDocument): IRequestUser { if (!userDoc._id) { throw new Error('User document is missing _id'); } - const requestUser: IRequestUser = { + return { id: userDoc._id.toString(), email: userDoc.email, username: userDoc.username, timezone: userDoc.timezone, lastLogin: userDoc.lastLogin, emailVerified: userDoc.emailVerified, - }; - return requestUser; + } as IRequestUser; } } diff --git a/chili-and-cilantro-api/src/services/user.ts b/chili-and-cilantro-api/src/services/user.ts index 83ca268..3023383 100644 --- a/chili-and-cilantro-api/src/services/user.ts +++ b/chili-and-cilantro-api/src/services/user.ts @@ -4,12 +4,14 @@ import { AccountStatusError, AccountStatusTypeEnum, constants, + DefaultIdType, EmailInUseError, EmailTokenExpiredError, EmailTokenSentTooRecentlyError, EmailTokenType, EmailTokenUsedOrInvalidError, EmailVerifiedError, + GetModelFunction, ICreateUserBasics, IEmailTokenDocument, InvalidCredentialsError, @@ -19,15 +21,12 @@ import { IUser, IUserDocument, IUserObject, + ModelName, PendingEmailVerificationError, UsernameInUseError, UsernameOrEmailRequiredError, UserNotFoundError, } from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { - EmailTokenModel, - UserModel, -} from '@chili-and-cilantro/chili-and-cilantro-node-lib'; import { MailDataRequired, MailService } from '@sendgrid/mail'; import { compare, hashSync } from 'bcrypt'; import { randomBytes } from 'crypto'; @@ -36,28 +35,34 @@ import { environment } from '../environment'; import { MongooseValidationError } from '../errors/mongoose-validation-error'; export class UserService { - private sendgridClient: MailService; - constructor() { + private readonly getModel: GetModelFunction; + private readonly sendgridClient: MailService; + constructor(getModel: GetModelFunction) { + this.getModel = getModel; this.sendgridClient = new MailService(); this.sendgridClient.setApiKey(environment.sendgridKey); } /** * Create a new email token to send to the user for email verification - * @param userDoc - * @returns + * @param userDoc The user to create the token for + * @param type The type of email token + * @returns The email token */ public async createEmailToken( userDoc: IUserDocument, type: EmailTokenType, ): Promise { + const EmailTokenModel = this.getModel( + ModelName.EmailToken, + ); // delete any expired tokens for the same user and email to prevent index constraint conflicts await EmailTokenModel.deleteMany({ userId: userDoc.id, email: userDoc.email, expiresAt: { $lt: new Date() }, }); - const emailToken: IEmailTokenDocument = await EmailTokenModel.create({ + return EmailTokenModel.create({ userId: userDoc.id, type: type, email: userDoc.email, @@ -66,12 +71,11 @@ export class UserService { createdAt: Date.now(), expiresAt: new Date(Date.now() + constants.EMAIL_TOKEN_EXPIRATION), }); - return emailToken; } /** * Send an email token to the user for email verification - * @param token + * @param emailToken The email token to send */ public async sendEmailToken(emailToken: IEmailTokenDocument): Promise { if ( @@ -133,6 +137,7 @@ export class UserService { email?: string, username?: string, ): Promise { + const UserModel = this.getModel(ModelName.User); let userDoc: IUserDocument | null; if (username) { @@ -175,12 +180,13 @@ export class UserService { /** * Fill in the default values to a user object - * @param newUser + * @param newUser The user's basic information + * @param createdBy The user that created the user * @returns */ public fillUserDefaults( newUser: ICreateUserBasics, - createdBy?: Types.ObjectId, + createdBy?: DefaultIdType, ): IUserObject { const now = new Date(); const userId = new Types.ObjectId(); @@ -192,7 +198,7 @@ export class UserService { email: newUser.email.toLowerCase(), emailVerified: false, accountStatusType: AccountStatusTypeEnum.NewUnverified, - password: 'willbereplaced', + password: '', createdAt: now, createdBy: createdById, updatedAt: now, @@ -207,10 +213,10 @@ export class UserService { * @returns */ public makeUserDoc(newUser: IUser, password: string): IUserDocument { - const hashedPassword = hashSync(password, constants.BCRYPT_ROUNDS); + const UserModel = this.getModel(ModelName.User); const newUserData: IUser = { ...newUser, - password: hashedPassword, + password: hashSync(password, constants.BCRYPT_ROUNDS), } as IUser; const newUserDoc: IUserDocument = new UserModel(newUserData); @@ -224,16 +230,15 @@ export class UserService { /** * Create a new user - * @param username - * @param email - * @param password - * @param timezone + * @param userData The user's basic information + * @param password The user's password * @returns */ public async newUser( userData: ICreateUserBasics, password: string, ): Promise { + const UserModel = this.getModel(ModelName.User); if (!constants.USERNAME_REGEX.test(userData.username)) { throw new InvalidUsernameError(); } @@ -279,6 +284,9 @@ export class UserService { * @param userId */ public async resendEmailToken(userId: string): Promise { + const EmailTokenModel = this.getModel( + ModelName.EmailToken, + ); const now = new Date(); const minLastSentTime = new Date( now.getTime() - constants.EMAIL_TOKEN_RESEND_INTERVAL, @@ -307,6 +315,9 @@ export class UserService { * @returns */ public async verifyEmailToken(emailToken: string): Promise { + const EmailTokenModel = this.getModel( + ModelName.EmailToken, + ); const token: IEmailTokenDocument | null = await EmailTokenModel.findOne({ token: emailToken, }); @@ -328,6 +339,10 @@ export class UserService { * @param emailToken */ public async verifyEmailTokenAndFinalize(emailToken: string): Promise { + const EmailTokenModel = this.getModel( + ModelName.EmailToken, + ); + const UserModel = this.getModel(ModelName.User); const token: IEmailTokenDocument | null = await EmailTokenModel.findOne({ token: emailToken, }); @@ -370,6 +385,7 @@ export class UserService { currentPassword: string, newPassword: string, ): Promise { + const UserModel = this.getModel(ModelName.User); const user: IUserDocument | null = await UserModel.findById(userId); if (!user) { throw new UserNotFoundError(); @@ -384,8 +400,7 @@ export class UserService { throw new InvalidPasswordError(); } - const hashedPassword = hashSync(newPassword, constants.BCRYPT_ROUNDS); - user.password = hashedPassword; + user.password = hashSync(newPassword, constants.BCRYPT_ROUNDS); await user.save(); } @@ -397,6 +412,7 @@ export class UserService { public async initiatePasswordReset( email: string, ): Promise<{ success: boolean; message: string }> { + const UserModel = this.getModel(ModelName.User); try { const user = await UserModel.findOne({ email: email.toLowerCase() }); if (!user) { @@ -445,6 +461,9 @@ export class UserService { public async validatePasswordResetToken( token: string, ): Promise { + const EmailTokenModel = this.getModel( + ModelName.EmailToken, + ); const emailToken = await EmailTokenModel.findOne({ token, type: EmailTokenType.PasswordReset, @@ -469,6 +488,10 @@ export class UserService { token: string, password: string, ): Promise { + const EmailTokenModel = this.getModel( + ModelName.EmailToken, + ); + const UserModel = this.getModel(ModelName.User); const emailToken = await EmailTokenModel.findOne({ token, type: EmailTokenType.PasswordReset, @@ -486,10 +509,8 @@ export class UserService { } // Hash the new password - const hashedPassword = hashSync(password, constants.BCRYPT_ROUNDS); - // Update the user's password - user.password = hashedPassword; + user.password = hashSync(password, constants.BCRYPT_ROUNDS); await user.save(); // Delete the used token diff --git a/chili-and-cilantro-api/src/services/utility.ts b/chili-and-cilantro-api/src/services/utility.ts index ab40357..ccd1d71 100644 --- a/chili-and-cilantro-api/src/services/utility.ts +++ b/chili-and-cilantro-api/src/services/utility.ts @@ -3,6 +3,7 @@ import { constants, ICard, } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { randomBytes } from 'crypto'; export abstract class UtilityService { /** @@ -11,9 +12,10 @@ export abstract class UtilityService { */ public static generateGameCode(): string { const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const bytes = randomBytes(constants.GAME_CODE_LENGTH); let code = ''; for (let i = 0; i < constants.GAME_CODE_LENGTH; i++) { - code += letters.charAt(Math.floor(Math.random() * letters.length)); + code += letters[bytes[i] % letters.length]; } return code; } diff --git a/chili-and-cilantro-api/src/setup-database.ts b/chili-and-cilantro-api/src/setup-database.ts deleted file mode 100644 index 4d9b415..0000000 --- a/chili-and-cilantro-api/src/setup-database.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ModelName } from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { Schema } from '@chili-and-cilantro/chili-and-cilantro-node-lib'; -import { ISchemaModelData } from 'chili-and-cilantro-node-lib/src/lib/interfaces/schema-model-data'; -import mongoose, { connect, set } from 'mongoose'; -import { environment } from './environment'; - -export async function setupDatabase(): Promise<{ - db: mongoose.Mongoose; - schema: Record>; -}> { - set('strictQuery', true); - const db = await connect(environment.mongo.uri, { - socketTimeoutMS: 30000, - connectTimeoutMS: 30000, - waitQueueTimeoutMS: 30000, - }); - return { db, schema: Schema }; -} diff --git a/chili-and-cilantro-api/src/setup-middlewares.ts b/chili-and-cilantro-api/src/setup-middlewares.ts deleted file mode 100644 index 0f165a8..0000000 --- a/chili-and-cilantro-api/src/setup-middlewares.ts +++ /dev/null @@ -1,54 +0,0 @@ -import cors from 'cors'; -import { Application, json as expressJson, urlencoded } from 'express'; -//import { auth } from 'express-oauth2-jwt-bearer'; -import { json } from 'body-parser'; -import compression from 'compression'; -import helmet from 'helmet'; -import logger from 'morgan'; -import nocache from 'nocache'; -import { cors as corslib } from './cors'; -import { environment } from './environment'; - -export function setupMiddlewares(app: Application) { - app.use(corslib); - app.use(compression()); - app.use(json()); - // app.use(auth({ - // issuerBaseURL: `https://${environment.auth0.domain}/`, - // audience: environment.auth0.audience, - // tokenSigningAlg: 'RS256', - // })); - app.use(expressJson()); - app.use(logger('dev')); - app.use(urlencoded({ extended: true })); - app.use( - helmet({ - hsts: { - maxAge: 31536000, - }, - contentSecurityPolicy: { - useDefaults: false, - directives: { - defaultSrc: ["'self'"], - imgSrc: ["'self'", 'https://cdn.auth0.com', 'https://s.gravatar.com'], - connectSrc: ["'self'", `https://${environment.auth0.domain}/`], - scriptSrc: ["'self'", "'unsafe-inline'"], - styleSrc: ["'self'", "'unsafe-inline'"], - frameSrc: ["'self'", `https://${environment.auth0.domain}/`], - }, - }, - frameguard: { - action: 'deny', - }, - }), - ); - app.use(nocache()); - app.use( - cors({ - origin: environment.siteUrl, - methods: ['GET'], - allowedHeaders: ['Authorization', 'Content-Type'], - maxAge: 86400, - }), - ); -} diff --git a/chili-and-cilantro-api/src/setup-routes.ts b/chili-and-cilantro-api/src/setup-routes.ts deleted file mode 100644 index 454f8fa..0000000 --- a/chili-and-cilantro-api/src/setup-routes.ts +++ /dev/null @@ -1,13 +0,0 @@ -import express from 'express'; -import { environment } from './environment'; -import { apiRouter } from './routes/api'; - -export function setupRoutes(app: express.Application) { - app.use('/', express.static(environment.developer.reactDir)); - //app.use('/auth', authRouter); - app.use('/api', apiRouter); // TODO: ensureAuthenticated - // fallback to index.html for anything unknown - app.get('*', (req, res) => { - res.sendFile('index.html', { root: environment.developer.reactDir }); - }); -} diff --git a/chili-and-cilantro-api/src/setup-static-react-app.ts b/chili-and-cilantro-api/src/setup-static-react-app.ts deleted file mode 100644 index 13742ee..0000000 --- a/chili-and-cilantro-api/src/setup-static-react-app.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Application, static as expressStatic } from 'express'; -import path from 'path'; -import { environment } from './environment'; - -const serveStaticOptions = { - index: ['index.html'], -}; - -export function setupStaticReactApp(app: Application) { - app.use(expressStatic(environment.developer.reactDir, serveStaticOptions)); - app.use( - '/assets', - expressStatic(path.join(environment.developer.reactDir, 'src', 'assets')), - ); -} diff --git a/chili-and-cilantro-api/src/types/express.d.ts b/chili-and-cilantro-api/src/types/express.d.ts index 2c0e512..827e4d5 100644 --- a/chili-and-cilantro-api/src/types/express.d.ts +++ b/chili-and-cilantro-api/src/types/express.d.ts @@ -1,9 +1,13 @@ -import { IRequestUser } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { + IRequestUser, + ValidatedBody, +} from '@chili-and-cilantro/chili-and-cilantro-lib'; declare global { namespace Express { interface Request { user?: IRequestUser; + validatedBody?: ValidatedBody; } } } diff --git a/chili-and-cilantro-api/test/fixtures/action.ts b/chili-and-cilantro-api/test/fixtures/action.ts index 7176e34..773ff2a 100644 --- a/chili-and-cilantro-api/test/fixtures/action.ts +++ b/chili-and-cilantro-api/test/fixtures/action.ts @@ -1,33 +1,49 @@ import { ActionType, CardType, + ChiliCilantroActions, constants, + DefaultIdType, ICreateGameActionObject, ICreateGameDetails, + IEndGameActionObject, + IEndGameDetails, + IEndRoundActionObject, + IEndRoundDetails, IExpireGameActionObject, IExpireGameDetails, + IFlipCardActionObject, + IFlipCardDetails, IJoinGameActionObject, IJoinGameDetails, + IMakeBidActionObject, + IMakeBidDetails, IMessageActionObject, IMessageDetails, IPassActionObject, IPassDetails, IPlaceCardActionObject, + IPlaceCardDetails, + IQuitGameActionObject, + IQuitGameDetails, IStartBiddingActionObject, IStartBiddingDetails, IStartGameActionObject, IStartGameDetails, + IStartNewRoundActionObject, + IStartNewRoundDetails, } from '@chili-and-cilantro/chili-and-cilantro-lib'; import { faker } from '@faker-js/faker'; -import { Types } from 'mongoose'; +import { MockedModel } from './mocked-model'; +import { generateObjectId } from './objectId'; export function generateCreateGameAction( - gameId: Types.ObjectId, - chefId: Types.ObjectId, - userId: Types.ObjectId, + gameId: DefaultIdType, + chefId: DefaultIdType, + userId: DefaultIdType, ): ICreateGameActionObject { return { - _id: new Types.ObjectId(), + _id: generateObjectId(), gameId: gameId, chefId: chefId, userId: userId, @@ -40,12 +56,12 @@ export function generateCreateGameAction( } export function generateJoinGameAction( - gameId: Types.ObjectId, - chefId: Types.ObjectId, - userId: Types.ObjectId, + gameId: DefaultIdType, + chefId: DefaultIdType, + userId: DefaultIdType, ): IJoinGameActionObject { return { - _id: new Types.ObjectId(), + _id: generateObjectId(), gameId: gameId, chefId: chefId, userId: userId, @@ -58,12 +74,12 @@ export function generateJoinGameAction( } export function generateStartGameAction( - gameId: Types.ObjectId, - chefId: Types.ObjectId, - userId: Types.ObjectId, + gameId: DefaultIdType, + chefId: DefaultIdType, + userId: DefaultIdType, ): IStartGameActionObject { return { - _id: new Types.ObjectId(), + _id: generateObjectId(), gameId: gameId, chefId: chefId, userId: userId, @@ -75,13 +91,49 @@ export function generateStartGameAction( }; } +export function generateEndGameAction( + gameId: DefaultIdType, + chefId: DefaultIdType, + userId: DefaultIdType, +): IEndGameActionObject { + return { + _id: generateObjectId(), + gameId: gameId, + chefId: chefId, + userId: userId, + type: ActionType.END_GAME, + details: {} as IEndGameDetails, + round: constants.NONE, + createdAt: faker.date.past(), + updatedAt: faker.date.past(), + }; +} + +export function generateEndRoundAction( + gameId: DefaultIdType, + chefId: DefaultIdType, + userId: DefaultIdType, +): IEndRoundActionObject { + return { + _id: generateObjectId(), + gameId: gameId, + chefId: chefId, + userId: userId, + type: ActionType.END_ROUND, + details: {} as IEndRoundDetails, + round: constants.NONE, + createdAt: faker.date.past(), + updatedAt: faker.date.past(), + }; +} + export function generateExpireGameAction( - gameId: Types.ObjectId, - chefId: Types.ObjectId, - userId: Types.ObjectId, + gameId: DefaultIdType, + chefId: DefaultIdType, + userId: DefaultIdType, ): IExpireGameActionObject { return { - _id: new Types.ObjectId(), + _id: generateObjectId(), gameId: gameId, chefId: chefId, userId: userId, @@ -94,13 +146,13 @@ export function generateExpireGameAction( } export function generateSendMessageAction( - gameId: Types.ObjectId, - chefId: Types.ObjectId, - userId: Types.ObjectId, + gameId: DefaultIdType, + chefId: DefaultIdType, + userId: DefaultIdType, message: string, ): IMessageActionObject { return { - _id: new Types.ObjectId(), + _id: generateObjectId(), gameId: gameId, chefId: chefId, userId: userId, @@ -114,15 +166,37 @@ export function generateSendMessageAction( }; } +export function generateMakeBidAction( + gameId: DefaultIdType, + chefId: DefaultIdType, + userId: DefaultIdType, + round: number, + bid: number, +): IMakeBidActionObject { + return { + _id: generateObjectId(), + gameId: gameId, + chefId: chefId, + userId: userId, + type: ActionType.MAKE_BID, + details: { + bidNumber: bid, + } as IMakeBidDetails, + round: round, + createdAt: faker.date.past(), + updatedAt: faker.date.past(), + }; +} + export function generateStartBiddingAction( - gameId: Types.ObjectId, - chefId: Types.ObjectId, - userId: Types.ObjectId, + gameId: DefaultIdType, + chefId: DefaultIdType, + userId: DefaultIdType, round: number, bid: number, ): IStartBiddingActionObject { return { - _id: new Types.ObjectId(), + _id: generateObjectId(), gameId: gameId, chefId: chefId, userId: userId, @@ -137,13 +211,13 @@ export function generateStartBiddingAction( } export function generatePassAction( - gameId: Types.ObjectId, - chefId: Types.ObjectId, - userId: Types.ObjectId, + gameId: DefaultIdType, + chefId: DefaultIdType, + userId: DefaultIdType, round: number, ): IPassActionObject { return { - _id: new Types.ObjectId(), + _id: generateObjectId(), gameId: gameId, chefId: chefId, userId: userId, @@ -155,16 +229,40 @@ export function generatePassAction( }; } +export function generateFlipCardAction( + gameId: DefaultIdType, + chefId: DefaultIdType, + userId: DefaultIdType, + round: number, + cardIndex: number, +): IFlipCardActionObject { + return { + _id: generateObjectId(), + gameId: gameId, + chefId: chefId, + userId: userId, + type: ActionType.FLIP_CARD, + details: { + chef: chefId, + card: generateObjectId(), + cardIndex: cardIndex, + } as IFlipCardDetails, + round: round, + createdAt: faker.date.past(), + updatedAt: faker.date.past(), + }; +} + export function generatePlaceCardAction( - gameId: Types.ObjectId, - chefId: Types.ObjectId, - userId: Types.ObjectId, + gameId: DefaultIdType, + chefId: DefaultIdType, + userId: DefaultIdType, round: number, cardType: CardType, position: number, ): IPlaceCardActionObject { return { - _id: new Types.ObjectId(), + _id: generateObjectId(), gameId: gameId, chefId: chefId, userId: userId, @@ -172,9 +270,157 @@ export function generatePlaceCardAction( details: { cardType: cardType, position: position, - }, + } as IPlaceCardDetails, round: round, createdAt: faker.date.past(), updatedAt: faker.date.past(), }; } + +export function generateQuitGameAction( + gameId: DefaultIdType, + chefId: DefaultIdType, + userId: DefaultIdType, +): IQuitGameActionObject { + return { + _id: generateObjectId(), + gameId: gameId, + chefId: chefId, + userId: userId, + type: ActionType.QUIT_GAME, + details: {} as IQuitGameDetails, + round: constants.NONE, + createdAt: faker.date.past(), + updatedAt: faker.date.past(), + }; +} + +export function generateStartRoundAction( + gameId: DefaultIdType, + chefId: DefaultIdType, + userId: DefaultIdType, + round: number, +): IStartNewRoundActionObject { + return { + _id: generateObjectId(), + gameId: gameId, + chefId: chefId, + userId: userId, + type: ActionType.START_NEW_ROUND, + details: { + round: round, + } as IStartNewRoundDetails, + round: constants.NONE, + createdAt: faker.date.past(), + updatedAt: faker.date.past(), + }; +} + +export function generateAction( + actionType: ActionType, + gameId: DefaultIdType, + chefId: DefaultIdType, + userId: DefaultIdType, +): ChiliCilantroActions & MockedModel { + let actionData: Partial; + switch (actionType) { + case ActionType.CREATE_GAME: + actionData = generateCreateGameAction(gameId, chefId, userId); + break; + case ActionType.END_GAME: + actionData = generateEndGameAction(gameId, chefId, userId); + break; + case ActionType.END_ROUND: + actionData = generateEndRoundAction(gameId, chefId, userId); + break; + case ActionType.EXPIRE_GAME: + actionData = generateExpireGameAction(gameId, chefId, userId); + break; + case ActionType.FLIP_CARD: + actionData = generateFlipCardAction( + gameId, + chefId, + userId, + faker.number.int({ min: 1, max: 6 }), + faker.number.int({ min: 0, max: 5 }), + ); + break; + case ActionType.JOIN_GAME: + actionData = generateJoinGameAction(gameId, chefId, userId); + break; + case ActionType.MAKE_BID: + actionData = generateMakeBidAction( + gameId, + chefId, + userId, + faker.number.int({ min: 1, max: 6 }), + faker.number.int({ min: 1, max: 6 }), + ); + break; + case ActionType.MESSAGE: + actionData = generateSendMessageAction( + gameId, + chefId, + userId, + faker.lorem.sentence(), + ); + break; + case ActionType.PASS: + actionData = generatePassAction( + gameId, + chefId, + userId, + faker.number.int({ min: 1, max: 6 }), + ); + break; + case ActionType.PLACE_CARD: + actionData = generatePlaceCardAction( + gameId, + chefId, + userId, + faker.number.int({ min: 1, max: 6 }), + faker.helpers.enumValue(CardType), + faker.number.int({ min: 1, max: 6 }), + ); + break; + case ActionType.QUIT_GAME: + actionData = generateQuitGameAction(gameId, chefId, userId); + break; + case ActionType.START_BIDDING: + actionData = generateStartBiddingAction( + gameId, + chefId, + userId, + faker.number.int({ min: 1, max: 6 }), + faker.number.int({ min: 1, max: 6 }), + ); + break; + case ActionType.START_GAME: + actionData = generateStartGameAction(gameId, chefId, userId); + break; + case ActionType.START_NEW_ROUND: + actionData = generateStartRoundAction( + gameId, + chefId, + userId, + faker.number.int({ min: 1, max: 6 }), + ); + break; + default: + throw new Error(`Unexpected action type: ${actionType}`); + } + + const action = { + find: jest.fn().mockReturnThis(), + findOne: jest.fn().mockReturnThis(), + findById: jest.fn().mockReturnThis(), + create: jest.fn().mockImplementation((doc) => Promise.resolve(doc)), + updateOne: jest.fn().mockResolvedValue({ nModified: 1 }), + deleteOne: jest.fn().mockResolvedValue({ deletedCount: 1 }), + populate: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue(actionData), + save: jest.fn().mockImplementation(() => Promise.resolve(action)), + ...actionData, + } as ChiliCilantroActions & MockedModel; + return action; +} diff --git a/chili-and-cilantro-api/test/fixtures/chef.ts b/chili-and-cilantro-api/test/fixtures/chef.ts index b80e533..e06de2d 100644 --- a/chili-and-cilantro-api/test/fixtures/chef.ts +++ b/chili-and-cilantro-api/test/fixtures/chef.ts @@ -1,6 +1,10 @@ -import { ChefState, IChef } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { + ChefState, + IChefDocument, +} from '@chili-and-cilantro/chili-and-cilantro-lib'; import { faker } from '@faker-js/faker'; import { UtilityService } from '../../src/services/utility'; +import { MockedModel } from './mocked-model'; import { generateObjectId } from './objectId'; /** @@ -8,8 +12,8 @@ import { generateObjectId } from './objectId'; * @param overrides Any values to override the generated values * @returns */ -export function generateChef(overrides?: Object): IChef & { save: jest.Mock } { - const chef = { +export function generateChef(overrides?: object): IChefDocument & MockedModel { + const chefData = { _id: generateObjectId(), gameId: generateObjectId(), name: faker.person.firstName(), @@ -19,9 +23,20 @@ export function generateChef(overrides?: Object): IChef & { save: jest.Mock } { userId: generateObjectId(), state: ChefState.LOBBY, host: false, - save: jest.fn(), ...overrides, - }; - chef.save.mockImplementation(() => Promise.resolve(chef)); + } as Partial; + + const chef = { + find: jest.fn().mockReturnThis(), + findOne: jest.fn().mockReturnThis(), + findById: jest.fn().mockReturnThis(), + create: jest.fn().mockImplementation((doc) => Promise.resolve(doc)), + updateOne: jest.fn().mockResolvedValue({ nModified: 1 }), + deleteOne: jest.fn().mockResolvedValue({ deletedCount: 1 }), + populate: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue(chefData), + save: jest.fn().mockImplementation(() => Promise.resolve(chef)), + ...chefData, + } as IChefDocument & MockedModel; return chef; } diff --git a/chili-and-cilantro-api/test/fixtures/database.ts b/chili-and-cilantro-api/test/fixtures/database.ts new file mode 100644 index 0000000..0d17b9f --- /dev/null +++ b/chili-and-cilantro-api/test/fixtures/database.ts @@ -0,0 +1,49 @@ +import { + AccountStatusTypeEnum, + GetModelFunction, + IBaseDocument, + ModelName, +} from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { faker } from '@faker-js/faker'; +import moment from 'moment-timezone'; + +function getRandomTimezone(): string { + const timezones = moment.tz.names(); + const randomIndex = Math.floor(Math.random() * timezones.length); + return timezones[randomIndex]; +} + +export class Database { + public getModel: GetModelFunction = >( + modelName: ModelName, + ) => { + let mockData: Partial = {}; + switch (modelName) { + case ModelName.User: + mockData = { + username: faker.internet.userName(), + password: faker.internet.password(), + email: faker.internet.email(), + emailVerified: true, + accountStatusType: AccountStatusTypeEnum.Active, + timezone: getRandomTimezone(), + lastLogin: faker.date.recent(), + } as unknown as Partial; + break; + default: + throw new Error(`Unexpected model name: ${modelName}`); + } + + return { + find: jest.fn().mockReturnThis(), + findOne: jest.fn().mockReturnThis(), + findById: jest.fn().mockReturnThis(), + create: jest.fn().mockImplementation((doc) => Promise.resolve(doc)), + updateOne: jest.fn().mockResolvedValue({ nModified: 1 }), + deleteOne: jest.fn().mockResolvedValue({ deletedCount: 1 }), + populate: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue(mockData), + ...mockData, + } as unknown as T; + }; +} diff --git a/chili-and-cilantro-api/test/fixtures/game.ts b/chili-and-cilantro-api/test/fixtures/game.ts index 6daabab..01cee47 100644 --- a/chili-and-cilantro-api/test/fixtures/game.ts +++ b/chili-and-cilantro-api/test/fixtures/game.ts @@ -11,9 +11,11 @@ import { import { faker } from '@faker-js/faker'; import { Types } from 'mongoose'; import { UtilityService } from '../../src/services/utility'; -import { numberBetween } from '../fixtures/utils'; import { generateChef } from './chef'; +import { MockedModel } from './mocked-model'; +import { generateObjectId } from './objectId'; import { generateUser } from './user'; +import { numberBetween } from './utils'; export function generateGamePassword(): string { let generatedPassword = ''; @@ -36,12 +38,12 @@ export function generateGamePassword(): string { */ export function generateGame( withPassword = true, - overrides?: Object, -): IGame & { save: jest.Mock } { - const hostChefId = new Types.ObjectId(); - const hostUserId = new Types.ObjectId(); - const game = { - _id: new Types.ObjectId(), + overrides?: object, +): IGameDocument & MockedModel { + const hostChefId = generateObjectId(); + const hostUserId = generateObjectId(); + const gameData = { + _id: generateObjectId(), code: UtilityService.generateGameCode(), name: faker.lorem.words(3), ...(withPassword ? { password: generateGamePassword() } : {}), @@ -60,10 +62,21 @@ export function generateGame( hostUserId: hostUserId, createdAt: faker.date.past(), updatedAt: faker.date.past(), - save: jest.fn(), ...(overrides ? overrides : {}), - }; - game.save.mockImplementation(() => Promise.resolve(game)); + } as Partial; + + const game = { + find: jest.fn().mockReturnThis(), + findOne: jest.fn().mockReturnThis(), + findById: jest.fn().mockReturnThis(), + create: jest.fn().mockImplementation((doc) => Promise.resolve(doc)), + updateOne: jest.fn().mockResolvedValue({ nModified: 1 }), + deleteOne: jest.fn().mockResolvedValue({ deletedCount: 1 }), + populate: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue(gameData), + save: jest.fn().mockImplementation(() => Promise.resolve(game)), + ...gameData, + } as IGameDocument & MockedModel; return game; } diff --git a/chili-and-cilantro-api/test/fixtures/jwksClient.ts b/chili-and-cilantro-api/test/fixtures/jwksClient.ts deleted file mode 100644 index 2e5010c..0000000 --- a/chili-and-cilantro-api/test/fixtures/jwksClient.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { SigningKey } from 'jwks-rsa'; - -/** - * Mocks the JwksClient and its jwksClient.getSigningKey function - */ -export class MockJwksClient { - getSigningKey( - kid: string, - callback: (err: Error | null, key?: SigningKey) => void, - ) { - callback(null, { - kid: kid, - alg: 'RS256', - getPublicKey() { - return 'public key'; - }, - publicKey: 'public key', - }); - } -} diff --git a/chili-and-cilantro-api/test/fixtures/mocked-model.ts b/chili-and-cilantro-api/test/fixtures/mocked-model.ts new file mode 100644 index 0000000..90a5814 --- /dev/null +++ b/chili-and-cilantro-api/test/fixtures/mocked-model.ts @@ -0,0 +1,11 @@ +export interface MockedModel { + find: jest.Mock; + findOne: jest.Mock; + findById: jest.Mock; + create: jest.Mock; + updateOne: jest.Mock; + deleteOne: jest.Mock; + populate: jest.Mock; + exec: jest.Mock; + save: jest.Mock; +} diff --git a/chili-and-cilantro-api/test/fixtures/objectId.ts b/chili-and-cilantro-api/test/fixtures/objectId.ts new file mode 100644 index 0000000..7cdd01c --- /dev/null +++ b/chili-and-cilantro-api/test/fixtures/objectId.ts @@ -0,0 +1,6 @@ +import { faker } from '@faker-js/faker'; +import { Types } from 'mongoose'; + +export function generateObjectId() { + return new Types.ObjectId(faker.string.uuid()); +} diff --git a/chili-and-cilantro-api/test/fixtures/user.ts b/chili-and-cilantro-api/test/fixtures/user.ts index 95619e2..1233c47 100644 --- a/chili-and-cilantro-api/test/fixtures/user.ts +++ b/chili-and-cilantro-api/test/fixtures/user.ts @@ -4,7 +4,8 @@ import { IUserDocument, } from '@chili-and-cilantro/chili-and-cilantro-lib'; import { faker } from '@faker-js/faker'; -import { Types } from 'mongoose'; +import { MockedModel } from './mocked-model'; +import { generateObjectId } from './objectId'; export function generateUserPassword(): string { let generatedPassword = ''; @@ -46,15 +47,12 @@ export function generateUserDisplayName(): string { */ export function generateUser( overrides?: Partial, -): IUserDocument & { save: jest.Mock } { - const id = new Types.ObjectId(); - const user = { +): IUserDocument & MockedModel { + const id = generateObjectId(); + const userData = { _id: id, username: generateUsername(), - password: faker.internet.password(), - givenName: faker.person.firstName(), - surname: faker.person.lastName(), - userPrincipalName: faker.internet.email(), + password: generateUserPassword(), email: faker.internet.email(), emailVerified: faker.datatype.boolean(), lastLogin: faker.date.past(), @@ -62,10 +60,20 @@ export function generateUser( updatedAt: faker.date.past(), createdBy: id, updatedBy: id, - save: jest.fn(), ...overrides, - } as IUserDocument & { save: jest.Mock }; + }; - user.save.mockImplementation(() => Promise.resolve(user)); + const user = { + find: jest.fn().mockReturnThis(), + findOne: jest.fn().mockReturnThis(), + findById: jest.fn().mockReturnThis(), + create: jest.fn().mockImplementation((doc) => Promise.resolve(doc)), + updateOne: jest.fn().mockResolvedValue({ nModified: 1 }), + deleteOne: jest.fn().mockResolvedValue({ deletedCount: 1 }), + populate: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue(userData), + save: jest.fn().mockImplementation(() => Promise.resolve(user)), + ...userData, + } as IUserDocument & MockedModel; return user; } diff --git a/chili-and-cilantro-api/test/fixtures/utils.ts b/chili-and-cilantro-api/test/fixtures/utils.ts index 16b9c4d..68d6a26 100644 --- a/chili-and-cilantro-api/test/fixtures/utils.ts +++ b/chili-and-cilantro-api/test/fixtures/utils.ts @@ -1,3 +1,5 @@ +import { randomBytes, randomInt } from 'crypto'; + /** * Generates a random string of length between minLength and maxLength * @param minLength @@ -5,15 +7,13 @@ * @returns */ export function generateString(minLength: number, maxLength: number): string { - // Generate a random string of length between minLength and maxLength - const length = - Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength; - // include random characters including space const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 '; + const length = randomInt(minLength, maxLength + 1); + const bytes = randomBytes(length); let result = ''; for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * characters.length)); + result += characters[bytes[i] % characters.length]; } return result; } @@ -25,5 +25,12 @@ export function generateString(minLength: number, maxLength: number): string { * @returns */ export function numberBetween(min: number, max: number): number { - return Math.floor(Math.random() * (max - min + 1)) + min; + const range = max - min + 1; + const bytesNeeded = Math.ceil(Math.log2(range) / 8); + let randomNumber; + do { + const bytes = randomBytes(bytesNeeded); + randomNumber = bytes.readUIntBE(0, bytesNeeded); + } while (randomNumber >= range * Math.floor(256 ** bytesNeeded / range)); + return min + (randomNumber % range); } diff --git a/chili-and-cilantro-api/test/unit/actionService.test.ts b/chili-and-cilantro-api/test/unit/actionService.test.ts index 27e5955..fabc88d 100644 --- a/chili-and-cilantro-api/test/unit/actionService.test.ts +++ b/chili-and-cilantro-api/test/unit/actionService.test.ts @@ -2,6 +2,7 @@ import { ActionType, CardType, constants, + DefaultIdType, IAction, IChef, ICreateGameAction, @@ -16,7 +17,7 @@ import { IUser, } from '@chili-and-cilantro/chili-and-cilantro-lib'; import { faker } from '@faker-js/faker'; -import { Model, Types } from 'mongoose'; +import { Model } from 'mongoose'; import { IDatabase } from '../../src/interfaces/database'; import { ActionService } from '../../src/services/action'; import { @@ -37,7 +38,7 @@ type MockModel = Model & }; describe('ActionService', () => { - let gameId: Types.ObjectId; + let gameId: DefaultIdType; let mockGame: IGame; let hostChef: IChef; let hostUser: IUser; diff --git a/chili-and-cilantro-api/test/unit/chefService.test.ts b/chili-and-cilantro-api/test/unit/chefService.test.ts index 164b092..c4c51bd 100644 --- a/chili-and-cilantro-api/test/unit/chefService.test.ts +++ b/chili-and-cilantro-api/test/unit/chefService.test.ts @@ -1,11 +1,12 @@ -import { NotInGameError } from 'chili-and-cilantro-api/src/errors/notInGame'; -import constants from 'chili-and-cilantro-lib/src/lib/constants'; +import { + ChefState, + constants, + NotInGameError, +} from '@chili-and-cilantro/chili-and-cilantro-lib'; import mongoose, { Types } from 'mongoose'; -import { ChefState } from '../../../chili-and-cilantro-lib/src'; import { ChefService } from '../../src/services/chef'; import { UtilityService } from '../../src/services/utility'; import { generateChefGameUser } from '../fixtures/game'; -import { generateObjectId } from '../fixtures/objectId'; import { generateUsername } from '../fixtures/user'; // Mocks @@ -29,7 +30,7 @@ describe('ChefService', () => { beforeEach(() => { mockChefModel = new mongoose.Model(); - chefService = new ChefService(mockChefModel); + chefService = new ChefService(); }); describe('newChefAsync', () => { @@ -233,8 +234,8 @@ describe('ChefService', () => { it('should throw NotInGameError if no chef is found', async () => { // Arrange - const gameId = generateObjectId(); - const userId = generateObjectId(); + const gameId = new Types.ObjectId(); + const userId = new Types.ObjectId(); const mockGame = { _id: gameId }; const mockUser = { _id: userId }; diff --git a/chili-and-cilantro-api/test/unit/gameService.createGame.test.ts b/chili-and-cilantro-api/test/unit/gameService.createGame.test.ts index af82b66..56ff687 100644 --- a/chili-and-cilantro-api/test/unit/gameService.createGame.test.ts +++ b/chili-and-cilantro-api/test/unit/gameService.createGame.test.ts @@ -1,21 +1,21 @@ import { + AlreadyJoinedOtherError, constants, IChef, IGame, + InvalidGameNameError, + InvalidGameParameterError, + InvalidGamePasswordError, + InvalidUserDisplayNameError, ModelName, } from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { InvalidGameNameError } from 'chili-and-cilantro-api/src/errors/invalidGameName'; -import { InvalidGameParameterError } from 'chili-and-cilantro-api/src/errors/invalidGameParameter'; -import { InvalidGamePasswordError } from 'chili-and-cilantro-api/src/errors/invalidGamePassword'; -import { UtilityService } from 'chili-and-cilantro-api/src/services/utility'; import sinon from 'sinon'; -import { AlreadyJoinedOtherError } from '../../src/errors/already-joined-other'; -import { InvalidUserDisplayNameError } from '../../src/errors/invalid-user-display-name'; import { ActionService } from '../../src/services/action'; import { ChefService } from '../../src/services/chef'; import { Database } from '../../src/services/database'; import { GameService } from '../../src/services/game'; import { PlayerService } from '../../src/services/player'; +import { UtilityService } from '../../src/services/utility'; import { generateCreateGameAction } from '../fixtures/action'; import { generateChefGameUser } from '../fixtures/game'; import { mockedWithTransactionAsync } from '../fixtures/transactionManager'; diff --git a/chili-and-cilantro-api/test/unit/gameService.joinGameAsync.test.ts b/chili-and-cilantro-api/test/unit/gameService.joinGameAsync.test.ts index 709a001..2b163bd 100644 --- a/chili-and-cilantro-api/test/unit/gameService.joinGameAsync.test.ts +++ b/chili-and-cilantro-api/test/unit/gameService.joinGameAsync.test.ts @@ -1,24 +1,29 @@ import { + AlreadyJoinedOtherError, constants, + DefaultIdType, + GameFullError, + GameInProgressError, + GamePasswordMismatchError, GamePhase, IChef, + IChefDocument, IGame, + IGameDocument, + InvalidUserDisplayNameError, + IUserDocument, ModelName, + UsernameInUseError, } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { faker } from '@faker-js/faker'; import sinon from 'sinon'; -import { AlreadyJoinedOtherError } from '../../src/errors/already-joined-other'; -import { GameFullError } from '../../src/errors/game-full'; -import { GameInProgressError } from '../../src/errors/game-in-progress'; -import { GamePasswordMismatchError } from '../../src/errors/game-password-mismatch'; -import { InvalidUserDisplayNameError } from '../../src/errors/invalid-user-display-name'; -import { UsernameInUseError } from '../../src/errors/username-in-use'; import { ActionService } from '../../src/services/action'; import { ChefService } from '../../src/services/chef'; -import { Database } from '../../src/services/database'; import { GameService } from '../../src/services/game'; import { PlayerService } from '../../src/services/player'; import { UtilityService } from '../../src/services/utility'; import { generateChef } from '../fixtures/chef'; +import { Database } from '../fixtures/database'; import { generateChefGameUser, generateGame } from '../fixtures/game'; import { generateObjectId } from '../fixtures/objectId'; import { mockedWithTransactionAsync } from '../fixtures/transactionManager'; @@ -46,7 +51,11 @@ describe('GameService', () => { }); describe('validateJoinGameOrThrowAsync', () => { - let gameId, game, chef, user, userDisplayName; + let gameId: DefaultIdType; + let game: IGameDocument; + let chef: IChefDocument; + let user: IUserDocument; + let userDisplayName: string; beforeEach(() => { // Setup initial valid parameters @@ -55,10 +64,7 @@ describe('GameService', () => { user = generated.user; chef = generated.chef; game = generated.game; - userDisplayName = generateString( - constants.MIN_USER_DISPLAY_NAME_LENGTH, - constants.MAX_USER_DISPLAY_NAME_LENGTH, - ); + userDisplayName = faker.person.firstName(); }); afterEach(() => { diff --git a/chili-and-cilantro-api/test/unit/jwtService.test.ts b/chili-and-cilantro-api/test/unit/jwtService.test.ts deleted file mode 100644 index da15754..0000000 --- a/chili-and-cilantro-api/test/unit/jwtService.test.ts +++ /dev/null @@ -1,573 +0,0 @@ -import { IUser } from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { GetUsers200ResponseOneOfInner } from 'auth0'; -import jwt, { JwtPayload } from 'jsonwebtoken'; -import { JwksClient, SigningKey } from 'jwks-rsa'; -import { Document } from 'mongoose'; -import { managementClient } from '../../src/auth0'; -import { JwtService } from '../../src/services/jwt'; -import { UserService } from '../../src/services/user'; -import { generateUser } from '../fixtures/user'; - -jest.mock('jwks-rsa', () => ({ - JwksClient: jest.fn().mockImplementation(() => ({ - getSigningKey: jest.fn().mockImplementation((kid, callback) => { - callback(null, { - getPublicKey: () => 'mock-signing-key', - } as unknown as SigningKey); - }), - })), -})); -jest.mock('../../src/auth0', () => ({ - managementClient: { - users: { - get: jest.fn(), - }, - }, -})); -jest.mock('../../src/services/user', () => { - return { - UserService: jest.fn().mockImplementation(() => { - return { - getUserByAuth0IdOrThrow: jest.fn(), - }; - }), - }; -}); -jest.mock('jsonwebtoken', () => ({ - ...jest.requireActual('jsonwebtoken'), - verify: jest.fn(), -})); - -describe('JwtService', () => { - let jwtService: JwtService; - let userService: jest.Mocked; - let mockRequest: { headers: { authorization?: string } }; - let mockResponse: { status: jest.Mock; json: jest.Mock }; - let nextFunction: jest.Mock; - - beforeEach(() => { - userService = new UserService() as jest.Mocked; - jwtService = new JwtService(userService); - mockRequest = { headers: { authorization: 'Bearer token' } }; - mockResponse = { - status: jest.fn().mockReturnThis(), - json: jest.fn(), - }; - nextFunction = jest.fn(); - (JwksClient as jest.Mock).mockClear(); - (managementClient.users.get as jest.Mock).mockClear(); - (jwt.verify as jest.Mock).mockClear(); - }); - - describe('validateAccessTokenAndFetchAuth0UserAsync', () => { - it('should validate token and fetch user', async () => { - // Mock the JWT verification and user fetching process - const mockDecodedToken: JwtPayload = { sub: 'user-id' }; - const mockAuth0User: GetUsers200ResponseOneOfInner = { - user_id: 'user-id', - name: 'Test User', - } as any; - - // Mocking the JWT verify callback behavior - jest - .spyOn(jwt, 'verify') - .mockImplementation( - ( - token: string, - getKey: jwt.Secret | jwt.GetPublicKeyOrSecret, - options: jwt.VerifyOptions | undefined, - callback?: - | jwt.VerifyCallback - | undefined, - ) => { - if (callback) { - callback(null, mockDecodedToken); - } - }, - ); - - // Mocking the managementClient to return a successful response - (managementClient.users.get as jest.Mock).mockResolvedValue({ - status: 200, - data: mockAuth0User, - }); - - // Call the method with a mock token - const result = - await jwtService.validateAccessTokenAndFetchAuth0UserAsync( - 'mock-token', - ); - - // Assertions - expect(result).toEqual(mockAuth0User); - expect(managementClient.users.get).toHaveBeenCalledWith({ - id: mockDecodedToken.sub, - }); - }); - - it('should throw an error for invalid token', async () => { - // Mock the JWT verify to simulate an invalid token - jest - .spyOn(jwt, 'verify') - .mockImplementation( - ( - token: string, - getKey: jwt.Secret | jwt.GetPublicKeyOrSecret, - options: jwt.VerifyOptions | undefined, - callback?: - | jwt.VerifyCallback - | undefined, - ) => { - const error = new jwt.JsonWebTokenError('Invalid token'); - if (callback) { - callback(error, undefined); - } - }, - ), - // Attempt to call the method with an invalid token and expect an error - await expect( - jwtService.validateAccessTokenAndFetchAuth0UserAsync('invalid-token'), - ).rejects.toThrow('Invalid token'); - - // Ensure that the managementClient's get method is not called - expect(managementClient.users.get).not.toHaveBeenCalled(); - }); - - it('should throw an error for invalid decoded token', async () => { - // Mock verify to return a decoded token without 'sub' - (jwt.verify as jest.Mock).mockImplementation( - ( - token: string, - getKey: jwt.Secret | jwt.GetPublicKeyOrSecret, - options: jwt.VerifyOptions | undefined, - callback?: - | jwt.VerifyCallback - | undefined, - ) => { - callback(null, {}); // No 'sub' field - }, - ); - - // Expect an error when calling validateAccessTokenAndFetchAuth0UserAsync - await expect( - jwtService.validateAccessTokenAndFetchAuth0UserAsync('token'), - ).rejects.toThrow('Invalid token'); - }); - - it('should throw an error when Auth0 user is not found', async () => { - // Mock verify to return a valid decoded token - (jwt.verify as jest.Mock).mockImplementation( - ( - token: string, - getKey: jwt.Secret | jwt.GetPublicKeyOrSecret, - options: jwt.VerifyOptions | undefined, - callback?: - | jwt.VerifyCallback - | undefined, - ) => { - callback(null, { sub: 'user-id' }); - }, - ); - - // Mock managementClient to simulate user not found - (managementClient.users.get as jest.Mock).mockResolvedValue({ - status: 404, - }); - - // Expect an error when calling validateAccessTokenAndFetchAuth0UserAsync - await expect( - jwtService.validateAccessTokenAndFetchAuth0UserAsync('token'), - ).rejects.toThrow('User not found'); - }); - }); - - describe('authenticateUserAsync', () => { - it('should authenticate user and call next function', async () => { - // Mock the JWT verification and user fetching process - const mockDecodedToken: JwtPayload = { sub: 'user-id' }; - const mockAuth0User: GetUsers200ResponseOneOfInner = { - user_id: 'user-id', - name: 'Test User', - } as any; - - // Mocking the JWT verify callback behavior - jest - .spyOn(jwt, 'verify') - .mockImplementation( - ( - token: string, - getKey: jwt.Secret | jwt.GetPublicKeyOrSecret, - options: jwt.VerifyOptions | undefined, - callback?: - | jwt.VerifyCallback - | undefined, - ) => { - if (callback) { - callback(null, mockDecodedToken); - } - }, - ); - - // Mock the managementClient to return a successful response - (managementClient.users.get as jest.Mock).mockResolvedValue({ - data: mockAuth0User, - status: 200, - }); - - // Set up mock request with authorization header - mockRequest.headers.authorization = 'Bearer valid-token'; - - // mock userservice getUserByAuth0IdOrThrow to return a mock user - const mockUser = generateUser({ auth0Id: mockAuth0User.user_id }); - const mockUserDocument = { - ...mockUser, - isModified: jest.fn().mockReturnValue(false), - save: jest.fn(), - } as any as Document & IUser; - userService.getUserByAuth0IdOrThrow.mockResolvedValue(mockUserDocument); - - // Call the authenticateUserAsync method - await jwtService.authenticateUserAsync( - mockRequest as any, - mockResponse as any, - nextFunction, - ); - - // Assertions - expect(jwt.verify).toHaveBeenCalledWith( - 'valid-token', - expect.any(Function), - expect.any(Object), - expect.any(Function), - ); - expect(managementClient.users.get).toHaveBeenCalledWith({ - id: mockDecodedToken.sub, - }); - expect(nextFunction).toHaveBeenCalled(); - }); - - it('should return 401 for missing access token', async () => { - // Remove the authorization header to simulate a missing token - delete mockRequest.headers.authorization; - - // Call the authenticateUserAsync method - await jwtService.authenticateUserAsync( - mockRequest as any, - mockResponse as any, - nextFunction, - ); - - // Assertions - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.json).toHaveBeenCalledWith({ - message: 'Access token not found', - }); - expect(nextFunction).not.toHaveBeenCalled(); - }); - it('should return 401 if unable to determine user id', async () => { - // mock an auth0user without a user_id - const mockDecodedToken: JwtPayload = { sub: 'user-id' }; - const mockAuth0User: GetUsers200ResponseOneOfInner = { - user_id: undefined, - name: 'Test User', - } as any; - - // Mocking the JWT verify callback behavior - jest - .spyOn(jwt, 'verify') - .mockImplementation( - ( - token: string, - getKey: jwt.Secret | jwt.GetPublicKeyOrSecret, - options: jwt.VerifyOptions | undefined, - callback?: - | jwt.VerifyCallback - | undefined, - ) => { - if (callback) { - callback(null, mockDecodedToken); - } - }, - ); - - // Mock the managementClient to return a successful response - (managementClient.users.get as jest.Mock).mockResolvedValue({ - data: mockAuth0User, - status: 200, - }); - - // Set up mock request with authorization header - mockRequest.headers.authorization = 'Bearer valid-token'; - - // Call the authenticateUserAsync method - await jwtService.authenticateUserAsync( - mockRequest as any, - mockResponse as any, - nextFunction, - ); - - // Assertions - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.json).toHaveBeenCalledWith({ - message: 'Unable to determine user id', - }); - expect(nextFunction).not.toHaveBeenCalled(); - }); - it('should return 401 if user not found', async () => { - // Mock the JWT verification to return a valid decoded token - const mockDecodedToken: JwtPayload = { sub: 'user-id' }; - const mockAuth0User: GetUsers200ResponseOneOfInner = { - user_id: 'user-id', - name: 'Test User', - } as any; - - // Mocking the JWT verify callback behavior - jest - .spyOn(jwt, 'verify') - .mockImplementation( - ( - token: string, - getKey: jwt.Secret | jwt.GetPublicKeyOrSecret, - options: jwt.VerifyOptions | undefined, - callback?: - | jwt.VerifyCallback - | undefined, - ) => { - if (callback) { - callback(null, mockDecodedToken); - } - }, - ); - - // Mock the managementClient to return a successful response - (managementClient.users.get as jest.Mock).mockResolvedValue({ - data: mockAuth0User, - status: 200, - }); - // Mock userService to simulate user not found - userService.getUserByAuth0IdOrThrow.mockResolvedValue(null); - - // Call the authenticateUserAsync method - await jwtService.authenticateUserAsync( - mockRequest as any, - mockResponse as any, - nextFunction, - ); - - // Assertions - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.json).toHaveBeenCalledWith({ - message: 'User not found', - }); - expect(nextFunction).not.toHaveBeenCalled(); - }); - it('should return 401 for invalid access token', async () => { - // Mock the JWT verification to simulate an invalid token - (jwt.verify as jest.Mock).mockImplementation( - ( - token: string, - getKey: jwt.Secret | jwt.GetPublicKeyOrSecret, - options: jwt.VerifyOptions | undefined, - callback?: - | jwt.VerifyCallback - | undefined, - ) => { - callback(new jwt.JsonWebTokenError('Invalid access token'), null); - }, - ); - - // Call the authenticateUserAsync method - await jwtService.authenticateUserAsync( - mockRequest as any, - mockResponse as any, - nextFunction, - ); - - // Assertions - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.json).toHaveBeenCalledWith({ - message: 'Invalid access token', - }); - expect(nextFunction).not.toHaveBeenCalled(); - }); - }); - - describe('getUserFromValidatedTokenAsync', () => { - it('should fetch user from validated token', async () => { - // Mock the JWT verification process - const mockDecodedToken: JwtPayload = { sub: 'user-id' }; - jest - .spyOn(jwt, 'verify') - .mockImplementation( - ( - token: string, - getKey: jwt.Secret | jwt.GetPublicKeyOrSecret, - options: jwt.VerifyOptions | undefined, - callback?: - | jwt.VerifyCallback - | undefined, - ) => { - if (callback) { - callback(null, mockDecodedToken); - } - }, - ); - - // Mock the user service to return a user - const mockUser = generateUser({ auth0Id: mockDecodedToken.sub }); - const mockUserDocument = { - ...mockUser, - isModified: jest.fn().mockReturnValue(false), - save: jest.fn(), - } as any as Document & IUser; - userService.getUserByAuth0IdOrThrow.mockResolvedValue(mockUserDocument); - - // Call the getUserFromValidatedTokenAsync method with a mock token - const user = - await jwtService.getUserFromValidatedTokenAsync('valid-token'); - - // Assertions - expect(user).toEqual(mockUserDocument); - expect(userService.getUserByAuth0IdOrThrow).toHaveBeenCalledWith( - mockDecodedToken.sub, - ); - }); - - it('should throw an error for invalid token', async () => { - // Mock the JWT verification to simulate an invalid token - jest - .spyOn(jwt, 'verify') - .mockImplementation( - ( - token: string, - getKey: jwt.Secret | jwt.GetPublicKeyOrSecret, - options: jwt.VerifyOptions | undefined, - callback?: - | jwt.VerifyCallback - | undefined, - ) => { - const error = new jwt.JsonWebTokenError('Invalid token'); - if (callback) { - callback(error, undefined); - } - }, - ); - - // Attempt to call the method with an invalid token and expect an error - await expect( - jwtService.getUserFromValidatedTokenAsync('invalid-token'), - ).rejects.toThrow('Token Verification Failed: Invalid token'); - - // Ensure that the userService's getUserByAuth0IdOrThrow method is not called - expect(userService.getUserByAuth0IdOrThrow).not.toHaveBeenCalled(); - }); - it('should throw an error for invalid token in getUserFromValidatedTokenAsync', async () => { - // Mock the JWT verification to simulate an invalid token - (jwt.verify as jest.Mock).mockImplementation( - ( - token: string, - getKey: jwt.Secret | jwt.GetPublicKeyOrSecret, - options: jwt.VerifyOptions | undefined, - callback?: - | jwt.VerifyCallback - | undefined, - ) => { - callback(new jwt.JsonWebTokenError('Invalid token'), null); - }, - ); - - // Expect an error when calling getUserFromValidatedTokenAsync - await expect( - jwtService.getUserFromValidatedTokenAsync('invalid-token'), - ).rejects.toThrow('Token Verification Failed: Invalid token'); - }); - it('should throw an error if user not found in database in getUserFromValidatedTokenAsync', async () => { - // Mock the JWT verification to return a valid decoded token - (jwt.verify as jest.Mock).mockImplementation( - ( - token: string, - getKey: jwt.Secret | jwt.GetPublicKeyOrSecret, - options: jwt.VerifyOptions | undefined, - callback?: - | jwt.VerifyCallback - | undefined, - ) => { - callback(null, { sub: 'user-id' }); - }, - ); - - // Mock userService to simulate user not found - userService.getUserByAuth0IdOrThrow.mockResolvedValue(null); - - // Expect an error when calling getUserFromValidatedTokenAsync - await expect( - jwtService.getUserFromValidatedTokenAsync('valid-token'), - ).rejects.toThrow('User not found in database'); - }); - it('should throw an error if there is not a sub field in the decoded token', async () => { - // Mock the JWT verification to return a decoded token without 'sub' - (jwt.verify as jest.Mock).mockImplementation( - ( - token: string, - getKey: jwt.Secret | jwt.GetPublicKeyOrSecret, - options: jwt.VerifyOptions | undefined, - callback?: - | jwt.VerifyCallback - | undefined, - ) => { - callback(null, {}); // No 'sub' field - }, - ); - - // Expect an error when calling getUserFromValidatedTokenAsync - await expect( - jwtService.getUserFromValidatedTokenAsync('token'), - ).rejects.toThrow('Invalid token: unable to extract payload or user ID'); - }); - }); - describe('getKey', () => { - let jwtService: JwtService; - let mockJwksClient: jest.Mocked; - let mockUserService: jest.Mocked; - beforeEach(() => { - mockJwksClient = new JwksClient({ jwksUri: 'mockUri' }) as any; - mockUserService = {} as any; // Mock UserService as needed - jwtService = new JwtService(mockUserService); - (jwtService as any)['client'] = mockJwksClient; - }); - it('should retrieve the signing key successfully', (done) => { - const mockKey = { getPublicKey: () => 'mockPublicKey' }; - mockJwksClient.getSigningKey.mockImplementation((kid, callback) => { - callback(null, mockKey as any); - }); - - const header = { kid: 'testKid', alg: 'RS256' }; - jwtService.getKey(header, (err, key) => { - expect(err).toBeNull(); - expect(key).toBe('mockPublicKey'); - done(); - }); - }); - - it('should handle errors from getSigningKey', (done) => { - mockJwksClient.getSigningKey.mockImplementation((kid, callback) => { - callback(new Error('Error fetching signing key'), undefined); - }); - - const header = { kid: 'testKid', alg: 'RS256' }; - jwtService.getKey(header, (err, key) => { - expect(err).toBeInstanceOf(Error); - expect(err.message).toBe('Error fetching signing key'); - expect(key).toBeUndefined(); - done(); - }); - }); - - it('should throw an error if no KID is found in JWT', () => { - const header = {}; // Missing 'kid' - expect(() => { - jwtService.getKey(header as any, jest.fn()); - }).toThrow('No KID found in JWT'); - }); - }); -}); diff --git a/chili-and-cilantro-api/test/unit/userService.test.ts b/chili-and-cilantro-api/test/unit/userService.test.ts index 2c224a8..07e7525 100644 --- a/chili-and-cilantro-api/test/unit/userService.test.ts +++ b/chili-and-cilantro-api/test/unit/userService.test.ts @@ -1,5 +1,4 @@ import { - BaseModel, IUser, ModelName, constants, @@ -11,7 +10,6 @@ import { InvalidPasswordError } from 'chili-and-cilantro-api/src/errors/invalidP import { InvalidUsernameError } from 'chili-and-cilantro-api/src/errors/invalidUsername'; import { UsernameExistsError } from 'chili-and-cilantro-api/src/errors/usernameExists'; import sinon from 'sinon'; -import { managementClient } from '../../src/auth0'; import { UserService } from '../../src/services/user'; import { generateGamePassword } from '../fixtures/game'; import { @@ -24,7 +22,7 @@ describe('userService', () => { let userService, userModel; beforeAll(() => { userService = new UserService(); - userModel = BaseModel.getModel(ModelName.User); + userModel = userService.getModel(ModelName.User); }); afterEach(() => { sinon.restore(); diff --git a/chili-and-cilantro-api/tsconfig.spec.json b/chili-and-cilantro-api/tsconfig.spec.json index def7ab8..4cdba67 100644 --- a/chili-and-cilantro-api/tsconfig.spec.json +++ b/chili-and-cilantro-api/tsconfig.spec.json @@ -10,6 +10,7 @@ "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts", + "test/fixtures/**/*.ts", "test/**/*.test.ts" ] } diff --git a/chili-and-cilantro-lib/package.json b/chili-and-cilantro-lib/package.json index 479cada..25be3fd 100644 --- a/chili-and-cilantro-lib/package.json +++ b/chili-and-cilantro-lib/package.json @@ -2,7 +2,14 @@ "name": "@chili-and-cilantro/chili-and-cilantro-lib", "version": "0.0.1", "dependencies": { - "tslib": "^2.3.0" + "express-validator": "^7.1.0", + "mongoose": "^7.6.2", + "tslib": "^2.6.3" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/jsonwebtoken": "^9.0.6", + "eslint": "^9.15.0" }, "type": "commonjs", "main": "./src/index.js", diff --git a/chili-and-cilantro-lib/src/index.ts b/chili-and-cilantro-lib/src/index.ts index 2ebd786..65581ff 100644 --- a/chili-and-cilantro-lib/src/index.ts +++ b/chili-and-cilantro-lib/src/index.ts @@ -91,6 +91,7 @@ export * from './lib/interfaces/has-updater'; export * from './lib/interfaces/has-updates'; export * from './lib/interfaces/models/action'; export * from './lib/interfaces/models/actions/create-game'; +export * from './lib/interfaces/models/actions/details/base'; export * from './lib/interfaces/models/actions/details/create-game'; export * from './lib/interfaces/models/actions/details/end-game'; export * from './lib/interfaces/models/actions/details/end-round'; @@ -145,3 +146,4 @@ export * from './lib/interfaces/objects/user'; export * from './lib/interfaces/request-user'; export * from './lib/interfaces/round-bids'; export * from './lib/interfaces/token-user'; +export * from './lib/shared-types'; diff --git a/chili-and-cilantro-lib/src/lib/action-strings.ts b/chili-and-cilantro-lib/src/lib/action-strings.ts new file mode 100644 index 0000000..aea0ca5 --- /dev/null +++ b/chili-and-cilantro-lib/src/lib/action-strings.ts @@ -0,0 +1,18 @@ +import ActionType from './enumerations/action-type'; + +export const ActionStringsMap: { [key in ActionType]: string } = { + [ActionType.CREATE_GAME]: 'Created a new game', + [ActionType.START_GAME]: 'Started the game', + [ActionType.JOIN_GAME]: 'Joined the game', + [ActionType.END_ROUND]: 'Ended the round', + [ActionType.START_NEW_ROUND]: 'Started a new round', + [ActionType.EXPIRE_GAME]: 'Expired the game', + [ActionType.END_GAME]: 'Ended the game', + [ActionType.PLACE_CARD]: 'Placed a card', + [ActionType.START_BIDDING]: 'Started bidding', + [ActionType.MAKE_BID]: 'Made a bid', + [ActionType.PASS]: 'Passed', + [ActionType.FLIP_CARD]: 'Flipped a card', + [ActionType.MESSAGE]: 'Sent a message', + [ActionType.QUIT_GAME]: 'Left the game', +}; diff --git a/chili-and-cilantro-lib/src/lib/constants.ts b/chili-and-cilantro-lib/src/lib/constants.ts index 7f18154..e533d74 100644 --- a/chili-and-cilantro-lib/src/lib/constants.ts +++ b/chili-and-cilantro-lib/src/lib/constants.ts @@ -121,15 +121,20 @@ export const JWT_ALGO: */ export const JWT_EXPIRATION = 86400; +/** + * The domain of the site + */ +export const SITE_DOMAIN = 'chilicilantro.com'; + /** * The address from which to send emails. */ -export const EMAIL_FROM = 'noreply@chilicilantro.com'; +export const EMAIL_FROM = `noreply@${SITE_DOMAIN}`; /** * The name of the application. */ -export const APPLICATION_NAME = 'Chili and Cilantro'; +export const APPLICATION_NAME = 'Chili & Cilantro'; /** * Duration in milliseconds for which an email token is valid. @@ -168,6 +173,7 @@ export const PASSWORD_REGEX_ERROR = export default { BCRYPT_ROUNDS, CHILI_PER_HAND, + EMAIL_FROM, EMAIL_TOKEN_RESEND_INTERVAL, GAME_CODE_LENGTH, JWT_ALGO, @@ -194,5 +200,6 @@ export default { ROUNDS_TO_WIN, USERNAME_REGEX, USERNAME_REGEX_ERROR, + SITE_DOMAIN, NONE: NONE, }; diff --git a/chili-and-cilantro-lib/src/lib/errors/invalid-action.ts b/chili-and-cilantro-lib/src/lib/errors/invalid-action.ts index cb1338e..4738d32 100644 --- a/chili-and-cilantro-lib/src/lib/errors/invalid-action.ts +++ b/chili-and-cilantro-lib/src/lib/errors/invalid-action.ts @@ -1,7 +1,5 @@ -import { - CardType, - TurnAction, -} from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { CardType } from '../enumerations/card-type'; +import { TurnAction } from '../enumerations/turn-action'; import { ValidationError } from './validation-error'; export class InvalidActionError extends ValidationError { diff --git a/chili-and-cilantro-lib/src/lib/errors/invalid-game-name.ts b/chili-and-cilantro-lib/src/lib/errors/invalid-game-name.ts index 754bb3d..77e0590 100644 --- a/chili-and-cilantro-lib/src/lib/errors/invalid-game-name.ts +++ b/chili-and-cilantro-lib/src/lib/errors/invalid-game-name.ts @@ -1,4 +1,4 @@ -import { constants } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import constants from '../constants'; import { ValidationError } from './validation-error'; export class InvalidGameNameError extends ValidationError { diff --git a/chili-and-cilantro-lib/src/lib/errors/invalid-game-password.ts b/chili-and-cilantro-lib/src/lib/errors/invalid-game-password.ts index 6214971..169d2ce 100644 --- a/chili-and-cilantro-lib/src/lib/errors/invalid-game-password.ts +++ b/chili-and-cilantro-lib/src/lib/errors/invalid-game-password.ts @@ -1,4 +1,4 @@ -import { constants } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import constants from '../constants'; import { ValidationError } from './validation-error'; export class InvalidGamePasswordError extends ValidationError { diff --git a/chili-and-cilantro-lib/src/lib/errors/invalid-message.ts b/chili-and-cilantro-lib/src/lib/errors/invalid-message.ts index beb76b6..1976493 100644 --- a/chili-and-cilantro-lib/src/lib/errors/invalid-message.ts +++ b/chili-and-cilantro-lib/src/lib/errors/invalid-message.ts @@ -1,4 +1,4 @@ -import { constants } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import constants from '../constants'; import { ValidationError } from './validation-error'; export class InvalidMessageError extends ValidationError { diff --git a/chili-and-cilantro-lib/src/lib/errors/invalid-user-display-name.ts b/chili-and-cilantro-lib/src/lib/errors/invalid-user-display-name.ts index 7768b38..5498f00 100644 --- a/chili-and-cilantro-lib/src/lib/errors/invalid-user-display-name.ts +++ b/chili-and-cilantro-lib/src/lib/errors/invalid-user-display-name.ts @@ -1,4 +1,4 @@ -import { constants } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import constants from '../constants'; import { ValidationError } from './validation-error'; export class InvalidUserDisplayNameError extends ValidationError { diff --git a/chili-and-cilantro-lib/src/lib/errors/out-of-ingredient.ts b/chili-and-cilantro-lib/src/lib/errors/out-of-ingredient.ts index 7a5fa29..0445673 100644 --- a/chili-and-cilantro-lib/src/lib/errors/out-of-ingredient.ts +++ b/chili-and-cilantro-lib/src/lib/errors/out-of-ingredient.ts @@ -1,4 +1,4 @@ -import { CardType } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { CardType } from '../enumerations/card-type'; import { ValidationError } from './validation-error'; export class OutOfIngredientError extends ValidationError { diff --git a/chili-and-cilantro-lib/src/lib/interfaces/bid.ts b/chili-and-cilantro-lib/src/lib/interfaces/bid.ts index ee99b35..e388ac3 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/bid.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/bid.ts @@ -1,10 +1,10 @@ -import { Types } from 'mongoose'; +import { DefaultIdType } from '../shared-types'; export interface IBid { /** * The chef who made the bid or pass */ - chefId: Types.ObjectId; + chefId: DefaultIdType; /** * The new bid amount. If the chef is passing, the bid remains the same. */ diff --git a/chili-and-cilantro-lib/src/lib/interfaces/card.ts b/chili-and-cilantro-lib/src/lib/interfaces/card.ts index 9281afc..c3aa375 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/card.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/card.ts @@ -1,7 +1,6 @@ import { CardType } from '../enumerations/card-type'; -import { IHasID } from './has-id'; -export interface ICard extends IHasID { +export interface ICard { type: CardType; faceUp: boolean; } diff --git a/chili-and-cilantro-lib/src/lib/interfaces/create-user-basics.ts b/chili-and-cilantro-lib/src/lib/interfaces/create-user-basics.ts index 617f20f..e2bc8ed 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/create-user-basics.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/create-user-basics.ts @@ -3,7 +3,6 @@ export interface ICreateUserBasics { username: string; - languages: string[]; /** * The user's email address, used for login if accountType is email/password. * Used for sending notifications, regardless. diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/action.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/action.ts index 52cef72..e3403a3 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/action.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/action.ts @@ -1,4 +1,10 @@ +import { DefaultIdType } from '../../shared-types'; import { IAction } from '../models/action'; +import { IActionDetailsBase } from '../models/actions/details/base'; import { IBaseDocument } from './base'; -export interface IActionDocument extends IAction, IBaseDocument {} +export interface IActionDocument< + I = DefaultIdType, + D extends IActionDetailsBase = IActionDetailsBase, +> extends IBaseDocument, I>, + IAction {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/create-game.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/create-game.ts index f9caaf6..83d6b0b 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/create-game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/create-game.ts @@ -1,6 +1,8 @@ +import { DefaultIdType } from '../../../shared-types'; import { ICreateGameAction } from '../../models/actions/create-game'; -import { IBaseDocument } from '../base'; +import { ICreateGameDetails } from '../../models/actions/details/create-game'; +import { IActionDocument } from '../action'; -export interface ICreateGameActionDocument - extends ICreateGameAction, - IBaseDocument {} +export interface ICreateGameActionDocument + extends IActionDocument, + ICreateGameAction {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/end-game.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/end-game.ts index d465819..614e12d 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/end-game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/end-game.ts @@ -1,6 +1,8 @@ +import { DefaultIdType } from '../../../shared-types'; +import { IEndGameDetails } from '../../models/actions/details/end-game'; import { IEndGameAction } from '../../models/actions/end-game'; -import { IBaseDocument } from '../base'; +import { IActionDocument } from '../action'; -export interface IEndGameActionDocument - extends IEndGameAction, - IBaseDocument {} +export interface IEndGameActionDocument + extends IActionDocument, + IEndGameAction {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/end-round.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/end-round.ts index 7f8eabf..e7534f3 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/end-round.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/end-round.ts @@ -1,6 +1,6 @@ import { IEndRoundAction } from '../../models/actions/end-round'; -import { IBaseDocument } from '../base'; +import { IActionDocument } from '../action'; export interface IEndRoundActionDocument - extends IEndRoundAction, - IBaseDocument {} + extends IActionDocument, + IEndRoundAction {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/expire-game.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/expire-game.ts index de0361e..24f895d 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/expire-game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/expire-game.ts @@ -1,6 +1,8 @@ +import { DefaultIdType } from '../../../shared-types'; +import { IExpireGameDetails } from '../../models/actions/details/expire-game'; import { IExpireGameAction } from '../../models/actions/expire-game'; -import { IBaseDocument } from '../base'; +import { IActionDocument } from '../action'; -export interface IExpireGameActionDocument - extends IExpireGameAction, - IBaseDocument {} +export interface IExpireGameActionDocument + extends IActionDocument, + IExpireGameAction {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/flip-card.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/flip-card.ts index a8693d2..6d57413 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/flip-card.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/flip-card.ts @@ -1,6 +1,8 @@ +import { DefaultIdType } from '../../../shared-types'; +import { IFlipCardDetails } from '../../models/actions/details/flip-card'; import { IFlipCardAction } from '../../models/actions/flip-card'; -import { IBaseDocument } from '../base'; +import { IActionDocument } from '../action'; -export interface IFlipCardActionDocument - extends IFlipCardAction, - IBaseDocument {} +export interface IFlipCardActionDocument + extends IActionDocument, + IFlipCardAction {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/join-game.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/join-game.ts index a5e01b6..a293e27 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/join-game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/join-game.ts @@ -1,6 +1,8 @@ +import { DefaultIdType } from '../../../shared-types'; +import { IJoinGameDetails } from '../../models/actions/details/join-game'; import { IJoinGameAction } from '../../models/actions/join-game'; -import { IBaseDocument } from '../base'; +import { IActionDocument } from '../action'; -export interface IJoinGameActionDocument - extends IJoinGameAction, - IBaseDocument {} +export interface IJoinGameActionDocument + extends IActionDocument, + IJoinGameAction {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/make-bid.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/make-bid.ts index 2a75460..60948d1 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/make-bid.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/make-bid.ts @@ -1,6 +1,8 @@ +import { DefaultIdType } from '../../../shared-types'; +import { IMakeBidDetails } from '../../models/actions/details/make-bid'; import { IMakeBidAction } from '../../models/actions/make-bid'; -import { IBaseDocument } from '../base'; +import { IActionDocument } from '../action'; -export interface IMakeBidActionDocument - extends IMakeBidAction, - IBaseDocument {} +export interface IMakeBidActionDocument + extends IActionDocument, + IMakeBidAction {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/message.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/message.ts index 008ca28..a216bca 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/message.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/message.ts @@ -1,6 +1,8 @@ +import { DefaultIdType } from '../../../shared-types'; +import { IMessageDetails } from '../../models/actions/details/message'; import { IMessageAction } from '../../models/actions/message'; -import { IBaseDocument } from '../base'; +import { IActionDocument } from '../action'; -export interface IMessageActionDocument - extends IMessageAction, - IBaseDocument {} +export interface IMessageActionDocument + extends IActionDocument, + IMessageAction {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/pass.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/pass.ts index 9044ec5..76b0eda 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/pass.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/pass.ts @@ -1,6 +1,8 @@ +import { DefaultIdType } from '../../../shared-types'; +import { IPassDetails } from '../../models/actions/details/pass'; import { IPassAction } from '../../models/actions/pass'; -import { IBaseDocument } from '../base'; +import { IActionDocument } from '../action'; -export interface IPassActionDocument - extends IPassAction, - IBaseDocument {} +export interface IPassActionDocument + extends IActionDocument, + IPassAction {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/place-card.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/place-card.ts index 7265d21..b24071f 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/place-card.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/place-card.ts @@ -1,6 +1,8 @@ +import { DefaultIdType } from '../../../shared-types'; +import { IPlaceCardDetails } from '../../models/actions/details/place-card'; import { IPlaceCardAction } from '../../models/actions/place-card'; -import { IBaseDocument } from '../base'; +import { IActionDocument } from '../action'; -export interface IPlaceCardActionDocument - extends IPlaceCardAction, - IBaseDocument {} +export interface IPlaceCardActionDocument + extends IActionDocument, + IPlaceCardAction {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/quit-game.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/quit-game.ts index 56983c9..0294427 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/quit-game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/quit-game.ts @@ -1,6 +1,8 @@ +import { DefaultIdType } from '../../../shared-types'; +import { IQuitGameDetails } from '../../models/actions/details/quit-game'; import { IQuitGameAction } from '../../models/actions/quit-game'; -import { IBaseDocument } from '../base'; +import { IActionDocument } from '../action'; -export interface IQuitGameActionDocument - extends IQuitGameAction, - IBaseDocument {} +export interface IQuitGameActionDocument + extends IActionDocument, + IQuitGameAction {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/start-bidding.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/start-bidding.ts index a792dff..a2a5a64 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/start-bidding.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/start-bidding.ts @@ -1,6 +1,8 @@ +import { DefaultIdType } from '../../../shared-types'; +import { IStartBiddingDetails } from '../../models/actions/details/start-bidding'; import { IStartBiddingAction } from '../../models/actions/start-bidding'; -import { IBaseDocument } from '../base'; +import { IActionDocument } from '../action'; -export interface IStartBiddingActionDocument - extends IStartBiddingAction, - IBaseDocument {} +export interface IStartBiddingActionDocument + extends IActionDocument, + IStartBiddingAction {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/start-game.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/start-game.ts index 8105453..6440191 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/start-game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/start-game.ts @@ -1,6 +1,8 @@ +import { DefaultIdType } from '../../../shared-types'; +import { IStartGameDetails } from '../../models/actions/details/start-game'; import { IStartGameAction } from '../../models/actions/start-game'; -import { IBaseDocument } from '../base'; +import { IActionDocument } from '../action'; -export interface IStartGameActionDocument - extends IStartGameAction, - IBaseDocument {} +export interface IStartGameActionDocument + extends IActionDocument, + IStartGameAction {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/start-new-round.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/start-new-round.ts index 064a24d..6f42d73 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/start-new-round.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/actions/start-new-round.ts @@ -1,6 +1,8 @@ +import { DefaultIdType } from '../../../shared-types'; +import { IStartNewRoundDetails } from '../../models/actions/details/start-new-round'; import { IStartNewRoundAction } from '../../models/actions/start-new-round'; -import { IBaseDocument } from '../base'; +import { IActionDocument } from '../action'; -export interface IStartNewRoundActionDocument - extends IStartNewRoundAction, - IBaseDocument {} +export interface IStartNewRoundActionDocument + extends IActionDocument, + IStartNewRoundAction {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/base.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/base.ts index b9968a5..e2f3a52 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/base.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/base.ts @@ -1,4 +1,4 @@ -import { Document, Types } from 'mongoose'; +import { Document } from 'mongoose'; +import { DefaultIdType } from '../../shared-types'; -export interface IBaseDocument - extends Document {} +export type IBaseDocument = Document & T; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/card.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/card.ts index 60598b8..972614c 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/card.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/card.ts @@ -1,6 +1,7 @@ -import { Document, Types } from 'mongoose'; +import { DefaultIdType } from '../../shared-types'; import { ICard } from '../card'; +import { IBaseDocument } from './base'; -export interface ICardDocument - extends ICard, - Document {} +export interface ICardDocument + extends IBaseDocument, + ICard {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/chef.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/chef.ts index 61ba242..0732ee6 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/chef.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/chef.ts @@ -1,4 +1,7 @@ +import { DefaultIdType } from '../../shared-types'; import { IChef } from '../models/chef'; import { IBaseDocument } from './base'; -export interface IChefDocument extends IChef, IBaseDocument {} +export interface IChefDocument + extends IBaseDocument, + IChef {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/email-token.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/email-token.ts index 6ec99a3..4d47409 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/email-token.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/email-token.ts @@ -1,9 +1,10 @@ -import { Document, Types } from 'mongoose'; +import { DefaultIdType } from '../../shared-types'; import { IEmailToken } from '../models/email-token'; +import { IBaseDocument } from './base'; /** * Composite interface for email token collection documents */ -export interface IEmailTokenDocument - extends IEmailToken, - Document {} +export interface IEmailTokenDocument + extends IBaseDocument, + IEmailToken {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/game.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/game.ts index cf25414..6ddc71d 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/game.ts @@ -1,4 +1,7 @@ +import { DefaultIdType } from '../../shared-types'; import { IGame } from '../models/game'; import { IBaseDocument } from './base'; -export interface IGameDocument extends IGame, IBaseDocument {} +export interface IGameDocument + extends IBaseDocument, + IGame {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/documents/user.ts b/chili-and-cilantro-lib/src/lib/interfaces/documents/user.ts index 7dfdc2f..07b9ffe 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/documents/user.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/documents/user.ts @@ -1,4 +1,7 @@ +import { DefaultIdType } from '../../shared-types'; import { IUser } from '../models/user'; import { IBaseDocument } from './base'; -export interface IUserDocument extends IUser, IBaseDocument {} +export interface IUserDocument + extends IBaseDocument, + IUser {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/has-id.ts b/chili-and-cilantro-lib/src/lib/interfaces/has-id.ts index 02ceba1..034c033 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/has-id.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/has-id.ts @@ -1,9 +1,9 @@ -import { Types } from 'mongoose'; +import { DefaultIdType } from '../shared-types'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export interface IHasID { +export interface IHasID { /** * The MongoDB unique identifier for the object. */ - _id?: T; + _id?: I; } diff --git a/chili-and-cilantro-lib/src/lib/interfaces/has-soft-deleter.ts b/chili-and-cilantro-lib/src/lib/interfaces/has-soft-deleter.ts index 7422685..d62c278 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/has-soft-deleter.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/has-soft-deleter.ts @@ -1,9 +1,9 @@ -import { Types } from 'mongoose'; +import { DefaultIdType } from '../shared-types'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface IHasSoftDeleter { /** * The MongoDB unique identifier for the user who deleted the object. */ - deletedBy?: Types.ObjectId; + deletedBy?: DefaultIdType; } diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/action.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/action.ts index c726b13..0bb7fb7 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/action.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/action.ts @@ -1,12 +1,16 @@ -import { Types } from 'mongoose'; import { ActionType } from '../../enumerations/action-type'; +import { DefaultIdType } from '../../shared-types'; import { IHasTimestamps } from '../has-timestamps'; +import { IActionDetailsBase } from './actions/details/base'; -export interface IAction extends IHasTimestamps { - gameId: Types.ObjectId; - chefId: Types.ObjectId; - userId: Types.ObjectId; +export interface IAction< + I = DefaultIdType, + D extends IActionDetailsBase = IActionDetailsBase, +> extends IHasTimestamps { + gameId: I; + chefId: I; + userId: I; type: ActionType; - details: object; + details: D; round: number; } diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/create-game.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/create-game.ts index 057001e..ae7aed9 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/create-game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/create-game.ts @@ -1,6 +1,8 @@ +import { DefaultIdType } from '../../../shared-types'; import { IAction } from '../action'; import { ICreateGameDetails } from './details/create-game'; -export interface ICreateGameAction extends IAction { - details: ICreateGameDetails; -} +export type ICreateGameAction = IAction< + I, + ICreateGameDetails +>; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/base.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/base.ts new file mode 100644 index 0000000..e60c115 --- /dev/null +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/base.ts @@ -0,0 +1 @@ +export interface IActionDetailsBase {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/create-game.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/create-game.ts index a8554ed..62e1719 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/create-game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/create-game.ts @@ -1 +1,3 @@ -export interface ICreateGameDetails {} +import { IActionDetailsBase } from './base'; + +export type ICreateGameDetails = IActionDetailsBase; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/end-game.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/end-game.ts index ec8ad68..0ca0ea1 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/end-game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/end-game.ts @@ -1 +1,3 @@ -export interface IEndGameDetails {} +import { IActionDetailsBase } from './base'; + +export type IEndGameDetails = IActionDetailsBase; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/end-round.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/end-round.ts index 12584b8..0da233d 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/end-round.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/end-round.ts @@ -1 +1,3 @@ -export interface IEndRoundDetails {} +import { IActionDetailsBase } from './base'; + +export type IEndRoundDetails = IActionDetailsBase; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/expire-game.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/expire-game.ts index f8ff221..11f0644 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/expire-game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/expire-game.ts @@ -1 +1,3 @@ -export interface IExpireGameDetails {} +import { IActionDetailsBase } from './base'; + +export type IExpireGameDetails = IActionDetailsBase; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/flip-card.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/flip-card.ts index dd7b43e..4b328d1 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/flip-card.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/flip-card.ts @@ -1,7 +1,8 @@ -import { Types } from 'mongoose'; +import { DefaultIdType } from '../../../../shared-types'; +import { IActionDetailsBase } from './base'; -export interface IFlipCardDetails { - chef: Types.ObjectId; - card: Types.ObjectId; +export interface IFlipCardDetails extends IActionDetailsBase { + chef: DefaultIdType; + card: DefaultIdType; cardIndex: number; } diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/join-game.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/join-game.ts index a465966..33054bf 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/join-game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/join-game.ts @@ -1 +1,3 @@ -export interface IJoinGameDetails {} +import { IActionDetailsBase } from './base'; + +export type IJoinGameDetails = IActionDetailsBase; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/make-bid.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/make-bid.ts index e33427b..2d38d67 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/make-bid.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/make-bid.ts @@ -1,4 +1,6 @@ -export interface IMakeBidDetails { +import { IActionDetailsBase } from './base'; + +export interface IMakeBidDetails extends IActionDetailsBase { /** * The number of cards the chef proposes they can flip without hitting a 'CHILI' */ diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/message.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/message.ts index f90e897..f86bc7a 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/message.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/message.ts @@ -1,3 +1,5 @@ -export interface IMessageDetails { +import { IActionDetailsBase } from './base'; + +export interface IMessageDetails extends IActionDetailsBase { message: string; } diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/pass.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/pass.ts index 134e2c3..ba8e409 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/pass.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/pass.ts @@ -1 +1,3 @@ -export interface IPassDetails {} +import { IActionDetailsBase } from './base'; + +export type IPassDetails = IActionDetailsBase; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/place-card.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/place-card.ts index f18381b..04538ce 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/place-card.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/place-card.ts @@ -1,6 +1,7 @@ import { CardType } from '../../../../enumerations/card-type'; +import { IActionDetailsBase } from './base'; -export interface IPlaceCardDetails { +export interface IPlaceCardDetails extends IActionDetailsBase { /** * The type of card placed, e.g., 'Cilantro' or 'Chili'. This might be hidden from other chefs but known to the game logic to check the results later. */ diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/quit-game.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/quit-game.ts index 0884b1e..27ffddc 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/quit-game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/quit-game.ts @@ -1,5 +1,6 @@ import { QuitGameReason } from '../../../../enumerations/quit-game-reason'; +import { IActionDetailsBase } from './base'; -export interface IQuitGameDetails { +export interface IQuitGameDetails extends IActionDetailsBase { reason: QuitGameReason; } diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/start-bidding.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/start-bidding.ts index f032f92..26142e5 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/start-bidding.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/start-bidding.ts @@ -1,3 +1,5 @@ -export interface IStartBiddingDetails { +import { IActionDetailsBase } from './base'; + +export interface IStartBiddingDetails extends IActionDetailsBase { bid: number; } diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/start-game.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/start-game.ts index b64105e..2b56713 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/start-game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/start-game.ts @@ -1 +1,3 @@ -export interface IStartGameDetails {} +import { IActionDetailsBase } from './base'; + +export type IStartGameDetails = IActionDetailsBase; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/start-new-round.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/start-new-round.ts index fd57cd6..b0c95df 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/start-new-round.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/details/start-new-round.ts @@ -1 +1,3 @@ -export interface IStartNewRoundDetails {} +import { IActionDetailsBase } from './base'; + +export type IStartNewRoundDetails = IActionDetailsBase; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/end-game.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/end-game.ts index 7ceb006..562421a 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/end-game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/end-game.ts @@ -1,6 +1,5 @@ +import { DefaultIdType } from '../../../shared-types'; import { IAction } from '../action'; import { IEndGameDetails } from './details/end-game'; -export interface IEndGameAction extends IAction { - details: IEndGameDetails; -} +export type IEndGameAction = IAction; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/end-round.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/end-round.ts index 3d20bc8..51de2f1 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/end-round.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/end-round.ts @@ -1,6 +1,5 @@ +import { DefaultIdType } from '../../../shared-types'; import { IAction } from '../action'; import { IEndRoundDetails } from './details/end-round'; -export interface IEndRoundAction extends IAction { - details: IEndRoundDetails; -} +export type IEndRoundAction = IAction; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/expire-game.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/expire-game.ts index 079a9ae..b23a205 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/expire-game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/expire-game.ts @@ -1,6 +1,8 @@ +import { DefaultIdType } from '../../../shared-types'; import { IAction } from '../action'; import { IExpireGameDetails } from './details/expire-game'; -export interface IExpireGameAction extends IAction { - details: IExpireGameDetails; -} +export type IExpireGameAction = IAction< + I, + IExpireGameDetails +>; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/flip-card.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/flip-card.ts index b92be69..7afb0fa 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/flip-card.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/flip-card.ts @@ -1,6 +1,5 @@ +import { DefaultIdType } from '../../../shared-types'; import { IAction } from '../action'; import { IFlipCardDetails } from './details/flip-card'; -export interface IFlipCardAction extends IAction { - details: IFlipCardDetails; -} +export type IFlipCardAction = IAction; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/join-game.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/join-game.ts index b1e0cb2..c7ef0ec 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/join-game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/join-game.ts @@ -1,6 +1,5 @@ +import { DefaultIdType } from '../../../shared-types'; import { IAction } from '../action'; import { IJoinGameDetails } from './details/join-game'; -export interface IJoinGameAction extends IAction { - details: IJoinGameDetails; -} +export type IJoinGameAction = IAction; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/make-bid.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/make-bid.ts index 54af492..57ee4e8 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/make-bid.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/make-bid.ts @@ -1,6 +1,5 @@ +import { DefaultIdType } from '../../../shared-types'; import { IAction } from '../action'; import { IMakeBidDetails } from './details/make-bid'; -export interface IMakeBidAction extends IAction { - details: IMakeBidDetails; -} +export type IMakeBidAction = IAction; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/message.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/message.ts index 10533c3..7833d8d 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/message.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/message.ts @@ -1,6 +1,5 @@ +import { DefaultIdType } from '../../../shared-types'; import { IAction } from '../action'; import { IMessageDetails } from './details/message'; -export interface IMessageAction extends IAction { - details: IMessageDetails; -} +export type IMessageAction = IAction; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/pass.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/pass.ts index 0aaa19b..81462d5 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/pass.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/pass.ts @@ -1,6 +1,5 @@ +import { DefaultIdType } from '../../../shared-types'; import { IAction } from '../action'; import { IPassDetails } from './details/pass'; -export interface IPassAction extends IAction { - details: IPassDetails; -} +export type IPassAction = IAction; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/place-card.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/place-card.ts index c4d9735..bdbcabc 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/place-card.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/place-card.ts @@ -1,6 +1,5 @@ +import { DefaultIdType } from '../../../shared-types'; import { IAction } from '../action'; import { IPlaceCardDetails } from './details/place-card'; -export interface IPlaceCardAction extends IAction { - details: IPlaceCardDetails; -} +export type IPlaceCardAction = IAction; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/quit-game.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/quit-game.ts index 65e0e82..525e3f3 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/quit-game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/quit-game.ts @@ -1,6 +1,5 @@ +import { DefaultIdType } from '../../../shared-types'; import { IAction } from '../action'; import { IQuitGameDetails } from './details/quit-game'; -export interface IQuitGameAction extends IAction { - details: IQuitGameDetails; -} +export type IQuitGameAction = IAction; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/start-bidding.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/start-bidding.ts index 9d0350d..54675a2 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/start-bidding.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/start-bidding.ts @@ -1,6 +1,8 @@ +import { DefaultIdType } from '../../../shared-types'; import { IAction } from '../action'; import { IStartBiddingDetails } from './details/start-bidding'; -export interface IStartBiddingAction extends IAction { - details: IStartBiddingDetails; -} +export type IStartBiddingAction = IAction< + I, + IStartBiddingDetails +>; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/start-game.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/start-game.ts index 8d10b0a..19deea8 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/start-game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/start-game.ts @@ -1,6 +1,5 @@ +import { DefaultIdType } from '../../../shared-types'; import { IAction } from '../action'; import { IStartGameDetails } from './details/start-game'; -export interface IStartGameAction extends IAction { - details: IStartGameDetails; -} +export type IStartGameAction = IAction; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/start-new-round.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/start-new-round.ts index e51baee..d6f01ce 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/actions/start-new-round.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/actions/start-new-round.ts @@ -1,6 +1,8 @@ +import { DefaultIdType } from '../../../shared-types'; import { IAction } from '../action'; import { IStartNewRoundDetails } from './details/start-new-round'; -export interface IStartNewRoundAction extends IAction { - details: IStartNewRoundDetails; -} +export type IStartNewRoundAction = IAction< + I, + IStartNewRoundDetails +>; diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/chef.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/chef.ts index c3cfdc2..a19d82d 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/chef.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/chef.ts @@ -1,15 +1,15 @@ -import { Types } from 'mongoose'; import { CardType } from '../../enumerations/card-type'; import { ChefState } from '../../enumerations/chef-state'; +import { DefaultIdType } from '../../shared-types'; import { ICard } from '../card'; -export interface IChef { - gameId: Types.ObjectId; +export interface IChef { + gameId: I; name: string; hand: ICard[]; placedCards: ICard[]; lostCards: CardType[]; - userId: Types.ObjectId; + userId: I; state: ChefState; host: boolean; } diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/email-token.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/email-token.ts index 3b85d44..6e26ca7 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/email-token.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/email-token.ts @@ -1,15 +1,15 @@ -import { Types } from 'mongoose'; import { EmailTokenType } from '../../enumerations/email-token-type'; +import { DefaultIdType } from '../../shared-types'; import { IHasCreation } from '../has-creation'; /** * Base interface for email token collection documents */ -export interface IEmailToken extends IHasCreation { +export interface IEmailToken extends IHasCreation { /** * The user ID associated with the token */ - userId: Types.ObjectId; + userId: I; /** * The type of token */ diff --git a/chili-and-cilantro-lib/src/lib/interfaces/models/game.ts b/chili-and-cilantro-lib/src/lib/interfaces/models/game.ts index 5b955e3..d3b6070 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/models/game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/models/game.ts @@ -1,10 +1,8 @@ -import { Types } from 'mongoose'; import { GamePhase } from '../../enumerations/game-phase'; +import { DefaultIdType } from '../../shared-types'; import { IBid } from '../bid'; -import { IHasID } from '../has-id'; -import { IHasTimestamps } from '../has-timestamps'; -export interface IGame extends IHasID, IHasTimestamps { +export interface IGame { /** * The game code. */ @@ -20,11 +18,11 @@ export interface IGame extends IHasID, IHasTimestamps { /** * Chef IDs in the game */ - chefIds: Types.ObjectId[]; + chefIds: I[]; /** * Eliminated chefs. */ - eliminatedChefIds: Types.ObjectId[]; + eliminatedChefIds: I[]; /** * Maximum number of chefs that can join the game. */ @@ -56,25 +54,25 @@ export interface IGame extends IHasID, IHasTimestamps { /** * The winning ChefIDs for each round. */ - roundWinners: Record; + roundWinners: Record; /** * The turn order for the game. ChefIDs shuffled randomly into a turn order when the game is started. */ - turnOrder: Types.ObjectId[]; + turnOrder: I[]; /** * The chef ID of the host chef who makes game decisions. */ - hostChefId: Types.ObjectId; + hostChefId: I; /** * The user ID of the host chef who created the game. */ - hostUserId: Types.ObjectId; + hostUserId: I; /** * The ID of the last game this is a continuation of. */ - lastGame?: Types.ObjectId; + lastGame?: I; /** * The winner of the game. */ - winner?: Types.ObjectId; + winner?: I; } diff --git a/chili-and-cilantro-lib/src/lib/interfaces/objects/action.ts b/chili-and-cilantro-lib/src/lib/interfaces/objects/action.ts index ed6f504..48c01e0 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/objects/action.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/objects/action.ts @@ -1,4 +1,10 @@ +import { Types } from 'mongoose'; import { IHasID } from '../has-id'; import { IAction } from '../models/action'; +import { IActionDetailsBase } from '../models/actions/details/base'; -export interface IActionObject extends IAction, IHasID {} +export interface IActionObject< + I = Types.ObjectId, + D extends IActionDetailsBase = IActionDetailsBase, +> extends IAction, + IHasID {} diff --git a/chili-and-cilantro-lib/src/lib/interfaces/objects/actions/create-game.ts b/chili-and-cilantro-lib/src/lib/interfaces/objects/actions/create-game.ts index ae94d4c..5386943 100644 --- a/chili-and-cilantro-lib/src/lib/interfaces/objects/actions/create-game.ts +++ b/chili-and-cilantro-lib/src/lib/interfaces/objects/actions/create-game.ts @@ -1,3 +1,3 @@ import { IActionObject } from '../action'; -export interface ICreateGameActionObject extends IActionObject {} +export type ICreateGameActionObject = IActionObject; diff --git a/chili-and-cilantro-lib/src/lib/shared-types.ts b/chili-and-cilantro-lib/src/lib/shared-types.ts new file mode 100644 index 0000000..7fe53eb --- /dev/null +++ b/chili-and-cilantro-lib/src/lib/shared-types.ts @@ -0,0 +1,53 @@ +import { ClientSession, Model, Types } from 'mongoose'; +import ModelName from './enumerations/model-name'; +import { IBaseDocument } from './interfaces/documents/base'; +import { IMakeBidAction } from './interfaces/models/actions/make-bid'; +import { IMessageAction } from './interfaces/models/actions/message'; +import { IPassAction } from './interfaces/models/actions/pass'; +import { IPlaceCardAction } from './interfaces/models/actions/place-card'; +import { IQuitGameAction } from './interfaces/models/actions/quit-game'; +import { IStartBiddingAction } from './interfaces/models/actions/start-bidding'; +import { IStartGameAction } from './interfaces/models/actions/start-game'; +import { IStartNewRoundAction } from './interfaces/models/actions/start-new-round'; +import { IChef } from './interfaces/models/chef'; +import { IEmailToken } from './interfaces/models/email-token'; +import { IGame } from './interfaces/models/game'; +import { IUser } from './interfaces/models/user'; + +export type ChiliCilantroActions = + | IBaseDocument + | IBaseDocument + | IBaseDocument + | IBaseDocument + | IBaseDocument + | IBaseDocument + | IBaseDocument + | IBaseDocument; + +export type ChiliCilantroDocuments = + | ChiliCilantroActions + | IBaseDocument + | IBaseDocument + | IBaseDocument + | IBaseDocument; + +/** + * Transaction callback type for withTransaction + */ +export type TransactionCallback = ( + session: ClientSession | undefined, + ...args: any +) => Promise; +/** + * Get model function type + */ +export type GetModelFunction = >( + modelName: ModelName, +) => Model; +/** + * Validated body for express-validator + */ +export type ValidatedBody = { + [K in T]: any; +}; +export type DefaultIdType = Types.ObjectId; diff --git a/chili-and-cilantro-lib/src/lib/turn-action-strings.ts b/chili-and-cilantro-lib/src/lib/turn-action-strings.ts new file mode 100644 index 0000000..fa5d708 --- /dev/null +++ b/chili-and-cilantro-lib/src/lib/turn-action-strings.ts @@ -0,0 +1,8 @@ +import TurnAction from './enumerations/turn-action'; + +export const TurnActionStrings: { [key in TurnAction]: string } = { + [TurnAction.Bid]: 'Made a bid', + [TurnAction.IncreaseBid]: 'Increased bid', + [TurnAction.Pass]: 'Passed', + [TurnAction.PlaceCard]: 'Placed a card', +}; diff --git a/chili-and-cilantro-lib/tsconfig.spec.json b/chili-and-cilantro-lib/tsconfig.spec.json index bd9faa2..5f4b531 100644 --- a/chili-and-cilantro-lib/tsconfig.spec.json +++ b/chili-and-cilantro-lib/tsconfig.spec.json @@ -4,7 +4,8 @@ "outDir": "../dist/out-tsc", "module": "commonjs", "types": ["jest", "node"], - "composite": true + "composite": true, + "declaration": true }, "include": [ "jest.config.ts", diff --git a/chili-and-cilantro-lib/yarn.lock b/chili-and-cilantro-lib/yarn.lock index 6131111..a281874 100644 --- a/chili-and-cilantro-lib/yarn.lock +++ b/chili-and-cilantro-lib/yarn.lock @@ -2,7 +2,755 @@ # yarn lockfile v1 -tslib@^2.3.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" - integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== +"@eslint-community/eslint-utils@^4.2.0": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz#d1145bf2c20132d6400495d6df4bf59362fd9d56" + integrity sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.12.1": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== + +"@eslint/config-array@^0.19.0": + version "0.19.0" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.19.0.tgz#3251a528998de914d59bb21ba4c11767cf1b3519" + integrity sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ== + dependencies: + "@eslint/object-schema" "^2.1.4" + debug "^4.3.1" + minimatch "^3.1.2" + +"@eslint/core@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.9.0.tgz#168ee076f94b152c01ca416c3e5cf82290ab4fcd" + integrity sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg== + +"@eslint/eslintrc@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.2.0.tgz#57470ac4e2e283a6bf76044d63281196e370542c" + integrity sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^10.0.1" + globals "^14.0.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@9.15.0": + version "9.15.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.15.0.tgz#df0e24fe869143b59731942128c19938fdbadfb5" + integrity sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg== + +"@eslint/object-schema@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.4.tgz#9e69f8bb4031e11df79e03db09f9dbbae1740843" + integrity sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ== + +"@eslint/plugin-kit@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz#812980a6a41ecf3a8341719f92a6d1e784a2e0e8" + integrity sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA== + dependencies: + levn "^0.4.1" + +"@humanfs/core@^0.19.1": + version "0.19.1" + resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" + integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== + +"@humanfs/node@^0.16.6": + version "0.16.6" + resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.6.tgz#ee2a10eaabd1131987bf0488fd9b820174cd765e" + integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw== + dependencies: + "@humanfs/core" "^0.19.1" + "@humanwhocodes/retry" "^0.3.0" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/retry@^0.3.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a" + integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA== + +"@humanwhocodes/retry@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.1.tgz#9a96ce501bc62df46c4031fbd970e3cc6b10f07b" + integrity sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA== + +"@mongodb-js/saslprep@^1.1.0": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz#e974bab8eca9faa88677d4ea4da8d09a52069004" + integrity sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw== + dependencies: + sparse-bitfield "^3.0.3" + +"@types/bcrypt@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@types/bcrypt/-/bcrypt-5.0.2.tgz#22fddc11945ea4fbc3655b3e8b8847cc9f811477" + integrity sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ== + dependencies: + "@types/node" "*" + +"@types/estree@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/jsonwebtoken@^9.0.6": + version "9.0.7" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz#e49b96c2b29356ed462e9708fc73b833014727d2" + integrity sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg== + dependencies: + "@types/node" "*" + +"@types/node@*": + version "22.9.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.4.tgz#31eefcdbe163a51f53cbfbb3e121b8ae9b16fdb2" + integrity sha512-d9RWfoR7JC/87vj7n+PVTzGg9hDyuFjir3RxUHbjFSKNd9mpxbxwMEyaCim/ddCmy4IuW7HjTzF3g9p3EtWEOg== + dependencies: + undici-types "~6.19.8" + +"@types/webidl-conversions@*": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz#1306dbfa53768bcbcfc95a1c8cde367975581859" + integrity sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA== + +"@types/whatwg-url@^8.2.1": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-8.2.2.tgz#749d5b3873e845897ada99be4448041d4cc39e63" + integrity sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA== + dependencies: + "@types/node" "*" + "@types/webidl-conversions" "*" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.14.0: + version "8.14.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" + integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== + +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + 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" + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +bson@^5.5.0: + version "5.5.1" + resolved "https://registry.yarnpkg.com/bson/-/bson-5.5.1.tgz#f5849d405711a7f23acdda9a442375df858e6833" + integrity sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g== + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +cross-spawn@^7.0.5: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@4.x, debug@^4.3.1, debug@^4.3.2: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-scope@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.2.0.tgz#377aa6f1cb5dc7592cfd0b7f892fd0cf352ce442" + integrity sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint-visitor-keys@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" + integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== + +eslint@^9.15.0: + version "9.15.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.15.0.tgz#77c684a4e980e82135ebff8ee8f0a9106ce6b8a6" + integrity sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.12.1" + "@eslint/config-array" "^0.19.0" + "@eslint/core" "^0.9.0" + "@eslint/eslintrc" "^3.2.0" + "@eslint/js" "9.15.0" + "@eslint/plugin-kit" "^0.2.3" + "@humanfs/node" "^0.16.6" + "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.4.1" + "@types/estree" "^1.0.6" + "@types/json-schema" "^7.0.15" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.5" + debug "^4.3.2" + escape-string-regexp "^4.0.0" + eslint-scope "^8.2.0" + eslint-visitor-keys "^4.2.0" + espree "^10.3.0" + esquery "^1.5.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^8.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + json-stable-stringify-without-jsonify "^1.0.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + +espree@^10.0.1, espree@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a" + integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg== + dependencies: + acorn "^8.14.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.2.0" + +esquery@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +express-validator@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/express-validator/-/express-validator-7.2.0.tgz#f6077758732d52e2365bb983b1abaca51bbefba6" + integrity sha512-I2ByKD8panjtr8Y05l21Wph9xk7kk64UMyvJCl/fFM/3CTJq8isXYPLeKW/aZBCdb/LYNv63PwhY8khw8VWocA== + dependencies: + lodash "^4.17.21" + validator "~13.12.0" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== + dependencies: + flat-cache "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.4" + +flatted@^3.2.9: + version "3.3.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.2.tgz#adba1448a9841bec72b42c532ea23dbbedef1a27" + integrity sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA== + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.0, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +kareem@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.5.1.tgz#7b8203e11819a8e77a34b3517d3ead206764d15d" + integrity sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA== + +keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +memory-pager@^1.0.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" + integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== + +minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +mongodb-connection-string-url@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz#57901bf352372abdde812c81be47b75c6b2ec5cf" + integrity sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ== + dependencies: + "@types/whatwg-url" "^8.2.1" + whatwg-url "^11.0.0" + +mongodb@5.9.2: + version "5.9.2" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-5.9.2.tgz#39a73b9fbc87ac9d9c1aaf8aab5c5bb69e2b913e" + integrity sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ== + dependencies: + bson "^5.5.0" + mongodb-connection-string-url "^2.6.0" + socks "^2.7.1" + optionalDependencies: + "@mongodb-js/saslprep" "^1.1.0" + +mongoose@^7.6.2: + version "7.8.2" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-7.8.2.tgz#578e4d0b0b60421459399cfc47cab2a43d90155f" + integrity sha512-/KDcZL84gg8hnmOHRRPK49WtxH3Xsph38c7YqvYPdxEB2OsDAXvwAknGxyEC0F2P3RJCqFOp+523iFCa0p3dfw== + dependencies: + bson "^5.5.0" + kareem "2.5.1" + mongodb "5.9.2" + mpath "0.9.0" + mquery "5.0.0" + ms "2.1.3" + sift "16.0.1" + +mpath@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.9.0.tgz#0c122fe107846e31fc58c75b09c35514b3871904" + integrity sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew== + +mquery@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/mquery/-/mquery-5.0.0.tgz#a95be5dfc610b23862df34a47d3e5d60e110695d" + integrity sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg== + dependencies: + debug "4.x" + +ms@2.1.3, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + 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" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +punycode@^2.1.0, punycode@^2.1.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +sift@16.0.1: + version "16.0.1" + resolved "https://registry.yarnpkg.com/sift/-/sift-16.0.1.tgz#e9c2ccc72191585008cf3e36fc447b2d2633a053" + integrity sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ== + +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks@^2.7.1: + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== + dependencies: + ip-address "^9.0.5" + smart-buffer "^4.2.0" + +sparse-bitfield@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" + integrity sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ== + dependencies: + memory-pager "^1.0.2" + +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +tr46@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" + integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== + dependencies: + punycode "^2.1.1" + +tslib@^2.6.3: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +undici-types@~6.19.8: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +validator@~13.12.0: + version "13.12.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.12.0.tgz#7d78e76ba85504da3fee4fd1922b385914d4b35f" + integrity sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg== + +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + +whatwg-url@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" + integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/chili-and-cilantro-node-lib/package.json b/chili-and-cilantro-node-lib/package.json index fc8ae1c..b76c199 100644 --- a/chili-and-cilantro-node-lib/package.json +++ b/chili-and-cilantro-node-lib/package.json @@ -2,10 +2,20 @@ "name": "@chili-and-cilantro/chili-and-cilantro-node-lib", "version": "0.0.1", "dependencies": { - "tslib": "^2.3.0" + "mongoose": "^7.6.2", + "tslib": "^2.3.0", + "validator": "^13.12.0", + "uuid": "^10.0.0", + "express": "4.21.1" + }, + "compilerOptions": { + "baseUrl": "." }, "type": "commonjs", "main": "./src/index.js", "typings": "./src/index.d.ts", - "private": true + "private": true, + "devDependencies": { + "eslint": "^9.15.0" + } } diff --git a/chili-and-cilantro-node-lib/src/index.ts b/chili-and-cilantro-node-lib/src/index.ts index 245ec07..847bf90 100644 --- a/chili-and-cilantro-node-lib/src/index.ts +++ b/chili-and-cilantro-node-lib/src/index.ts @@ -1,5 +1,8 @@ export * from './lib/action-schema-map'; export * from './lib/discriminators/action'; +export * from './lib/interfaces/application'; +export * from './lib/interfaces/schema-data'; +export * from './lib/interfaces/schema-model-data'; export * from './lib/models/action'; export * from './lib/models/chef'; export * from './lib/models/email-token'; @@ -25,3 +28,7 @@ export * from './lib/schemas/chef'; export * from './lib/schemas/email-token'; export * from './lib/schemas/game'; export * from './lib/schemas/user'; +export * from './lib/utils'; +export * from './types/shared-types'; + +/// diff --git a/chili-and-cilantro-node-lib/src/lib/action-schema-map.ts b/chili-and-cilantro-node-lib/src/lib/action-schema-map.ts index 9312761..dc819d5 100644 --- a/chili-and-cilantro-node-lib/src/lib/action-schema-map.ts +++ b/chili-and-cilantro-node-lib/src/lib/action-schema-map.ts @@ -1,5 +1,5 @@ import { ActionType } from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { Schema } from 'mongoose'; +import { ActionSchemaMapType } from '../types/shared-types'; import { CreateGameActionSchema } from './schemas/actions/create-game'; import { EndGameActionSchema } from './schemas/actions/end-game'; import { EndRoundActionSchema } from './schemas/actions/end-round'; @@ -15,7 +15,7 @@ import { StartBiddingActionSchema } from './schemas/actions/start-bidding'; import { StartGameActionSchema } from './schemas/actions/start-game'; import { StartNewRoundActionSchema } from './schemas/actions/start-new-round'; -export const ActionSchemas: Record = { +export const ActionSchemas: ActionSchemaMapType = { [ActionType.CREATE_GAME]: CreateGameActionSchema, [ActionType.END_GAME]: EndGameActionSchema, [ActionType.END_ROUND]: EndRoundActionSchema, diff --git a/chili-and-cilantro-node-lib/src/lib/discriminators/action.ts b/chili-and-cilantro-node-lib/src/lib/discriminators/action.ts index 4ae8cc3..87ab42b 100644 --- a/chili-and-cilantro-node-lib/src/lib/discriminators/action.ts +++ b/chili-and-cilantro-node-lib/src/lib/discriminators/action.ts @@ -1,35 +1,35 @@ import { - ActionDocumentTypes, - ActionDocumentTypesMap, ActionType, ActionTypeRecordMap, IActionDocument, } from '@chili-and-cilantro/chili-and-cilantro-lib'; -import mongoose, { Model, Schema } from 'mongoose'; +import mongoose, { Connection, Model, Schema } from 'mongoose'; import { ActionSchemas } from '../action-schema-map'; -const BaseSchema = new Schema( +const BaseActionSchema = new Schema( {}, - { discriminatorKey: 'kind', timestamps: true }, + { discriminatorKey: 'type', timestamps: true }, ); -const BaseModel = mongoose.model('Base', BaseSchema); +const BaseActionModel = (connection: Connection) => + connection.model('BaseAction', BaseActionSchema); function generateDiscriminators( - baseModel: mongoose.Model, + connection: Connection, actionEnum: Record, actionSchemas: Record, - actionDocumentTypeMap: ActionDocumentTypes, + // actionDocumentTypeMap: ActionDocumentTypes, ): { discriminatorRecords: Record>; discriminatorArray: Array>; } { + const baseModel = BaseActionModel(connection); const discriminatorRecords: Record> = {}; const discriminatorArray: Array> = []; Object.keys(actionEnum).forEach((actionKey) => { const action = actionEnum[actionKey as keyof typeof actionEnum]; const schema = actionSchemas[action]; - const type = actionDocumentTypeMap[action]; + //const type = actionDocumentTypeMap[action]; // Ensure the correct type is inferred here const discriminator = baseModel.discriminator(action, schema); @@ -40,15 +40,22 @@ function generateDiscriminators( return { discriminatorRecords, discriminatorArray }; } -const ActionDiscriminators = generateDiscriminators( - BaseModel, - ActionTypeRecordMap, - ActionSchemas, - ActionDocumentTypesMap, -); -const ActionDiscriminatorsByActionType: { [key in ActionType]: Model } = - ActionDiscriminators.discriminatorRecords as { +const ActionDiscriminators = (connection: Connection) => + generateDiscriminators( + connection, + ActionTypeRecordMap, + ActionSchemas, + // ActionDocumentTypesMap, + ); +const ActionDiscriminatorsByActionType = ( + connection: Connection, +): { [key in ActionType]: Model } => + ActionDiscriminators(connection).discriminatorRecords as { [key in ActionType]: Model; }; -export { ActionDiscriminators, ActionDiscriminatorsByActionType, BaseModel }; +export { + ActionDiscriminators, + ActionDiscriminatorsByActionType, + BaseActionModel, +}; diff --git a/chili-and-cilantro-node-lib/src/lib/errors/missing-validated-data.ts b/chili-and-cilantro-node-lib/src/lib/errors/missing-validated-data.ts new file mode 100644 index 0000000..0f9ffb6 --- /dev/null +++ b/chili-and-cilantro-node-lib/src/lib/errors/missing-validated-data.ts @@ -0,0 +1,14 @@ +export class MissingValidatedDataError extends Error { + public readonly data?: string | string[]; + constructor(data?: string | string[]) { + super( + data + ? Array.isArray(data) + ? `Missing validated data: ${data.join(', ')}` + : `Missing validated data: ${data}` + : 'Missing validated data', + ); + this.data = data; + this.name = 'MissingValidatedDataError'; + } +} diff --git a/chili-and-cilantro-node-lib/src/lib/interfaces/application.ts b/chili-and-cilantro-node-lib/src/lib/interfaces/application.ts new file mode 100644 index 0000000..3635f4c --- /dev/null +++ b/chili-and-cilantro-node-lib/src/lib/interfaces/application.ts @@ -0,0 +1,9 @@ +import { GetModelFunction } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import mongoose from 'mongoose'; + +export interface IApplication { + get db(): typeof mongoose; + get ready(): boolean; + start(): Promise; + getModel: GetModelFunction; +} diff --git a/chili-and-cilantro-node-lib/src/lib/interfaces/schema-data.ts b/chili-and-cilantro-node-lib/src/lib/interfaces/schema-data.ts new file mode 100644 index 0000000..4023e03 --- /dev/null +++ b/chili-and-cilantro-node-lib/src/lib/interfaces/schema-data.ts @@ -0,0 +1,15 @@ +import { + IBaseDocument, + ModelName, + ModelNameCollection, +} from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { Model, Schema } from 'mongoose'; + +export interface ISchemaData> { + name: ModelName; + description: string; + collection: ModelNameCollection; + schema: Schema; + path: string; + discriminators?: Array>; +} diff --git a/chili-and-cilantro-node-lib/src/lib/interfaces/schema-model-data.ts b/chili-and-cilantro-node-lib/src/lib/interfaces/schema-model-data.ts index 8b0788d..dc1cf80 100644 --- a/chili-and-cilantro-node-lib/src/lib/interfaces/schema-model-data.ts +++ b/chili-and-cilantro-node-lib/src/lib/interfaces/schema-model-data.ts @@ -1,16 +1,8 @@ -import { - IBaseDocument, - ModelName, - ModelNameCollection, -} from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { Model, Schema } from 'mongoose'; +import { IBaseDocument } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { Model } from 'mongoose'; +import { ISchemaData } from './schema-data'; -export interface ISchemaModelData> { - name: ModelName; - description: string; - collection: ModelNameCollection; +export interface ISchemaModelData> + extends ISchemaData { model: Model; - schema: Schema; - path: string; - discriminators?: Array>; } diff --git a/chili-and-cilantro-node-lib/src/lib/models/action.ts b/chili-and-cilantro-node-lib/src/lib/models/action.ts index 915e6fa..71c9c7c 100644 --- a/chili-and-cilantro-node-lib/src/lib/models/action.ts +++ b/chili-and-cilantro-node-lib/src/lib/models/action.ts @@ -1,16 +1,13 @@ -import { - IActionDocument, - ModelName, - ModelNameCollection, -} from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { model } from 'mongoose'; -import { ActionSchema } from '../schemas/action'; +import { IActionDocument } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { Connection, Model } from 'mongoose'; +import { Schema } from '../schema'; -export const ActionModel = model( - ModelName.Action, - ActionSchema, - ModelNameCollection.Action, -); +export const ActionModel = (connection: Connection): Model => + connection.model( + Schema.Action.name, + Schema.Action.schema, + Schema.Action.collection, + ); // export const CreateGameDiscriminator = // ActionModel.discriminator( diff --git a/chili-and-cilantro-node-lib/src/lib/models/chef.ts b/chili-and-cilantro-node-lib/src/lib/models/chef.ts index 630f66c..35795b8 100644 --- a/chili-and-cilantro-node-lib/src/lib/models/chef.ts +++ b/chili-and-cilantro-node-lib/src/lib/models/chef.ts @@ -1,15 +1,12 @@ -import { - IChefDocument, - ModelName, - ModelNameCollection, -} from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { model } from 'mongoose'; -import { ChefSchema } from '../schemas/chef'; +import { IChefDocument } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { Connection, Model } from 'mongoose'; +import { Schema } from '../schema'; -export const ChefModel = model( - ModelName.Chef, - ChefSchema, - ModelNameCollection.Chef, -); +export const ChefModel = (connection: Connection): Model => + connection.model( + Schema.Chef.name, + Schema.Chef.schema, + Schema.Chef.collection, + ); export default ChefModel; diff --git a/chili-and-cilantro-node-lib/src/lib/models/email-token.ts b/chili-and-cilantro-node-lib/src/lib/models/email-token.ts index ac25072..81ad5b9 100644 --- a/chili-and-cilantro-node-lib/src/lib/models/email-token.ts +++ b/chili-and-cilantro-node-lib/src/lib/models/email-token.ts @@ -1,9 +1,12 @@ import { IEmailTokenDocument } from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { model } from 'mongoose'; +import { Connection, Model } from 'mongoose'; import { Schema } from '../schema'; -export const EmailTokenModel = model( - Schema.EmailToken.name, - Schema.EmailToken.schema, - Schema.EmailToken.collection, -); +export const EmailTokenModel = ( + connection: Connection, +): Model => + connection.model( + Schema.EmailToken.name, + Schema.EmailToken.schema, + Schema.EmailToken.collection, + ); diff --git a/chili-and-cilantro-node-lib/src/lib/models/game.ts b/chili-and-cilantro-node-lib/src/lib/models/game.ts index bcb8acc..bbbb36e 100644 --- a/chili-and-cilantro-node-lib/src/lib/models/game.ts +++ b/chili-and-cilantro-node-lib/src/lib/models/game.ts @@ -1,15 +1,12 @@ -import { - IGameDocument, - ModelName, - ModelNameCollection, -} from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { model } from 'mongoose'; -import { GameSchema } from '../schemas/game'; +import { IGameDocument } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { Connection, Model } from 'mongoose'; +import { Schema } from '../schema'; -export const GameModel = model( - ModelName.Game, - GameSchema, - ModelNameCollection.Game, -); +export const GameModel = (connection: Connection): Model => + connection.model( + Schema.Game.name, + Schema.Game.schema, + Schema.Game.collection, + ); export default GameModel; diff --git a/chili-and-cilantro-node-lib/src/lib/models/user.ts b/chili-and-cilantro-node-lib/src/lib/models/user.ts index 75abf7d..a552b43 100644 --- a/chili-and-cilantro-node-lib/src/lib/models/user.ts +++ b/chili-and-cilantro-node-lib/src/lib/models/user.ts @@ -3,13 +3,14 @@ import { ModelName, ModelNameCollection, } from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { model } from 'mongoose'; +import { Connection, Model } from 'mongoose'; import { UserSchema } from '../schemas/user'; -export const UserModel = model( - ModelName.User, - UserSchema, - ModelNameCollection.User, -); +export const UserModel = (connection: Connection): Model => + connection.model( + ModelName.User, + UserSchema, + ModelNameCollection.User, + ); export default UserModel; diff --git a/chili-and-cilantro-node-lib/src/lib/schema.ts b/chili-and-cilantro-node-lib/src/lib/schema.ts index 4b8144c..4ccc0d7 100644 --- a/chili-and-cilantro-node-lib/src/lib/schema.ts +++ b/chili-and-cilantro-node-lib/src/lib/schema.ts @@ -1,14 +1,23 @@ import { + IAction, + IActionDocument, + IBaseDocument, + IChef, + IChefDocument, + IEmailToken, + IEmailTokenDocument, + IGame, + IGameDocument, + IUser, + IUserDocument, ModelName, ModelNameCollection, } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { Connection, Schema as MongooseSchema } from 'mongoose'; +import { SchemaMap } from '../types/shared-types'; import { ActionDiscriminators } from './discriminators/action'; +import { ISchemaData } from './interfaces/schema-data'; import { ISchemaModelData } from './interfaces/schema-model-data'; -import { ActionModel } from './models/action'; -import { ChefModel } from './models/chef'; -import { EmailTokenModel } from './models/email-token'; -import { GameModel } from './models/game'; -import { UserModel } from './models/user'; import { ActionSchema } from './schemas/action'; import { ChefSchema } from './schemas/chef'; import { EmailTokenSchema } from './schemas/email-token'; @@ -21,25 +30,18 @@ function modelNameCollectionToPath( return `/${modelNameCollection as string}`; } -/** - * The schema for all models in the system. - * This includes the name, description, plural name, and api name of each model. - */ -export const Schema: Record> = { +export const Schema: { [key in ModelName]: ISchemaData> } = { [ModelName.Action]: { name: ModelName.Action, description: 'An action taken by a chef in a game.', collection: ModelNameCollection.Action, - model: ActionModel, schema: ActionSchema, path: modelNameCollectionToPath(ModelNameCollection.Action), - discriminators: ActionDiscriminators.discriminatorArray, }, [ModelName.Chef]: { name: ModelName.Chef, description: 'A chef in a game.', collection: ModelNameCollection.Chef, - model: ChefModel, schema: ChefSchema, path: modelNameCollectionToPath(ModelNameCollection.Chef), }, @@ -47,7 +49,6 @@ export const Schema: Record> = { name: ModelName.EmailToken, description: 'An email token for email verification or password reset', collection: ModelNameCollection.EmailToken, - model: EmailTokenModel, schema: EmailTokenSchema, path: modelNameCollectionToPath(ModelNameCollection.EmailToken), }, @@ -55,7 +56,6 @@ export const Schema: Record> = { name: ModelName.Game, description: 'A game in the system.', collection: ModelNameCollection.Game, - model: GameModel, schema: GameSchema, path: modelNameCollectionToPath(ModelNameCollection.Game), }, @@ -63,8 +63,57 @@ export const Schema: Record> = { name: ModelName.User, description: 'A user in the system.', collection: ModelNameCollection.User, - model: UserModel, schema: UserSchema, path: modelNameCollectionToPath(ModelNameCollection.User), }, }; + +export function getSchemaModel, D>( + modelName: ModelName, + connection: Connection, +) { + const value = Schema[modelName]; + return connection.model( + modelName, + value.schema as MongooseSchema, + value.collection, + ); +} + +/** + * The schema for all models in the system. + * This includes the name, description, plural name, and api name of each model. + */ +export function getSchemaMap(connection: Connection): SchemaMap { + const { discriminatorArray: actionDiscriminatorArray } = + ActionDiscriminators(connection); + return { + [ModelName.Action]: { + ...Schema[ModelName.Action], + model: getSchemaModel( + ModelName.Action, + connection, + ), + discriminators: actionDiscriminatorArray, + } as ISchemaModelData, + [ModelName.Chef]: { + ...Schema[ModelName.Chef], + model: getSchemaModel(ModelName.Chef, connection), + } as ISchemaModelData, + [ModelName.EmailToken]: { + ...Schema[ModelName.EmailToken], + model: getSchemaModel( + ModelName.EmailToken, + connection, + ), + } as ISchemaModelData, + [ModelName.Game]: { + ...Schema[ModelName.Game], + model: getSchemaModel(ModelName.Game, connection), + } as ISchemaModelData, + [ModelName.User]: { + ...Schema[ModelName.User], + model: getSchemaModel(ModelName.User, connection), + } as ISchemaModelData, + }; +} diff --git a/chili-and-cilantro-node-lib/src/lib/schemas/action.ts b/chili-and-cilantro-node-lib/src/lib/schemas/action.ts index 373926e..8e892af 100644 --- a/chili-and-cilantro-node-lib/src/lib/schemas/action.ts +++ b/chili-and-cilantro-node-lib/src/lib/schemas/action.ts @@ -1,30 +1,32 @@ import { - ActionType, IActionDocument, ModelName, } from '@chili-and-cilantro/chili-and-cilantro-lib'; import { Schema } from 'mongoose'; +export const ActionSchemaBase = { + gameId: { + type: Schema.Types.ObjectId, + required: true, + ref: ModelName.Game, + }, + chefId: { + type: Schema.Types.ObjectId, + required: true, + ref: ModelName.Chef, + }, + userId: { + type: Schema.Types.ObjectId, + required: true, + ref: ModelName.User, + }, + round: { type: Number, required: true }, +}; + export const ActionSchema = new Schema( { - gameId: { - type: Schema.Types.ObjectId, - required: true, - ref: ModelName.Game, - }, - chefId: { - type: Schema.Types.ObjectId, - required: true, - ref: ModelName.Chef, - }, - userId: { - type: Schema.Types.ObjectId, - required: true, - ref: ModelName.User, - }, - type: { type: String, enum: Object.values(ActionType), required: true }, - details: { type: Object, required: true }, - round: { type: Number, required: true }, + ...ActionSchemaBase, + details: { type: Schema.Types.Mixed, required: true }, }, { timestamps: true, diff --git a/chili-and-cilantro-node-lib/src/lib/schemas/actions/create-game.ts b/chili-and-cilantro-node-lib/src/lib/schemas/actions/create-game.ts index 105777d..b21ad59 100644 --- a/chili-and-cilantro-node-lib/src/lib/schemas/actions/create-game.ts +++ b/chili-and-cilantro-node-lib/src/lib/schemas/actions/create-game.ts @@ -3,6 +3,7 @@ import { ICreateGameDetails, } from '@chili-and-cilantro/chili-and-cilantro-lib'; import { Schema } from 'mongoose'; +import { ActionSchemaBase } from '../action'; export const CreateGameDetailsSchema = new Schema( {}, @@ -10,5 +11,6 @@ export const CreateGameDetailsSchema = new Schema( ); export const CreateGameActionSchema = new Schema({ + ...ActionSchemaBase, details: { type: CreateGameDetailsSchema, required: true }, }); diff --git a/chili-and-cilantro-node-lib/src/lib/schemas/actions/end-game.ts b/chili-and-cilantro-node-lib/src/lib/schemas/actions/end-game.ts index c6ea624..8c45d47 100644 --- a/chili-and-cilantro-node-lib/src/lib/schemas/actions/end-game.ts +++ b/chili-and-cilantro-node-lib/src/lib/schemas/actions/end-game.ts @@ -3,6 +3,7 @@ import { IEndGameDetails, } from '@chili-and-cilantro/chili-and-cilantro-lib'; import { Schema } from 'mongoose'; +import { ActionSchemaBase } from '../action'; export const EndGameDetailsSchema = new Schema( {}, @@ -10,5 +11,6 @@ export const EndGameDetailsSchema = new Schema( ); export const EndGameActionSchema = new Schema({ + ...ActionSchemaBase, details: { type: EndGameDetailsSchema, required: true }, }); diff --git a/chili-and-cilantro-node-lib/src/lib/schemas/actions/end-round.ts b/chili-and-cilantro-node-lib/src/lib/schemas/actions/end-round.ts index 05e030e..47dc6a3 100644 --- a/chili-and-cilantro-node-lib/src/lib/schemas/actions/end-round.ts +++ b/chili-and-cilantro-node-lib/src/lib/schemas/actions/end-round.ts @@ -3,6 +3,7 @@ import { IEndRoundDetails, } from '@chili-and-cilantro/chili-and-cilantro-lib'; import { Schema } from 'mongoose'; +import { ActionSchemaBase } from '../action'; export const EndRoundDetailsSchema = new Schema( {}, @@ -10,5 +11,6 @@ export const EndRoundDetailsSchema = new Schema( ); export const EndRoundActionSchema = new Schema({ + ...ActionSchemaBase, details: { type: EndRoundDetailsSchema, required: true }, }); diff --git a/chili-and-cilantro-node-lib/src/lib/schemas/actions/expire-game.ts b/chili-and-cilantro-node-lib/src/lib/schemas/actions/expire-game.ts index 0a21708..c737003 100644 --- a/chili-and-cilantro-node-lib/src/lib/schemas/actions/expire-game.ts +++ b/chili-and-cilantro-node-lib/src/lib/schemas/actions/expire-game.ts @@ -3,6 +3,7 @@ import { IExpireGameDetails, } from '@chili-and-cilantro/chili-and-cilantro-lib'; import { Schema } from 'mongoose'; +import { ActionSchemaBase } from '../action'; export const ExpireGameDetailsSchema = new Schema( {}, @@ -10,5 +11,6 @@ export const ExpireGameDetailsSchema = new Schema( ); export const ExpireGameActionSchema = new Schema({ + ...ActionSchemaBase, details: { type: ExpireGameDetailsSchema, required: true }, }); diff --git a/chili-and-cilantro-node-lib/src/lib/schemas/actions/flip-card.ts b/chili-and-cilantro-node-lib/src/lib/schemas/actions/flip-card.ts index b49e7fe..a539f68 100644 --- a/chili-and-cilantro-node-lib/src/lib/schemas/actions/flip-card.ts +++ b/chili-and-cilantro-node-lib/src/lib/schemas/actions/flip-card.ts @@ -4,6 +4,7 @@ import { ModelName, } from '@chili-and-cilantro/chili-and-cilantro-lib'; import { Schema } from 'mongoose'; +import { ActionSchemaBase } from '../action'; export const FlipCardDetailsSchema = new Schema( { @@ -15,5 +16,6 @@ export const FlipCardDetailsSchema = new Schema( ); export const FlipCardActionSchema = new Schema({ + ...ActionSchemaBase, details: { type: FlipCardDetailsSchema, required: true }, }); diff --git a/chili-and-cilantro-node-lib/src/lib/schemas/actions/join-game.ts b/chili-and-cilantro-node-lib/src/lib/schemas/actions/join-game.ts index 3d6715d..cc8a2dc 100644 --- a/chili-and-cilantro-node-lib/src/lib/schemas/actions/join-game.ts +++ b/chili-and-cilantro-node-lib/src/lib/schemas/actions/join-game.ts @@ -3,6 +3,7 @@ import { IJoinGameDetails, } from '@chili-and-cilantro/chili-and-cilantro-lib'; import { Schema } from 'mongoose'; +import { ActionSchemaBase } from '../action'; export const JoinGameDetailsSchema = new Schema( {}, @@ -10,5 +11,6 @@ export const JoinGameDetailsSchema = new Schema( ); export const JoinGameActionSchema = new Schema({ + ...ActionSchemaBase, details: { type: JoinGameDetailsSchema, required: true }, }); diff --git a/chili-and-cilantro-node-lib/src/lib/schemas/actions/make-bid.ts b/chili-and-cilantro-node-lib/src/lib/schemas/actions/make-bid.ts index cc4c7ee..5b8bef5 100644 --- a/chili-and-cilantro-node-lib/src/lib/schemas/actions/make-bid.ts +++ b/chili-and-cilantro-node-lib/src/lib/schemas/actions/make-bid.ts @@ -3,6 +3,7 @@ import { IMakeBidDetails, } from '@chili-and-cilantro/chili-and-cilantro-lib'; import { Schema } from 'mongoose'; +import { ActionSchemaBase } from '../action'; export const MakeBidDetailsSchema = new Schema( { @@ -12,5 +13,6 @@ export const MakeBidDetailsSchema = new Schema( ); export const MakeBidActionSchema = new Schema({ + ...ActionSchemaBase, details: { type: MakeBidDetailsSchema, required: true }, }); diff --git a/chili-and-cilantro-node-lib/src/lib/schemas/actions/message.ts b/chili-and-cilantro-node-lib/src/lib/schemas/actions/message.ts index dc7eb1d..e1fe857 100644 --- a/chili-and-cilantro-node-lib/src/lib/schemas/actions/message.ts +++ b/chili-and-cilantro-node-lib/src/lib/schemas/actions/message.ts @@ -3,6 +3,7 @@ import { IMessageDetails, } from '@chili-and-cilantro/chili-and-cilantro-lib'; import { Schema } from 'mongoose'; +import { ActionSchemaBase } from '../action'; export const MessageDetailsSchema = new Schema( { @@ -12,5 +13,6 @@ export const MessageDetailsSchema = new Schema( ); export const MessageActionSchema = new Schema({ + ...ActionSchemaBase, details: { type: MessageDetailsSchema, required: true }, }); diff --git a/chili-and-cilantro-node-lib/src/lib/schemas/actions/pass.ts b/chili-and-cilantro-node-lib/src/lib/schemas/actions/pass.ts index 577ffa8..5cac894 100644 --- a/chili-and-cilantro-node-lib/src/lib/schemas/actions/pass.ts +++ b/chili-and-cilantro-node-lib/src/lib/schemas/actions/pass.ts @@ -3,9 +3,11 @@ import { IPassDetails, } from '@chili-and-cilantro/chili-and-cilantro-lib'; import { Schema } from 'mongoose'; +import { ActionSchemaBase } from '../action'; export const PassDetailsSchema = new Schema({}, { _id: false }); export const PassActionSchema = new Schema({ + ...ActionSchemaBase, details: { type: PassDetailsSchema, required: true }, }); diff --git a/chili-and-cilantro-node-lib/src/lib/schemas/actions/place-card.ts b/chili-and-cilantro-node-lib/src/lib/schemas/actions/place-card.ts index 5f4690a..ae9003f 100644 --- a/chili-and-cilantro-node-lib/src/lib/schemas/actions/place-card.ts +++ b/chili-and-cilantro-node-lib/src/lib/schemas/actions/place-card.ts @@ -4,6 +4,7 @@ import { IPlaceCardDetails, } from '@chili-and-cilantro/chili-and-cilantro-lib'; import { Schema } from 'mongoose'; +import { ActionSchemaBase } from '../action'; export const PlaceCardDetailsSchema = new Schema( { @@ -14,5 +15,6 @@ export const PlaceCardDetailsSchema = new Schema( ); export const PlaceCardActionSchema = new Schema({ + ...ActionSchemaBase, details: { type: PlaceCardDetailsSchema, required: true }, }); diff --git a/chili-and-cilantro-node-lib/src/lib/schemas/actions/quit-game.ts b/chili-and-cilantro-node-lib/src/lib/schemas/actions/quit-game.ts index c3b332d..827c488 100644 --- a/chili-and-cilantro-node-lib/src/lib/schemas/actions/quit-game.ts +++ b/chili-and-cilantro-node-lib/src/lib/schemas/actions/quit-game.ts @@ -4,6 +4,7 @@ import { QuitGameReason, } from '@chili-and-cilantro/chili-and-cilantro-lib'; import { Schema } from 'mongoose'; +import { ActionSchemaBase } from '../action'; export const QuitGameDetailsSchema = new Schema( { @@ -17,5 +18,6 @@ export const QuitGameDetailsSchema = new Schema( ); export const QuitGameActionSchema = new Schema({ + ...ActionSchemaBase, details: { type: QuitGameDetailsSchema, required: true }, }); diff --git a/chili-and-cilantro-node-lib/src/lib/schemas/actions/start-bidding.ts b/chili-and-cilantro-node-lib/src/lib/schemas/actions/start-bidding.ts index af56ba3..71a9157 100644 --- a/chili-and-cilantro-node-lib/src/lib/schemas/actions/start-bidding.ts +++ b/chili-and-cilantro-node-lib/src/lib/schemas/actions/start-bidding.ts @@ -3,6 +3,7 @@ import { IStartBiddingDetails, } from '@chili-and-cilantro/chili-and-cilantro-lib'; import { Schema } from 'mongoose'; +import { ActionSchemaBase } from '../action'; export const StartBiddingDetailsSchema = new Schema( { @@ -13,6 +14,7 @@ export const StartBiddingDetailsSchema = new Schema( export const StartBiddingActionSchema = new Schema( { + ...ActionSchemaBase, details: { type: StartBiddingDetailsSchema, required: true }, }, ); diff --git a/chili-and-cilantro-node-lib/src/lib/schemas/actions/start-game.ts b/chili-and-cilantro-node-lib/src/lib/schemas/actions/start-game.ts index c6e0826..9f26211 100644 --- a/chili-and-cilantro-node-lib/src/lib/schemas/actions/start-game.ts +++ b/chili-and-cilantro-node-lib/src/lib/schemas/actions/start-game.ts @@ -3,6 +3,7 @@ import { IStartGameDetails, } from '@chili-and-cilantro/chili-and-cilantro-lib'; import { Schema } from 'mongoose'; +import { ActionSchemaBase } from '../action'; export const StartGameDetailsSchema = new Schema( {}, @@ -10,5 +11,6 @@ export const StartGameDetailsSchema = new Schema( ); export const StartGameActionSchema = new Schema({ + ...ActionSchemaBase, details: { type: StartGameDetailsSchema, required: true }, }); diff --git a/chili-and-cilantro-node-lib/src/lib/schemas/actions/start-new-round.ts b/chili-and-cilantro-node-lib/src/lib/schemas/actions/start-new-round.ts index 61b7273..4b2806b 100644 --- a/chili-and-cilantro-node-lib/src/lib/schemas/actions/start-new-round.ts +++ b/chili-and-cilantro-node-lib/src/lib/schemas/actions/start-new-round.ts @@ -3,6 +3,7 @@ import { IStartNewRoundDetails, } from '@chili-and-cilantro/chili-and-cilantro-lib'; import { Schema } from 'mongoose'; +import { ActionSchemaBase } from '../action'; export const StartNewRoundDetailsSchema = new Schema( {}, @@ -11,5 +12,6 @@ export const StartNewRoundDetailsSchema = new Schema( export const StartNewRoundActionSchema = new Schema({ + ...ActionSchemaBase, details: { type: StartNewRoundDetailsSchema, required: true }, }); diff --git a/chili-and-cilantro-node-lib/src/lib/schemas/chef.ts b/chili-and-cilantro-node-lib/src/lib/schemas/chef.ts index 3c5844f..517f947 100644 --- a/chili-and-cilantro-node-lib/src/lib/schemas/chef.ts +++ b/chili-and-cilantro-node-lib/src/lib/schemas/chef.ts @@ -6,7 +6,7 @@ import { IChefDocument, ModelName, } from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { Schema } from 'mongoose'; +import { Schema, ValidatorProps } from 'mongoose'; import validator from 'validator'; export const CardSchema = new Schema( @@ -36,7 +36,8 @@ export const ChefSchema = new Schema( v.length <= constants.MAX_USER_DISPLAY_NAME_LENGTH ); }, - message: (props) => `${props.value} is not a valid chef name!`, + message: (props: ValidatorProps) => + `${props.value} is not a valid chef name!`, }, set: (v: string) => (v || '').trim(), }, diff --git a/chili-and-cilantro-node-lib/src/lib/schemas/email-token.ts b/chili-and-cilantro-node-lib/src/lib/schemas/email-token.ts index 0f3c3a8..8ed3ba5 100644 --- a/chili-and-cilantro-node-lib/src/lib/schemas/email-token.ts +++ b/chili-and-cilantro-node-lib/src/lib/schemas/email-token.ts @@ -3,7 +3,7 @@ import { IEmailTokenDocument, ModelName, } from '@chili-and-cilantro/chili-and-cilantro-lib'; -import { Schema } from 'mongoose'; +import { Schema, ValidatorProps } from 'mongoose'; import { v4 as uuidv4 } from 'uuid'; import validator from 'validator'; @@ -53,7 +53,8 @@ export const EmailTokenSchema = new Schema({ immutable: true, validate: { validator: (v: string) => validator.isEmail(v), - message: (props) => `${props.value} is not a valid email address!`, + message: (props: ValidatorProps) => + `${props.value} is not a valid email address!`, }, }, /** diff --git a/chili-and-cilantro-node-lib/src/lib/schemas/game.ts b/chili-and-cilantro-node-lib/src/lib/schemas/game.ts index bed27ec..6a3e6e8 100644 --- a/chili-and-cilantro-node-lib/src/lib/schemas/game.ts +++ b/chili-and-cilantro-node-lib/src/lib/schemas/game.ts @@ -6,7 +6,7 @@ import { IRoundBids, ModelName, } from '@chili-and-cilantro/chili-and-cilantro-lib'; -import mongoose from 'mongoose'; +import mongoose, { ValidatorProps } from 'mongoose'; import validator from 'validator'; const { Schema } = mongoose; @@ -44,7 +44,8 @@ export const GameSchema = new Schema( v.length == constants.GAME_CODE_LENGTH ); }, - message: (props) => `${props.value} is not a valid game code!`, + message: (props: ValidatorProps) => + `${props.value} is not a valid game code!`, }, set: (v: string) => (v || '').trim().toUpperCase(), }, @@ -60,7 +61,8 @@ export const GameSchema = new Schema( v.length <= constants.MAX_GAME_NAME_LENGTH ); }, - message: (props) => `${props.value} is not a valid game name!`, + message: (props: ValidatorProps) => + `${props.value} is not a valid game name!`, }, set: (v: string) => (v || '').trim(), }, @@ -76,7 +78,8 @@ export const GameSchema = new Schema( v.length <= constants.MAX_GAME_PASSWORD_LENGTH ); }, - message: (props) => `${props.value} is not a valid password!`, + message: (props: ValidatorProps) => + `${props.value} is not a valid password!`, }, set: (v: string) => (v || '').trim(), }, @@ -101,7 +104,8 @@ export const GameSchema = new Schema( validator: function (v: number) { return v >= constants.MIN_CHEFS && v <= constants.MAX_CHEFS; }, - message: (props) => `${props.value} is not a valid number of chefs!`, + message: (props: ValidatorProps) => + `${props.value} is not a valid number of chefs!`, }, }, cardsPlaced: { diff --git a/chili-and-cilantro-node-lib/src/lib/schemas/user.ts b/chili-and-cilantro-node-lib/src/lib/schemas/user.ts index 80790c9..3f6242e 100644 --- a/chili-and-cilantro-node-lib/src/lib/schemas/user.ts +++ b/chili-and-cilantro-node-lib/src/lib/schemas/user.ts @@ -24,10 +24,7 @@ export const UserSchema = new Schema( trim: true, validate: { validator: async function (value: any) { - if (!validator.isEmail(value)) { - return false; - } - return true; + return validator.isEmail(value); }, }, }, diff --git a/chili-and-cilantro-node-lib/src/lib/utils.ts b/chili-and-cilantro-node-lib/src/lib/utils.ts new file mode 100644 index 0000000..a3e3545 --- /dev/null +++ b/chili-and-cilantro-node-lib/src/lib/utils.ts @@ -0,0 +1,127 @@ +import { TransactionCallback } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { Request, Response } from 'express'; +import { Types, startSession } from 'mongoose'; +import { MissingValidatedDataError } from './errors/missing-validated-data'; + +/** + * Checks if the given id is a valid string id + * @param id The id to check + * @returns True if the id is a valid string id + */ +export function isValidStringId(id: unknown): boolean { + return typeof id === 'string' && Types.ObjectId.isValid(id); +} + +/** + * Verifies the required fields were validated by express-validator and sends an error response if not or calls the callback if they are + * @param req The request object + * @param res The response object + * @param fields The fields to check + * @param callback The callback to call if the fields are valid + * @param errorCallback The function to call if a field is invalid + * @returns The result of the callback + */ +export async function requireValidatedFieldsAsync( + req: Request, + res: Response, + fields: string[], + callback: () => Promise, + errorCallback: (res: Response, field: string) => void, +): Promise { + return new Promise((resolve, reject) => { + if (req.validatedBody === undefined) { + reject(new MissingValidatedDataError()); + return; + } + + const validatedBody = req.validatedBody; + for (const field of fields) { + if (validatedBody[field] === undefined) { + errorCallback(res, field); + reject(new MissingValidatedDataError(field)); + return; + } + } + + // All fields are valid, call the callback + callback().then(resolve).catch(reject); + }); +} + +/** + * Verifies at least one of the required fields were validated by express-validator and sends an error response if not or calls the callback if they are + * @param req The request object + * @param res The response object + * @param fields The fields to check + * @param callback The callback to call if the fields are valid + * @param errorCallback The function to call if not at least one field is valid + * @returns The result of the callback + */ +export async function requireOneOfValidatedFieldsAsync>( + req: Request, + res: Response, + fields: string[], + callback: () => Promise, + errorCallback: (res: Response, fields: string[]) => void, +): Promise { + return new Promise((resolve, reject) => { + if (req.validatedBody === undefined) { + throw new Error('No validated body found on the request'); + } + const validatedBody = req.validatedBody; + // return 422 if none of the fields are valid + if (!fields.some((field) => validatedBody[field] !== undefined)) { + errorCallback(res, fields); + reject(new MissingValidatedDataError(fields)); + return; + } + // All fields are valid, call the callback + callback().then(resolve).catch(reject); + }); +} + +/** + * Verifies the required fields were validated by express-validator and throws an error if not or calls the callback if they are + * @param req The request object + * @param fields The fields to check + * @param callback The callback to call if the fields are valid + * @returns The result of the callback + */ +export function requireValidatedFieldsOrThrow( + req: Request, + fields: string[], + callback: () => T, +): T { + if (req.validatedBody === undefined) { + throw new Error('No validated body found on the request'); + } + const validatedBody = req.validatedBody; + fields.forEach((field) => { + if (validatedBody[field] === undefined) { + throw new MissingValidatedDataError(field); + } + }); + return callback(); +} + +export async function withTransaction( + useTransaction: boolean, + callback: TransactionCallback, + ...args: any +) { + if (!useTransaction) { + return callback(undefined, ...args); + } + const session = await startSession(); + try { + session.startTransaction(); + const result = await callback(session, ...args); + await session.commitTransaction(); + return result; + } catch (error) { + await session.abortTransaction(); + throw error; + } finally { + await session.endSession(); + } +} diff --git a/chili-and-cilantro-node-lib/src/types/shared-types.ts b/chili-and-cilantro-node-lib/src/types/shared-types.ts new file mode 100644 index 0000000..0c8cf93 --- /dev/null +++ b/chili-and-cilantro-node-lib/src/types/shared-types.ts @@ -0,0 +1,18 @@ +import { + ActionDocumentTypes, + ActionType, + ModelName, +} from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { Schema } from 'mongoose'; +import { ISchemaModelData } from '../lib/interfaces/schema-model-data'; + +/** + * Schema map interface + */ +export type SchemaMap = { + [key in ModelName]: ISchemaModelData; +}; + +export type ActionSchemaMapType = { + [K in ActionType]: Schema; +}; diff --git a/chili-and-cilantro-node-lib/src/types/types.d.ts b/chili-and-cilantro-node-lib/src/types/types.d.ts new file mode 100644 index 0000000..ad6b2f7 --- /dev/null +++ b/chili-and-cilantro-node-lib/src/types/types.d.ts @@ -0,0 +1,12 @@ +import { + IRequestUser, + ValidatedBody, +} from '@chili-and-cilantro/chili-and-cilantro-lib'; +declare global { + namespace Express { + interface Request { + user?: IRequestUser; + validatedBody?: ValidatedBody; + } + } +} diff --git a/chili-and-cilantro-node-lib/tsconfig.lib.json b/chili-and-cilantro-node-lib/tsconfig.lib.json index 6f3c503..a46e502 100644 --- a/chili-and-cilantro-node-lib/tsconfig.lib.json +++ b/chili-and-cilantro-node-lib/tsconfig.lib.json @@ -5,6 +5,6 @@ "declaration": true, "types": ["node"] }, - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "src/**/*.d.ts"], "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] } diff --git a/chili-and-cilantro-node-lib/yarn.lock b/chili-and-cilantro-node-lib/yarn.lock index 6131111..c7dd68c 100644 --- a/chili-and-cilantro-node-lib/yarn.lock +++ b/chili-and-cilantro-node-lib/yarn.lock @@ -2,7 +2,1193 @@ # yarn lockfile v1 +"@eslint-community/eslint-utils@^4.2.0": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz#d1145bf2c20132d6400495d6df4bf59362fd9d56" + integrity sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.12.1": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== + +"@eslint/config-array@^0.19.0": + version "0.19.0" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.19.0.tgz#3251a528998de914d59bb21ba4c11767cf1b3519" + integrity sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ== + dependencies: + "@eslint/object-schema" "^2.1.4" + debug "^4.3.1" + minimatch "^3.1.2" + +"@eslint/core@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.9.0.tgz#168ee076f94b152c01ca416c3e5cf82290ab4fcd" + integrity sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg== + +"@eslint/eslintrc@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.2.0.tgz#57470ac4e2e283a6bf76044d63281196e370542c" + integrity sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^10.0.1" + globals "^14.0.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@9.15.0": + version "9.15.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.15.0.tgz#df0e24fe869143b59731942128c19938fdbadfb5" + integrity sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg== + +"@eslint/object-schema@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.4.tgz#9e69f8bb4031e11df79e03db09f9dbbae1740843" + integrity sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ== + +"@eslint/plugin-kit@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz#812980a6a41ecf3a8341719f92a6d1e784a2e0e8" + integrity sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA== + dependencies: + levn "^0.4.1" + +"@humanfs/core@^0.19.1": + version "0.19.1" + resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" + integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== + +"@humanfs/node@^0.16.6": + version "0.16.6" + resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.6.tgz#ee2a10eaabd1131987bf0488fd9b820174cd765e" + integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw== + dependencies: + "@humanfs/core" "^0.19.1" + "@humanwhocodes/retry" "^0.3.0" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/retry@^0.3.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a" + integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA== + +"@humanwhocodes/retry@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.1.tgz#9a96ce501bc62df46c4031fbd970e3cc6b10f07b" + integrity sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA== + +"@mongodb-js/saslprep@^1.1.0": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz#e974bab8eca9faa88677d4ea4da8d09a52069004" + integrity sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw== + dependencies: + sparse-bitfield "^3.0.3" + +"@types/estree@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/node@*": + version "22.10.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.0.tgz#89bfc9e82496b9c7edea3382583fa94f75896e81" + integrity sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA== + dependencies: + undici-types "~6.20.0" + +"@types/webidl-conversions@*": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz#1306dbfa53768bcbcfc95a1c8cde367975581859" + integrity sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA== + +"@types/whatwg-url@^8.2.1": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-8.2.2.tgz#749d5b3873e845897ada99be4448041d4cc39e63" + integrity sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA== + dependencies: + "@types/node" "*" + "@types/webidl-conversions" "*" + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.14.0: + version "8.14.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" + integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== + +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + 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" + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +bson@^5.5.0: + version "5.5.1" + resolved "https://registry.yarnpkg.com/bson/-/bson-5.5.1.tgz#f5849d405711a7f23acdda9a442375df858e6833" + integrity sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== + +cross-spawn@^7.0.5: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4.x, debug@^4.3.1, debug@^4.3.2: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-scope@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.2.0.tgz#377aa6f1cb5dc7592cfd0b7f892fd0cf352ce442" + integrity sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint-visitor-keys@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" + integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== + +eslint@^9.15.0: + version "9.15.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.15.0.tgz#77c684a4e980e82135ebff8ee8f0a9106ce6b8a6" + integrity sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.12.1" + "@eslint/config-array" "^0.19.0" + "@eslint/core" "^0.9.0" + "@eslint/eslintrc" "^3.2.0" + "@eslint/js" "9.15.0" + "@eslint/plugin-kit" "^0.2.3" + "@humanfs/node" "^0.16.6" + "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.4.1" + "@types/estree" "^1.0.6" + "@types/json-schema" "^7.0.15" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.5" + debug "^4.3.2" + escape-string-regexp "^4.0.0" + eslint-scope "^8.2.0" + eslint-visitor-keys "^4.2.0" + espree "^10.3.0" + esquery "^1.5.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^8.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + json-stable-stringify-without-jsonify "^1.0.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + +espree@^10.0.1, espree@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a" + integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg== + dependencies: + acorn "^8.14.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.2.0" + +esquery@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +express@4.21.1: + version "4.21.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" + integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.7.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.3.1" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.10" + proxy-addr "~2.0.7" + qs "6.13.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.19.0" + serve-static "1.16.2" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== + dependencies: + flat-cache "^4.0.0" + +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.4" + +flatted@^3.2.9: + version "3.3.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.2.tgz#adba1448a9841bec72b42c532ea23dbbedef1a27" + integrity sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.0, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +kareem@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.5.1.tgz#7b8203e11819a8e77a34b3517d3ead206764d15d" + integrity sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA== + +keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memory-pager@^1.0.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" + integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== + +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +mongodb-connection-string-url@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz#57901bf352372abdde812c81be47b75c6b2ec5cf" + integrity sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ== + dependencies: + "@types/whatwg-url" "^8.2.1" + whatwg-url "^11.0.0" + +mongodb@5.9.2: + version "5.9.2" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-5.9.2.tgz#39a73b9fbc87ac9d9c1aaf8aab5c5bb69e2b913e" + integrity sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ== + dependencies: + bson "^5.5.0" + mongodb-connection-string-url "^2.6.0" + socks "^2.7.1" + optionalDependencies: + "@mongodb-js/saslprep" "^1.1.0" + +mongoose@^7.6.2: + version "7.8.2" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-7.8.2.tgz#578e4d0b0b60421459399cfc47cab2a43d90155f" + integrity sha512-/KDcZL84gg8hnmOHRRPK49WtxH3Xsph38c7YqvYPdxEB2OsDAXvwAknGxyEC0F2P3RJCqFOp+523iFCa0p3dfw== + dependencies: + bson "^5.5.0" + kareem "2.5.1" + mongodb "5.9.2" + mpath "0.9.0" + mquery "5.0.0" + ms "2.1.3" + sift "16.0.1" + +mpath@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.9.0.tgz#0c122fe107846e31fc58c75b09c35514b3871904" + integrity sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew== + +mquery@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/mquery/-/mquery-5.0.0.tgz#a95be5dfc610b23862df34a47d3e5d60e110695d" + integrity sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg== + dependencies: + debug "4.x" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +object-inspect@^1.13.1: + version "1.13.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" + integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + 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" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-to-regexp@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +punycode@^2.1.0, punycode@^2.1.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.19.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +sift@16.0.1: + version "16.0.1" + resolved "https://registry.yarnpkg.com/sift/-/sift-16.0.1.tgz#e9c2ccc72191585008cf3e36fc447b2d2633a053" + integrity sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ== + +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks@^2.7.1: + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== + dependencies: + ip-address "^9.0.5" + smart-buffer "^4.2.0" + +sparse-bitfield@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" + integrity sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ== + dependencies: + memory-pager "^1.0.2" + +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tr46@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" + integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== + dependencies: + punycode "^2.1.1" + tslib@^2.3.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" - integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" + integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== + +validator@^13.12.0: + version "13.12.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.12.0.tgz#7d78e76ba85504da3fee4fd1922b385914d4b35f" + integrity sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + +whatwg-url@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" + integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/chili-and-cilantro-react/.eslintrc.json b/chili-and-cilantro-react/.eslintrc.json index c5bdb41..4b3bd2b 100644 --- a/chili-and-cilantro-react/.eslintrc.json +++ b/chili-and-cilantro-react/.eslintrc.json @@ -1,5 +1,10 @@ { - "extends": ["plugin:@nx/react", "../.eslintrc.json"], + "extends": [ + "plugin:@nx/react", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "../.eslintrc.json" + ], "ignorePatterns": ["!**/*"], "overrides": [ { @@ -8,11 +13,26 @@ }, { "files": ["*.ts", "*.tsx"], + "extends": ["plugin:@typescript-eslint/recommended"], + "parserOptions": { + "project": [ + "chili-and-cilantro-react/tsconfig.app.json", + "chili-and-cilantro-react/tsconfig.spec.json" + ] + }, "rules": {} }, { "files": ["*.js", "*.jsx"], "rules": {} } - ] + ], + "settings": { + "react": { + "version": "detect" + } + }, + "rules": { + "react/react-in-jsx-scope": "off" + } } diff --git a/chili-and-cilantro-react/project.json b/chili-and-cilantro-react/project.json index 2a90e0b..50f359f 100644 --- a/chili-and-cilantro-react/project.json +++ b/chili-and-cilantro-react/project.json @@ -21,7 +21,7 @@ "chili-and-cilantro-react/src/favicon.ico", "chili-and-cilantro-react/src/assets" ], - "styles": ["chili-and-cilantro-react/src/styles.css"], + "styles": ["chili-and-cilantro-react/src/styles.scss"], "scripts": [], "isolatedConfig": true, "webpackConfig": "chili-and-cilantro-react/webpack.config.js" diff --git a/chili-and-cilantro-react/src/app/app.tsx b/chili-and-cilantro-react/src/app/app.tsx index 70b15f3..5b438e2 100644 --- a/chili-and-cilantro-react/src/app/app.tsx +++ b/chili-and-cilantro-react/src/app/app.tsx @@ -1,101 +1,54 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { useAuth0 } from '@auth0/auth0-react'; -import ApiAccess from '../components/api-access'; -import { AuthenticationGuard } from '../components/authentication-required'; -import Callback from '../components/callback'; -import Game from '../components/game'; -import LoginLink from '../components/login-link'; -import LogoutLink from '../components/logout-link'; -import AccountError from '../pages/account-error'; -import Register from '../pages/register'; -import UserProfile from '../pages/user-profile'; +import { Route, Routes } from 'react-router-dom'; +import '../styles.scss'; +import ChangePasswordPage from './components/change-password-page'; +import DashboardPage from './components/dashboard-page'; +import ForgotPasswordPage from './components/forgot-password-page'; +import Game from './components/game'; +import LoginPage from './components/login-page'; +import PrivateRoute from './components/private-route'; +import RegisterPage from './components/register-page'; +import SplashPage from './components/splash-page'; +import TopMenu from './components/top-menu'; +import VerifyEmailPage from './components/verify-email-page'; +import { MenuProvider } from './menu-context'; -import { Link, Route, Routes } from 'react-router-dom'; - -export function App() { - const { isAuthenticated } = useAuth0(); +function App() { return ( -
- {/* START: routes */} - {/* These routes and navigation have been generated for you */} - {/* Feel free to move and update them to fit your needs */} -
-
-
-
-
    -
  • - Home -
  • -
  • - Page 2 -
  • - {!isAuthenticated && ( -
  • - Register -
  • - )} - {!isAuthenticated && ( -
  • - -
  • - )} - {isAuthenticated && ( -
  • - Game -
  • - )} - {isAuthenticated && ( -
  • - Profile -
  • - )} - {isAuthenticated && ( -
  • - -
  • - )} -
-
- - - This is the generated root route.{' '} - Click here for page 2. -
- } - /> - } /> - } /> - - - - } - /> - - - - } - /> - - - - } - /> - } /> - - {/* END: routes */} +
+ + + + } /> + + + + } + /> + } /> + } /> + + + + } + /> + + + + } + /> + } /> + } /> + +
); } diff --git a/chili-and-cilantro-react/src/app/auth-provider.tsx b/chili-and-cilantro-react/src/app/auth-provider.tsx new file mode 100644 index 0000000..59f205c --- /dev/null +++ b/chili-and-cilantro-react/src/app/auth-provider.tsx @@ -0,0 +1,201 @@ +import { IRequestUser } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { isAxiosError } from 'axios'; +import { + createContext, + ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { useNavigate } from 'react-router-dom'; +import authService from './services/auth-service'; + +export interface AuthContextData { + user: IRequestUser | null; + isAuthenticated: boolean; + loading: boolean; + error: string | null; + login: ( + identifier: string, + password: string, + isEmail: boolean, + ) => Promise<{ token: string } | { error: string; status?: number }>; + logout: () => void; + changePassword: ( + currentPassword: string, + newPassword: string, + ) => Promise<{ success: boolean; message: string }>; + verifyToken: (token: string) => Promise; + checkAuth: () => void; + authState: number; +} + +export type AuthProviderProps = { + children: ReactNode; +}; + +export const AuthContext = createContext( + {} as AuthContextData, +); + +export const AuthProvider = ({ children }: AuthProviderProps) => { + const [user, setUser] = useState(null); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [authState, setAuthState] = useState(0); + const navigate = useNavigate(); + + const checkAuth = useCallback(async () => { + const token = localStorage.getItem('authToken'); + if (!token) { + setUser(null); + setIsAuthenticated(false); + setLoading(false); + return; + } + + try { + const userData: IRequestUser = await authService.verifyToken(token); + setUser(userData); + setIsAuthenticated(true); + } catch (error) { + console.error('Token verification failed:', error); + setUser(null); + setIsAuthenticated(false); + localStorage.removeItem('authToken'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + const token = localStorage.getItem('authToken'); + if (token) { + (async () => { + await checkAuth(); + })(); + } else { + setLoading(false); + } + }, [checkAuth, authState]); + + const login = useCallback( + async ( + identifier: string, + password: string, + isEmail: boolean, + ): Promise<{ token: string } | { error: string; status?: number }> => { + try { + setLoading(true); + const loginResult = await authService.login( + identifier, + password, + isEmail, + ); + // if loginResult is an object with an error, setError with it + if (typeof loginResult === 'object' && 'error' in loginResult) { + setError(loginResult.error); + } else if (typeof loginResult === 'object' && 'token' in loginResult) { + localStorage.setItem('authToken', loginResult.token); + setAuthState((prev) => prev + 1); + } + return loginResult; + } catch (error: unknown) { + if (error instanceof Error) { + setError(error.message); + setLoading(false); + return { error: error.message }; + } else { + setError('An unknown error occurred'); + setLoading(false); + return { error: 'An unknown error occurred' }; + } + } + }, + [], + ); + + const logout = useCallback(() => { + authService.logout(); + setUser(null); + setIsAuthenticated(false); + setError(null); + setAuthState((prev) => prev + 1); + navigate('/'); + }, [navigate]); + + const verifyToken = useCallback(async (token: string) => { + try { + await authService.verifyToken(token); + } catch (error) { + console.error('Token verification failed:', error); + setError('Invalid token'); + } + }, []); + + const changePassword = useCallback( + async ( + currentPassword: string, + newPassword: string, + ): Promise<{ success: boolean; message: string }> => { + try { + await authService.changePassword(currentPassword, newPassword); + // Handle success (e.g., show a message) + return { success: true, message: 'Password changed successfully' }; + } catch (error) { + // Handle error (e.g., set error state) + if (isAxiosError(error)) { + throw new Error( + error.response?.data?.message || + 'An error occurred while changing the password', + ); + } else if (error instanceof Error) { + throw error; + } else { + throw new Error( + 'An unexpected error occurred while changing the password', + ); + } + } + }, + [], + ); + + const contextValue = useMemo( + () => ({ + user, + isAuthenticated, + loading, + error, + changePassword, + login, + logout, + verifyToken, + checkAuth, + authState, + }), + [ + user, + isAuthenticated, + loading, + error, + changePassword, + login, + logout, + verifyToken, + checkAuth, + authState, + ], + ); + + return ( + {children} + ); +}; + +export const useAuth = () => { + return useContext(AuthContext); +}; diff --git a/chili-and-cilantro-react/src/components/api-access.tsx b/chili-and-cilantro-react/src/app/components/api-access.tsx similarity index 65% rename from chili-and-cilantro-react/src/components/api-access.tsx rename to chili-and-cilantro-react/src/app/components/api-access.tsx index 52da0f4..7cc851b 100644 --- a/chili-and-cilantro-react/src/components/api-access.tsx +++ b/chili-and-cilantro-react/src/app/components/api-access.tsx @@ -1,21 +1,19 @@ -import { useAuth0 } from '@auth0/auth0-react'; import { useEffect, useState } from 'react'; function ApiAccess() { - const { isLoading, error, getAccessTokenSilently } = useAuth0(); + const isLoading = false; const [token, setToken] = useState(null); useEffect(() => { if (!isLoading) { - getAccessTokenSilently() - .then((accessToken) => { - setToken(accessToken); - }) - .catch((err) => { - console.error('Error getting the access token:', err); - }); + const token = localStorage.getItem('authToken'); + if (token) { + setToken(token); + } else { + console.error('Error getting the access token'); + } } - }, [isLoading, getAccessTokenSilently]); + }, [isLoading]); const copyToClipboard = async () => { if (token) { @@ -32,9 +30,9 @@ function ApiAccess() { return
Loading...
; } - if (error) { - return
Error: {error.message}
; - } + // if (error !== null) { + // return
Error: {error.message}
; + // } return (
diff --git a/chili-and-cilantro-react/src/app/components/auth.scss b/chili-and-cilantro-react/src/app/components/auth.scss new file mode 100644 index 0000000..1d1666a --- /dev/null +++ b/chili-and-cilantro-react/src/app/components/auth.scss @@ -0,0 +1,74 @@ +@import '../../styles.scss'; + +// Auth container (for login and registration) +.auth-container { + max-width: 400px; + margin: 2rem auto; + padding: 2rem; + background-color: $light-color; + border-radius: 8px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + + .auth-title { + text-align: center; + margin-bottom: 2rem; + color: $primary-color; + } + + .auth-form { + display: flex; + flex-direction: column; + gap: 1rem; + + .error-message { + color: $danger-color; + margin-bottom: 1rem; + } + + .form-group { + position: relative; + + .error { + color: $danger-color; + font-size: 0.875rem; + margin-top: 0.25rem; + } + } + + .form-errors { + color: $danger-color; + margin-bottom: 1rem; + } + + button[type='submit'] { + @extend .btn; + @extend .btn-primary; + width: 100%; + } + + .toggle-login-type { + text-align: center; + margin-top: 1rem; + + a { + color: $primary-color; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } + + .resend-verification { + @extend .btn; + @extend .btn-primary; + margin-top: 1rem; + } + } + + .auth-links { + display: flex; + flex-direction: column; + } +} diff --git a/chili-and-cilantro-react/src/app/components/change-password-page.tsx b/chili-and-cilantro-react/src/app/components/change-password-page.tsx new file mode 100644 index 0000000..1f39af2 --- /dev/null +++ b/chili-and-cilantro-react/src/app/components/change-password-page.tsx @@ -0,0 +1,136 @@ +import { constants } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { useFormik } from 'formik'; +import React, { useContext, useState } from 'react'; +import { Navigate } from 'react-router-dom'; +import * as Yup from 'yup'; +import { AuthContext } from '../auth-provider'; +import './auth.scss'; + +const ChangePasswordPage: React.FC = () => { + const { isAuthenticated, user, loading, error, changePassword } = + useContext(AuthContext); + const [successMessage, setSuccessMessage] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + + const validationSchema = Yup.object({ + currentPassword: Yup.string() + .matches(constants.PASSWORD_REGEX, constants.PASSWORD_REGEX_ERROR) + .required('Current password is required'), + newPassword: Yup.string() + .matches(constants.PASSWORD_REGEX, constants.PASSWORD_REGEX_ERROR) + .notOneOf( + [Yup.ref('currentPassword')], + 'New password must be different from the current password', + ) + .required('New password is required'), + confirmNewPassword: Yup.string() + .oneOf([Yup.ref('newPassword')], 'Passwords must match') + .required('Please confirm your new password'), + }); + + const formik = useFormik({ + initialValues: { + currentPassword: '', + newPassword: '', + confirmNewPassword: '', + }, + validationSchema, + onSubmit: async (values, { setSubmitting, resetForm }) => { + try { + const result = await changePassword( + values.currentPassword, + values.newPassword, + ); + if (result.success) { + setSuccessMessage(result.message); + setErrorMessage(null); + resetForm(); + } + } catch (err) { + console.error('Error changing password:', err); + if (err instanceof Error) { + setErrorMessage(err.message); + } else { + setErrorMessage('An unexpected error occurred'); + } + setSuccessMessage(null); + } finally { + setSubmitting(false); + } + }, + }); + + if (loading) { + return
Loading...
; + } + + if (!isAuthenticated || !user) { + return ; + } + + return ( +
+

Change Password

+
+
+ + + {formik.touched.currentPassword && formik.errors.currentPassword ? ( +
{formik.errors.currentPassword}
+ ) : null} +
+ +
+ + + {formik.touched.newPassword && formik.errors.newPassword ? ( +
{formik.errors.newPassword}
+ ) : null} +
+ +
+ + + {formik.touched.confirmNewPassword && + formik.errors.confirmNewPassword ? ( +
{formik.errors.confirmNewPassword}
+ ) : null} +
+ + {(error || errorMessage) && ( +
{error || errorMessage}
+ )} + {successMessage && ( +
{successMessage}
+ )} + + +
+
+ ); +}; + +export default ChangePasswordPage; diff --git a/chili-and-cilantro-react/src/app/components/dashboard-page.scss b/chili-and-cilantro-react/src/app/components/dashboard-page.scss new file mode 100644 index 0000000..c1b713d --- /dev/null +++ b/chili-and-cilantro-react/src/app/components/dashboard-page.scss @@ -0,0 +1,52 @@ +@import '../../styles.scss'; + +// Dashboard styles +.dashboard-container { + max-width: 800px; + margin: 2rem auto; + padding: 2rem; + background-color: $light-color; + border-radius: 8px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +.dashboard-content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.dashboard-title { + text-align: center; + margin-bottom: 2rem; + color: $primary-color; +} + +.create-game-button { + align-self: center; + margin-top: 1rem; +} + +.game-list { + h2 { + color: $primary-color; + margin-bottom: 1rem; + } + + ul { + list-style-type: none; + padding: 0; + } + + li { + margin-bottom: 0.5rem; + } + + a { + color: $text-color; + text-decoration: none; + &:hover { + text-decoration: underline; + } + } +} diff --git a/chili-and-cilantro-react/src/app/components/dashboard-page.tsx b/chili-and-cilantro-react/src/app/components/dashboard-page.tsx new file mode 100644 index 0000000..65b6d02 --- /dev/null +++ b/chili-and-cilantro-react/src/app/components/dashboard-page.tsx @@ -0,0 +1,82 @@ +import { isAxiosError } from 'axios'; +import React, { memo, useCallback, useEffect, useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { useAuth } from '../auth-provider'; +import api from '../services/authenticated-api'; +import './dashboard-page.scss'; + +interface Game { + _id: string; + name: string; + balance: number; +} + +const DashboardPage: React.FC = () => { + const [participatingGames, setParticipatingGames] = useState([]); + const [createdGames, setCreatedGames] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const { isAuthenticated, user } = useAuth(); + const navigate = useNavigate(); + + const fetchGames = useCallback(async () => { + if (!isAuthenticated || !user) { + setIsLoading(false); + return; + } + setIsLoading(true); + try { + const response = await api.get('/game/list'); + setParticipatingGames(response.data.participatingGames); + setCreatedGames(response.data.createdGames); + } catch (error) { + console.error('Error fetching games:', error); + if (isAxiosError(error) && error.response?.status === 401) { + navigate('/login'); + } + } finally { + setIsLoading(false); + } + }, [isAuthenticated, user, navigate]); + + useEffect(() => { + (async () => { + await fetchGames(); + })(); + }, [fetchGames, isAuthenticated, user]); + + if (isLoading) { + return
Loading dashboard data...
; + } + + const renderGameList = (games: Game[], title: string) => ( +
+

{title}

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

No games available.

+ ) : ( +
    + {games.map((game) => ( +
  • + {game.name} +
  • + ))} +
+ )} +
+ ); + + return ( +
+
+

Your Dashboard

+ {renderGameList(participatingGames, "Games You're Participating In")} + {renderGameList(createdGames, "Games You've Created")} + + Create New Game + +
+
+ ); +}; + +export default memo(DashboardPage); diff --git a/chili-and-cilantro-react/src/app/components/forgot-password-page.tsx b/chili-and-cilantro-react/src/app/components/forgot-password-page.tsx new file mode 100644 index 0000000..acb9cbd --- /dev/null +++ b/chili-and-cilantro-react/src/app/components/forgot-password-page.tsx @@ -0,0 +1,175 @@ +import { constants } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { isAxiosError } from 'axios'; +import { useFormik } from 'formik'; +import React, { useEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import * as Yup from 'yup'; +import api from '../services/api'; +import './auth.scss'; + +type FormValues = { + email?: string; + password?: string; + confirmPassword?: string; +}; + +const ForgotPasswordPage: React.FC = () => { + const [successMessage, setSuccessMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + const [isTokenValid, setIsTokenValid] = useState(null); + const navigate = useNavigate(); + const location = useLocation(); + + useEffect(() => { + const params = new URLSearchParams(location.search); + const token = params.get('token'); + + if (token) { + (async () => { + await validateToken(token); + })(); + } + }, [location]); + + const validateToken = async (token: string) => { + try { + await api.get(`/user/verify-reset-token?token=${token}`); + setIsTokenValid(true); + } catch { + setIsTokenValid(false); + setErrorMessage( + 'Invalid or expired token. Please request a new password reset.', + ); + } + }; + + const initialValues: FormValues = isTokenValid + ? { password: '', confirmPassword: '' } + : { email: '' }; + + const validationSchema = isTokenValid + ? Yup.object({ + password: Yup.string() + .matches(constants.PASSWORD_REGEX, constants.PASSWORD_REGEX_ERROR) + .required('Required'), + confirmPassword: Yup.string() + .oneOf([Yup.ref('password')], 'Passwords must match') + .required('Required'), + }) + : Yup.object({ + email: Yup.string().email('Invalid email address').required('Required'), + }); + + const handleSubmit = async (values: FormValues) => { + try { + if (values.email) { + // Handle forgot password + const response = await api.post('/user/forgot-password', { + email: values.email, + }); + if (response.status === 200) { + setSuccessMessage(response.data.message); + setErrorMessage(''); + } else { + setErrorMessage(response.data.message); + setSuccessMessage(''); + } + } else if (values.password && values.confirmPassword) { + // Handle password reset + const params = new URLSearchParams(location.search); + const token = params.get('token'); + if (!token) { + setErrorMessage( + 'Invalid token. Please try the password reset process again.', + ); + return; + } + const response = await api.post('/user/reset-password', { + token, + password: values.password, + }); + if (response.status === 200) { + setSuccessMessage( + 'Your password has been successfully reset. You can now log in with your new password.', + ); + setErrorMessage(''); + setTimeout(() => navigate('/login'), 3000); + } else { + setErrorMessage(response.data.message); + setSuccessMessage(''); + } + } + } catch (error) { + if (isAxiosError(error) && error.response) { + setErrorMessage( + error.response.data.message || + 'An error occurred while processing your request.', + ); + setSuccessMessage(''); + } else { + setErrorMessage('An unexpected error occurred'); + setSuccessMessage(''); + } + } + }; + + const formik = useFormik({ + initialValues, + validationSchema, + onSubmit: handleSubmit, + }); + + return ( +
+

+ {isTokenValid ? 'Reset Password' : 'Forgot Password'} +

+
+ {isTokenValid ? ( + <> +
+ + + {formik.errors.password && ( +
{formik.errors.password}
+ )} +
+
+ + + {formik.errors.confirmPassword && ( +
{formik.errors.confirmPassword}
+ )} +
+ + ) : ( +
+ + + {formik.errors.email && ( +
{formik.errors.email}
+ )} +
+ )} + + +
+ {successMessage && ( +
{successMessage}
+ )} + {errorMessage &&
{errorMessage}
} +
+ ); +}; + +export default ForgotPasswordPage; diff --git a/chili-and-cilantro-react/src/components/game.tsx b/chili-and-cilantro-react/src/app/components/game.tsx similarity index 75% rename from chili-and-cilantro-react/src/components/game.tsx rename to chili-and-cilantro-react/src/app/components/game.tsx index 738fb55..dcc8f1a 100644 --- a/chili-and-cilantro-react/src/components/game.tsx +++ b/chili-and-cilantro-react/src/app/components/game.tsx @@ -1,10 +1,10 @@ -import { IdToken, useAuth0 } from '@auth0/auth0-react'; -import { useEffect, useState } from 'react'; -import { environment } from '../environments/environment.prod'; +import React, { useState } from 'react'; +import { environment } from '../../environments/environment.prod'; +import { useAuth } from '../auth-provider'; // import { connect, Socket } from 'socket.io-client'; function GameComponent() { - const { isAuthenticated, getIdTokenClaims } = useAuth0(); + const { isAuthenticated } = useAuth(); const [mode, setMode] = useState<'CREATE' | 'JOIN' | null>(null); const [token, setToken] = useState(null); const [gameName, setGameName] = useState(''); @@ -13,19 +13,6 @@ function GameComponent() { const [gameCode, setGameCode] = useState(''); //const [socket, setSocket] = useState(null); - useEffect(() => { - if (isAuthenticated) { - getIdTokenClaims().then((claims: IdToken | undefined) => { - if (claims === undefined) { - return; - } - const idToken = claims.__raw; - setToken(idToken); - //connectToSocket(idToken); - }); - } - }, [isAuthenticated]); - // const connectToSocket = (token: string) => { // const socketInstance = connect(environment.game.socketHost, { // query: { token }, @@ -56,7 +43,7 @@ function GameComponent() { displayName, }), }); - handleGameResponse(response); + await handleGameResponse(response); }; const joinGame = async () => { @@ -72,7 +59,7 @@ function GameComponent() { displayName, }), }); - handleGameResponse(response); + await handleGameResponse(response); }; return ( @@ -85,7 +72,9 @@ function GameComponent() { setGameName(e.target.value)} + onChange={(e: React.ChangeEvent) => + setGameName(e.target.value) + } />
)} @@ -95,14 +84,18 @@ function GameComponent() { setDisplayName(e.target.value)} + onChange={(e: React.ChangeEvent) => + setDisplayName(e.target.value) + } /> {mode === 'JOIN' && ( setGameCode(e.target.value)} + onChange={(e: React.ChangeEvent) => + setGameCode(e.target.value) + } /> )} @@ -110,7 +103,9 @@ function GameComponent() { placeholder="Game Password (optional)" type="password" value={gamePassword} - onChange={(e) => setGamePassword(e.target.value)} + onChange={(e: React.ChangeEvent) => + setGamePassword(e.target.value) + } /> + )} + + + + )} + +
+
+ { + e.preventDefault(); + setLoginType(loginType === 'email' ? 'username' : 'email'); + }} + > + Switch to {loginType === 'email' ? 'Username' : 'Email'} Login + +
+
+ Forgot Password? + + Don't have an account? Register + +
+
+ + ); +}; + +export default LoginPage; diff --git a/chili-and-cilantro-react/src/app/components/player-disc.scss b/chili-and-cilantro-react/src/app/components/player-disc.scss new file mode 100644 index 0000000..8cca43f --- /dev/null +++ b/chili-and-cilantro-react/src/app/components/player-disc.scss @@ -0,0 +1,71 @@ +.player-disc { + width: 200px; + height: 200px; + border-radius: 50%; + position: relative; + display: flex; + align-items: center; + justify-content: center; + box-shadow: + 0 4px 8px rgba(0, 0, 0, 0.2), + 0 2px 4px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease-in-out; +} + +.player-disc.reveal { + transform: rotateY(180deg); +} + +.card-front { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + backface-visibility: hidden; +} + +.card-front img { + width: 70%; + height: 70%; + object-fit: contain; +} + +.card-back { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 50%; + background: repeating-radial-gradient( + circle, + #ffffff, + rgba(255, 255, 255, 0.1) 20% + ); + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + backface-visibility: hidden; + transform: rotateY(180deg); +} + +.card-back .flourish { + position: absolute; + width: 100%; + height: 100%; + pointer-events: none; + background: repeating-radial-gradient( + circle, + transparent, + rgba(255, 255, 255, 0.2) 20% + ); +} + +.card-back p { + color: white; + font-size: 1.2rem; + font-family: 'Playfair Display', serif; + text-align: center; +} diff --git a/chili-and-cilantro-react/src/app/components/player-disc.tsx b/chili-and-cilantro-react/src/app/components/player-disc.tsx new file mode 100644 index 0000000..2df426d --- /dev/null +++ b/chili-and-cilantro-react/src/app/components/player-disc.tsx @@ -0,0 +1,119 @@ +import { CardType } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { Box, Typography } from '@mui/material'; +import { FC } from 'react'; + +export interface PlayerDiscProps { + player: number; + type: CardType; + reveal: boolean; +} + +export const PlayerDisc: FC = ({ + player, + type, + reveal, +}: { + player: number; + type: CardType; + reveal: boolean; +}) => { + const playerThemes = [ + { background: '#ff5733', border: '#c70039' }, // Player 1: Chili red + { background: '#4caf50', border: '#087f23' }, // Player 2: Cilantro green + { background: '#ffc107', border: '#c79100' }, // Player 3: Spicy yellow + { background: '#3f51b5', border: '#2c387e' }, // Player 4: Bold blue + { background: '#e91e63', border: '#b0003a' }, // Player 5: Vibrant pink + { background: '#795548', border: '#4b2c20' }, // Player 6: Earthy brown + ]; + + // Get the current player's theme + const theme = playerThemes[player - 1]; + + // Determine the icon to display + const iconSrc = + type === 'chili' + ? '/assets/images/chili.png' + : '/assets/images/cilantro.png'; + + return ( + + {reveal ? ( + + + + Reveal + + + ) : ( + + + + )} + + ); +}; diff --git a/chili-and-cilantro-react/src/app/components/private-route.tsx b/chili-and-cilantro-react/src/app/components/private-route.tsx new file mode 100644 index 0000000..9e200b8 --- /dev/null +++ b/chili-and-cilantro-react/src/app/components/private-route.tsx @@ -0,0 +1,28 @@ +import React, { useContext } from 'react'; +import { Navigate, useLocation } from 'react-router-dom'; +import { AuthContext } from '../auth-provider'; + +interface PrivateRouteProps { + children: React.ReactNode; +} + +const PrivateRoute: React.FC = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { isAuthenticated, loading } = useContext(AuthContext); + const location = useLocation(); + + if (loading) { + return
Checking authentication...
; + } + + if (!isAuthenticated) { + return ; + } + + return <>{children}; +}; + +export default PrivateRoute; diff --git a/chili-and-cilantro-react/src/app/components/register-page.tsx b/chili-and-cilantro-react/src/app/components/register-page.tsx new file mode 100644 index 0000000..cc1de6a --- /dev/null +++ b/chili-and-cilantro-react/src/app/components/register-page.tsx @@ -0,0 +1,155 @@ +import { constants } from '@chili-and-cilantro/chili-and-cilantro-lib'; +import { isAxiosError } from 'axios'; +import { FormikHelpers, useFormik } from 'formik'; +import React, { useEffect, useState } from 'react'; +import { Navigate, useNavigate } from 'react-router-dom'; +import * as Yup from 'yup'; +import { AuthContext } from '../auth-provider'; +import authService from '../services/auth-service'; +import './auth.scss'; + +interface FormValues { + username: string; + email: string; + password: string; + timezone: string; +} + +const RegisterPage: React.FC = () => { + const navigate = useNavigate(); + const { isAuthenticated } = React.useContext(AuthContext); + const [registrationError, setRegistrationError] = useState( + null, + ); + const [registrationSuccess, setRegistrationSuccess] = + useState(false); + const [userTimezone, setUserTimezone] = useState(''); + + useEffect(() => { + // Get the user's timezone + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + setUserTimezone(timezone); + }, []); + + const formik = useFormik({ + initialValues: { + username: '', + email: '', + password: '', + timezone: userTimezone, + }, + enableReinitialize: true, + validationSchema: Yup.object({ + username: Yup.string() + .matches(constants.USERNAME_REGEX, constants.USERNAME_REGEX_ERROR) + .required('Required'), + email: Yup.string().email('Invalid email address').required('Required'), + password: Yup.string() + .matches(constants.PASSWORD_REGEX, constants.PASSWORD_REGEX_ERROR) + .required('Required'), + timezone: Yup.string().required('Timezone is required'), + }), + onSubmit: async (values, { setSubmitting }: FormikHelpers) => { + try { + await authService.register( + values.username, + values.email, + values.password, + values.timezone, + ); + setRegistrationError(null); + setRegistrationSuccess(true); + setTimeout(() => { + navigate('/login'); + }, 3000); + } catch (error: unknown) { + console.error(error); + if (isAxiosError(error) && error.response) { + setRegistrationError( + error.response.data?.message || + 'An error occurred during registration. Please try again.', + ); + } else { + setRegistrationError( + 'An unexpected error occurred. Please try again.', + ); + } + setRegistrationSuccess(false); + } finally { + setSubmitting(false); + } + }, + }); + + if (isAuthenticated) { + return ; + } + + return ( +
+

Register

+ {registrationSuccess && ( +
+ Registration successful! You will be redirected to the login page + shortly. +
+ )} +
+
+ + + {formik.touched.username && formik.errors.username ? ( +
{formik.errors.username}
+ ) : null} +
+ +
+ + + {formik.touched.email && formik.errors.email ? ( +
{formik.errors.email}
+ ) : null} +
+ +
+ + + {formik.touched.password && formik.errors.password ? ( +
{formik.errors.password}
+ ) : null} +
+ + {registrationError && ( +
{registrationError}
+ )} + + +
+
+ ); +}; + +export default RegisterPage; diff --git a/chili-and-cilantro-react/src/app/components/splash-page.scss b/chili-and-cilantro-react/src/app/components/splash-page.scss new file mode 100644 index 0000000..3576c57 --- /dev/null +++ b/chili-and-cilantro-react/src/app/components/splash-page.scss @@ -0,0 +1,102 @@ +@import '../../styles.scss'; + +// Splash Page styles +.splash-container { + max-width: 800px; + margin: 2rem auto; + padding: 2rem; + background-color: $primary-dark; // Using dark theme background + border-radius: 8px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); // Darker shadow for depth + text-align: center; + color: $text-color; // Use shared text color + + .splash-logo { + max-width: 60%; + margin-bottom: 1rem; + } + + .splash-subdescription { + font-size: 1.5rem; + color: $highlight-color; // Use highlight color + font-family: $font-family; // Shared primary font + margin-bottom: 2rem; + } + + .feature-list { + margin-bottom: 2rem; + text-align: left; + + .feature-title { + color: $accent-color; // Use accent color for headers + font-size: 1.8rem; + font-family: $font-secondary; // Use secondary font for contrast + margin-bottom: 1rem; + text-align: center; + } + + .feature-bullets { + list-style-type: disc; + padding-left: 2rem; + + li { + margin-bottom: 0.5rem; + font-size: 1.1rem; + color: $text-muted; // Muted text color for bullets + font-family: $font-family; + } + + li:hover { + color: $text-color; // Highlight bullets on hover + transition: color 0.3s ease-in-out; + } + } + } + + .game-description { + background-color: $secondary-dark; // Use lighter dark for the card + padding: 1.5rem; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); // Subtle shadow for depth + margin-bottom: 2rem; + + h3 { + color: $highlight-color; // Use highlight color for headers + font-family: $font-family; + font-size: 1.5rem; + margin-bottom: 1rem; + } + + p { + color: $text-muted; // Muted text for description + font-family: $font-family; + margin: 0; + } + } + + .cta-container { + display: flex; + justify-content: center; + gap: 1rem; + + .btn { + @extend .btn; // Extend shared button styles + text-decoration: none; + text-align: center; + + &-primary { + @extend .btn-primary; // Use primary button style + &:hover { + background-color: $highlight-hover; // Shared hover color + } + } + + &-secondary { + @extend .btn-secondary; // Use secondary button style + &:hover { + background-color: $accent-hover; // Shared hover color + } + } + } + } +} diff --git a/chili-and-cilantro-react/src/app/components/splash-page.tsx b/chili-and-cilantro-react/src/app/components/splash-page.tsx new file mode 100644 index 0000000..137f8e8 --- /dev/null +++ b/chili-and-cilantro-react/src/app/components/splash-page.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import chiliCilantroLogo from '../../assets/images/Chili-and-Cilantro-logo.png'; +import './splash-page.scss'; + +const SplashPage: React.FC = () => { + return ( +
+ Chili and Cilantro +

A Spicy Bluffing Game

+
+

Key Features:

+
    +
  • Exciting bluffing gameplay with a culinary twist
  • +
  • Strategic bidding and card placement
  • +
  • Quick to learn, challenging to master
  • +
  • Supports 2 or more players
  • +
  • Rounds of suspenseful card flipping
  • +
  • Risk management: avoid the dreaded chili!
  • +
  • First to season two dishes or last chef standing wins
  • +
  • Perfect for game nights and family gatherings
  • +
+
+
+

How to Play:

+

+ In Chili and Cilantro, aspiring chefs compete to create the perfect + dish. Your goal is to add just the right amount of cilantro without + ruining it with a scorching chili. Be the first to successfully season + two dishes or be the last chef standing to win! +

+
+
+ + Start Cooking! + + + Return to Kitchen + +
+
+ ); +}; + +export default SplashPage; diff --git a/chili-and-cilantro-react/src/app/components/top-menu.scss b/chili-and-cilantro-react/src/app/components/top-menu.scss new file mode 100644 index 0000000..0ece32a --- /dev/null +++ b/chili-and-cilantro-react/src/app/components/top-menu.scss @@ -0,0 +1,143 @@ +@import '../../styles.scss'; + +// TopMenu styles +.top-menu { + background-color: $background-color; + padding: 0.5rem 2rem; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + + .menu-icon { + background: none; + border: none; + cursor: pointer; + font-size: 1.5rem; + color: $light-color; + padding: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + transition: background-color 0.3s ease; + + &:hover { + background-color: rgba($light-color, 0.1); + } + } + + .game-dropdown { + position: relative; + + .game-dropdown-menu { + position: absolute; + right: 0; + top: 100%; + background-color: $background-color; + border-radius: 4px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + min-width: 150px; + z-index: 1001; + + button { + background: none; + border: none; + color: $light-color; + padding: 0.5rem 1rem; + text-align: left; + cursor: pointer; + transition: background-color 0.3s ease; + + &:hover { + background-color: rgba($light-color, 0.1); + } + } + } + } + + .user-dropdown { + position: relative; + + .fallback-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + opacity: 0; + transition: opacity 0.3s ease; + } + + &:not(:has(svg)) .fallback-icon { + opacity: 1; + } + + .dropdown-menu { + position: absolute; + right: 0; + top: 100%; + background-color: $background-color; + border-radius: 4px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + min-width: 150px; + + a { + color: $light-color; + padding: 0.5rem 1rem; + text-decoration: none; + transition: background-color 0.3s ease; + + &:hover { + background-color: rgba($light-color, 0.1); + } + } + } + } + + .container { + display: flex; + justify-content: space-between; + align-items: center; + max-width: 1200px; + margin: 0 auto; + } + + .logo-container { + display: flex; + align-items: center; + background-color: $background-color; + + .logo-symbol { + height: 30px; + } + + .logo-text { + font-size: 1.2rem; + color: $primary-color; + } + } + + .nav-links { + display: flex; + align-items: center; + gap: 1rem; + + a { + color: $light-color; + text-decoration: none; + padding: 0.25rem 0.5rem; + border-radius: 4px; + transition: background-color 0.3s ease; + + &:hover { + background-color: rgba($light-color, 0.1); + } + } + } +} diff --git a/chili-and-cilantro-react/src/app/components/top-menu.tsx b/chili-and-cilantro-react/src/app/components/top-menu.tsx new file mode 100644 index 0000000..e72239d --- /dev/null +++ b/chili-and-cilantro-react/src/app/components/top-menu.tsx @@ -0,0 +1,143 @@ +import { faSackDollar, faUserCircle } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import React, { useEffect, useRef, useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import chiliCilantroSymbol from '../../assets/images/Chili-and-Cilantro.png'; +import { AuthContext } from '../auth-provider'; +import { GameMenuOption, useMenu } from '../menu-context'; +import './top-menu.scss'; + +const TopMenu: React.FC = () => { + const { isAuthenticated, logout } = React.useContext(AuthContext); + const { gameOptions } = useMenu(); + const [, setRender] = useState(0); + const [dropdownOpen, setDropdownOpen] = useState(false); + const [gameDropdownOpen, setGameDropdownOpen] = useState(false); + const navigate = useNavigate(); + const dropdownRef = useRef(null); + const gameDropdownRef = useRef(null); + + useEffect(() => { + setRender((prev) => prev + 1); + }, [isAuthenticated, navigate]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setDropdownOpen(false); + } + if ( + gameDropdownRef.current && + !gameDropdownRef.current.contains(event.target as Node) + ) { + setGameDropdownOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const toggleDropdown = () => { + setDropdownOpen(!dropdownOpen); + }; + + return ( + + ); +}; + +export default TopMenu; diff --git a/chili-and-cilantro-react/src/app/components/verify-email-page.scss b/chili-and-cilantro-react/src/app/components/verify-email-page.scss new file mode 100644 index 0000000..5200012 --- /dev/null +++ b/chili-and-cilantro-react/src/app/components/verify-email-page.scss @@ -0,0 +1,38 @@ +@import '../../styles.scss'; + +.verification-container { + max-width: 400px; + margin: 80px auto 0; // Increased top margin to account for TopMenu + padding: 2rem; + background-color: $light-color; + border-radius: 8px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + text-align: center; + + .verification-title { + color: $primary-color; + margin-bottom: 1rem; + } + + .verification-message { + margin-bottom: 1rem; + + &.success { + color: $success-color; + } + + &.error { + color: $danger-color; + } + } + + .verification-action { + margin-top: 1rem; + + a { + @extend .btn; + @extend .btn-primary; + text-decoration: none; + } + } +} diff --git a/chili-and-cilantro-react/src/app/components/verify-email-page.tsx b/chili-and-cilantro-react/src/app/components/verify-email-page.tsx new file mode 100644 index 0000000..4518514 --- /dev/null +++ b/chili-and-cilantro-react/src/app/components/verify-email-page.tsx @@ -0,0 +1,72 @@ +import React, { useEffect, useState } from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import api from '../services/api'; +import './verify-email-page.scss'; + +const VerifyEmailPage: React.FC = () => { + const [message, setMessage] = useState(''); + const [loading, setLoading] = useState(true); + const [verificationStatus, setVerificationStatus] = useState< + 'pending' | 'success' | 'error' + >('pending'); + + const location = useLocation(); + + useEffect(() => { + const query = new URLSearchParams(location.search); + const tokenFromQuery = query.get('token'); + + if (tokenFromQuery) { + (async () => { + await verifyEmail(tokenFromQuery); + })(); + } else { + setLoading(false); + setMessage('No verification token provided.'); + setVerificationStatus('error'); + } + }, [location]); + + const verifyEmail = async (token: string) => { + try { + const response = await api.get(`/user/verify-email?token=${token}`); + if (response.status === 200) { + setMessage('Email verified successfully!'); + setVerificationStatus('success'); + } else { + setMessage('Email verification failed. Please try again.'); + setVerificationStatus('error'); + } + } catch { + setMessage( + 'An error occurred during email verification. Please try again.', + ); + setVerificationStatus('error'); + } finally { + setLoading(false); + } + }; + + if (loading) { + return
Verifying email...
; + } + + return ( +
+

Email Verification

+

{message}

+ {verificationStatus === 'success' && ( +
+ Proceed to Login +
+ )} + {verificationStatus === 'error' && ( +
+ Return to Home +
+ )} +
+ ); +}; + +export default VerifyEmailPage; diff --git a/chili-and-cilantro-react/src/app/menu-context.tsx b/chili-and-cilantro-react/src/app/menu-context.tsx new file mode 100644 index 0000000..8207bc9 --- /dev/null +++ b/chili-and-cilantro-react/src/app/menu-context.tsx @@ -0,0 +1,87 @@ +// src/app/menuContext.tsx +import React, { + createContext, + useCallback, + useContext, + useMemo, + useState, +} from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from './auth-provider'; + +export type GameMenuOption = { + id: string; + label: string; + action: () => void; + isGlobal?: boolean; +}; + +interface MenuProviderProps { + children: React.ReactNode; +} + +interface MenuContextType { + gameOptions: GameMenuOption[]; + addGameOptions: (newOptions: GameMenuOption[]) => void; + setContextOptions: (newOptions: GameMenuOption[]) => void; + resetToGlobalOptions: () => void; +} + +const MenuContext = createContext(undefined); + +export const MenuProvider: React.FC = ({ children }) => { + const { user } = useAuth(); + const navigate = useNavigate(); + + const initialGameOptions: GameMenuOption[] = [ + { + id: 'create-game', + label: 'Create Game', + action: () => navigate('/create-game'), + isGlobal: true, + }, + ]; + const [globalOptions, setGlobalOptions] = + useState(initialGameOptions); + const [contextOptions, setContextOptions] = useState([]); + + const addGameOptions = useCallback((newOptions: GameMenuOption[]) => { + setGlobalOptions((prevOptions) => [ + ...prevOptions, + ...newOptions.filter((o) => o.isGlobal), + ]); + setContextOptions(newOptions); + }, []); + + const resetToGlobalOptions = useCallback(() => { + setContextOptions([]); + }, []); + + const contextValue = useMemo(() => { + const gameOptions = [...globalOptions, ...contextOptions]; + return { + gameOptions: gameOptions, + addGameOptions: addGameOptions, + setContextOptions, + resetToGlobalOptions, + }; + }, [ + globalOptions, + contextOptions, + addGameOptions, + setContextOptions, + resetToGlobalOptions, + ]); + + return ( + {children} + ); +}; + +export const useMenu = (): MenuContextType => { + const context = useContext(MenuContext); + if (context === undefined) { + throw new Error('useMenu must be used within a MenuProvider'); + } + return context; +}; diff --git a/chili-and-cilantro-react/src/app/nx-welcome.tsx b/chili-and-cilantro-react/src/app/nx-welcome.tsx deleted file mode 100644 index f0cd657..0000000 --- a/chili-and-cilantro-react/src/app/nx-welcome.tsx +++ /dev/null @@ -1,845 +0,0 @@ -/* - * * * * * * * * * * * * * * * * * * * * * * * * * * * * - This is a starter component and can be deleted. - * * * * * * * * * * * * * * * * * * * * * * * * * * * * - Delete this file and get started with your project! - * * * * * * * * * * * * * * * * * * * * * * * * * * * * - */ -export function NxWelcome({ title }: { title: string }) { - return ( - <> -