From e79b8d576f0e83ffb527bc5f6e77c212e1aa4bc4 Mon Sep 17 00:00:00 2001 From: Gabriel Goh <77230723+gycgabriel@users.noreply.github.com> Date: Sun, 12 Nov 2023 17:03:24 +0800 Subject: [PATCH 01/19] Delete collab service, deployment and gateway (#243) --- .github/workflows/integration.yml | 82 +++-- .github/workflows/production.yml | 21 +- README.md | 90 ++--- deployment/build-export-prod-images.sh | 26 -- .../admin-service-autoscaling.yaml | 18 - .../admin-service-deployment.yaml | 41 --- .../admin-service-service.yaml | 16 - .../collaboration-service-autoscaling.yaml | 18 - .../collaboration-service-deployment.yaml | 58 ---- .../collaboration-service-service.yaml | 16 - .../frontend-autoscaling.yaml | 18 - .../frontend-deployment.yaml | 33 -- .../gke-prod-manifests/frontend-ingress.yaml | 13 - .../gke-prod-manifests/frontend-service.yaml | 16 - .../gateway-autoscaling.yaml | 18 - .../gateway-backend-config.yaml | 14 - .../gateway-deployment.yaml | 57 ---- .../gateway-http-ingress.yaml | 13 - .../gke-prod-manifests/gateway-service.yaml | 28 -- .../gateway-wscollaboration-ingress.yaml | 13 - .../gateway-wsmatch-ingress.yaml | 13 - .../gke-prod-manifests/gke-managed-cert.yaml | 10 - .../matching-service-autoscaling.yaml | 18 - .../matching-service-deployment.yaml | 45 --- .../matching-service-service.yaml | 16 - .../question-service-autoscaling.yaml | 18 - .../question-service-deployment.yaml | 41 --- .../question-service-service.yaml | 16 - .../user-service-autoscaling.yaml | 18 - .../user-service-deployment.yaml | 41 --- .../user-service-service.yaml | 16 - .../prod-dockerfiles/Dockerfile.admin-service | 3 - .../Dockerfile.collaboration-service | 28 -- .../prod-dockerfiles/Dockerfile.frontend | 12 - .../prod-dockerfiles/Dockerfile.gateway | 24 -- .../Dockerfile.matching-service | 3 - .../Dockerfile.question-service | 3 - .../prod-dockerfiles/Dockerfile.user-service | 3 - docker-compose.yml | 33 -- .../src/gateway-address/gateway-address.ts | 18 +- frontend/src/hooks/useCollaboration.tsx | 221 ------------ .../src/pages/api/collaborationHandler.ts | 33 -- frontend/src/pages/interviews/match-found.tsx | 2 +- frontend/src/pages/room/[id].tsx | 187 ---------- .../src/providers/MatchmakingProvider.tsx | 2 +- services/collaboration-service/.gitignore | 64 ---- services/collaboration-service/README.md | 39 --- services/collaboration-service/package.json | 36 -- services/collaboration-service/src/app.ts | 42 --- .../collaboration-service/src/db/prisma-db.ts | 252 -------------- services/collaboration-service/src/ot.ts | 246 -------------- .../src/routes/demo.html | 136 -------- .../collaboration-service/src/routes/demo.ts | 9 - .../collaboration-service/src/routes/room.ts | 308 ----------------- .../src/swagger-output.json | 81 ----- .../collaboration-service/swagger-doc-gen.ts | 25 -- services/collaboration-service/tsconfig.json | 14 - services/gateway/.gitignore | 64 ---- services/gateway/README.md | 53 --- services/gateway/package.json | 27 -- services/gateway/src/app.ts | 81 ----- services/gateway/src/auth/auth.ts | 87 ----- services/gateway/src/auth/firebase.ts | 50 --- services/gateway/src/logging/logging.ts | 6 - .../src/proxied_routes/proxied_route_type.ts | 12 - .../src/proxied_routes/proxied_routes.ts | 87 ----- .../src/proxied_routes/service_names.ts | 34 -- services/gateway/src/proxy/proxy.ts | 14 - services/gateway/tsconfig.json | 14 - start-app-no-docker.sh | 2 - yarn.lock | 321 ++---------------- 71 files changed, 127 insertions(+), 3410 deletions(-) delete mode 100644 deployment/build-export-prod-images.sh delete mode 100644 deployment/gke-prod-manifests/admin-service-autoscaling.yaml delete mode 100644 deployment/gke-prod-manifests/admin-service-deployment.yaml delete mode 100644 deployment/gke-prod-manifests/admin-service-service.yaml delete mode 100644 deployment/gke-prod-manifests/collaboration-service-autoscaling.yaml delete mode 100644 deployment/gke-prod-manifests/collaboration-service-deployment.yaml delete mode 100644 deployment/gke-prod-manifests/collaboration-service-service.yaml delete mode 100644 deployment/gke-prod-manifests/frontend-autoscaling.yaml delete mode 100644 deployment/gke-prod-manifests/frontend-deployment.yaml delete mode 100644 deployment/gke-prod-manifests/frontend-ingress.yaml delete mode 100644 deployment/gke-prod-manifests/frontend-service.yaml delete mode 100644 deployment/gke-prod-manifests/gateway-autoscaling.yaml delete mode 100644 deployment/gke-prod-manifests/gateway-backend-config.yaml delete mode 100644 deployment/gke-prod-manifests/gateway-deployment.yaml delete mode 100644 deployment/gke-prod-manifests/gateway-http-ingress.yaml delete mode 100644 deployment/gke-prod-manifests/gateway-service.yaml delete mode 100644 deployment/gke-prod-manifests/gateway-wscollaboration-ingress.yaml delete mode 100644 deployment/gke-prod-manifests/gateway-wsmatch-ingress.yaml delete mode 100644 deployment/gke-prod-manifests/gke-managed-cert.yaml delete mode 100644 deployment/gke-prod-manifests/matching-service-autoscaling.yaml delete mode 100644 deployment/gke-prod-manifests/matching-service-deployment.yaml delete mode 100644 deployment/gke-prod-manifests/matching-service-service.yaml delete mode 100644 deployment/gke-prod-manifests/question-service-autoscaling.yaml delete mode 100644 deployment/gke-prod-manifests/question-service-deployment.yaml delete mode 100644 deployment/gke-prod-manifests/question-service-service.yaml delete mode 100644 deployment/gke-prod-manifests/user-service-autoscaling.yaml delete mode 100644 deployment/gke-prod-manifests/user-service-deployment.yaml delete mode 100644 deployment/gke-prod-manifests/user-service-service.yaml delete mode 100644 deployment/prod-dockerfiles/Dockerfile.collaboration-service delete mode 100644 deployment/prod-dockerfiles/Dockerfile.gateway delete mode 100644 frontend/src/hooks/useCollaboration.tsx delete mode 100644 frontend/src/pages/api/collaborationHandler.ts delete mode 100644 frontend/src/pages/room/[id].tsx delete mode 100644 services/collaboration-service/.gitignore delete mode 100644 services/collaboration-service/README.md delete mode 100644 services/collaboration-service/package.json delete mode 100644 services/collaboration-service/src/app.ts delete mode 100644 services/collaboration-service/src/db/prisma-db.ts delete mode 100644 services/collaboration-service/src/ot.ts delete mode 100644 services/collaboration-service/src/routes/demo.html delete mode 100644 services/collaboration-service/src/routes/demo.ts delete mode 100644 services/collaboration-service/src/routes/room.ts delete mode 100644 services/collaboration-service/src/swagger-output.json delete mode 100644 services/collaboration-service/swagger-doc-gen.ts delete mode 100644 services/collaboration-service/tsconfig.json delete mode 100644 services/gateway/.gitignore delete mode 100644 services/gateway/README.md delete mode 100644 services/gateway/package.json delete mode 100644 services/gateway/src/app.ts delete mode 100644 services/gateway/src/auth/auth.ts delete mode 100644 services/gateway/src/auth/firebase.ts delete mode 100644 services/gateway/src/logging/logging.ts delete mode 100644 services/gateway/src/proxied_routes/proxied_route_type.ts delete mode 100644 services/gateway/src/proxied_routes/proxied_routes.ts delete mode 100644 services/gateway/src/proxied_routes/service_names.ts delete mode 100644 services/gateway/src/proxy/proxy.ts delete mode 100644 services/gateway/tsconfig.json diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index a17da917..9be0dad3 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -6,8 +6,8 @@ on: - gh-pages env: - NODE_VER: '18.x' - JAVA_DISTRIBUTION: 'zulu' + NODE_VER: "18.x" + JAVA_DISTRIBUTION: "zulu" JAVA_VER: 11 FIREBASE_AUTH_EMULATOR_HOST: "127:0:0:1:9099" FIREBASE_TOKEN: ${{ secrets.FIREBASE_CI_TOKEN }} @@ -22,51 +22,47 @@ jobs: runs-on: ubuntu-22.04 steps: - - name: Checkout repository - uses: actions/checkout@v3 - with: - fetch-depth: 0 + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: ${{ env.NODE_VER }} + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VER }} - - name: Set up JDK 11 - uses: actions/setup-java@v3 - with: - distribution: ${{ env.JAVA_DISTRIBUTION }} - java-version: ${{ env.JAVA_VER }} + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + distribution: ${{ env.JAVA_DISTRIBUTION }} + java-version: ${{ env.JAVA_VER }} - - name: Install dependencies with immutable lockfile - run: yarn install --frozen-lockfile + - name: Install dependencies with immutable lockfile + run: yarn install --frozen-lockfile - - name: Run linting - run: | - yarn workspace admin-service lint - yarn workspace collaboration-service lint - yarn workspace frontend lint - yarn workspace gateway lint - yarn workspace matching-service lint - yarn workspace question-service lint - yarn workspace user-service lint + - name: Run linting + run: | + yarn workspace admin-service lint + yarn workspace frontend lint + yarn workspace matching-service lint + yarn workspace question-service lint + yarn workspace user-service lint - - name: Run unit tests - run: | - yarn workspace user-service test - yarn workspace admin-service test:ci + - name: Run unit tests + run: | + yarn workspace user-service test + yarn workspace admin-service test:ci - - name: Run system tests - run: | - yarn workspace user-service systemtest:ci - yarn workspace admin-service systemtest:ci + - name: Run system tests + run: | + yarn workspace user-service systemtest:ci + yarn workspace admin-service systemtest:ci - - name: Simulate production build - run: | - yarn workspace admin-service build - yarn workspace collaboration-service build - yarn workspace gateway build - yarn workspace matching-service build - yarn workspace question-service build - yarn workspace user-service build - yarn workspace frontend build + - name: Simulate production build + run: | + yarn workspace admin-service build + yarn workspace matching-service build + yarn workspace question-service build + yarn workspace user-service build + yarn workspace frontend build diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml index b16d2016..154ebd42 100644 --- a/.github/workflows/production.yml +++ b/.github/workflows/production.yml @@ -7,20 +7,17 @@ on: workflows: ["Continuous Integration"] # Run only after CI passes types: [completed] branches: - - prod + - prod env: PROJECT_ID: peerprep-group11-prod ARTIFACT_REPOSITORY_NAME: codeparty-prod-images - GKE_CLUSTER: codeparty-g11-prod # Add your cluster name here. - GKE_REGION: asia-southeast1 # Add your cluster zone here. + GKE_CLUSTER: codeparty-g11-prod # Add your cluster name here. + GKE_REGION: asia-southeast1 # Add your cluster zone here. FIREBASE_SERVICE_ACCOUNT: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_PROD }} PRISMA_DATABASE_URL: ${{ secrets.PRISMA_DATABASE_URL_PROD }} MONGO_ATLAS_URL: ${{ secrets.MONGO_ATLAS_URL_PROD }} NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG: ${{ secrets.FRONTEND_FIREBASE_CONFIG_PROD }} - NEXT_PUBLIC_HTTP_PROXY_GATEWAY_ADDRESS: https://api.codeparty.org/ - NEXT_PUBLIC_WS_MATCH_PROXY_GATEWAY_ADDRESS: https://wsmatch.codeparty.org - NEXT_PUBLIC_WS_COLLABORATION_PROXY_GATEWAY_ADDRESS: https://wscollab.codeparty.org TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }} TWILIO_API_KEY: ${{ secrets.TWILIO_API_KEY }} TWILIO_API_SECRET: ${{ secrets.TWILIO_API_SECRET }} @@ -32,20 +29,20 @@ jobs: environment: production if: ${{ github.event.workflow_run.conclusion == 'success' }} permissions: - contents: 'read' - id-token: 'write' + contents: "read" + id-token: "write" steps: - name: Checkout uses: actions/checkout@v4 - - id: 'auth' + - id: "auth" name: Authenticate to Google Cloud - uses: 'google-github-actions/auth@v1' + uses: "google-github-actions/auth@v1" with: - token_format: 'access_token' + token_format: "access_token" workload_identity_provider: projects/345207492413/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-oidc - service_account: 'github-actions-service@peerprep-group11-prod.iam.gserviceaccount.com' + service_account: "github-actions-service@peerprep-group11-prod.iam.gserviceaccount.com" # Setup gcloud CLI - name: Setup Google Cloud SDK diff --git a/README.md b/README.md index 5f79a4ca..9e3a6dad 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,15 @@ Prerequisites for PeerPrep Monorepo: 1. **Yarn:** Ensure you have the latest version of Yarn installed. Yarn - Workspaces is available in Yarn v1.0 and later. + Workspaces is available in Yarn v1.0 and later. 2. Installation (if not already installed): - ```bash - npm install -g yarn - ``` + ```bash + npm install -g yarn + ``` 3. **Node.js:** Check each application's documentation for the recommended - Node.js version. + Node.js version. 4. **Git (Optional but Recommended):** 5. **Docker (If deploying with Docker):** 6. **Kubernetes Tools (If deploying with Kubernetes):** @@ -32,8 +32,6 @@ your services / frontend. │ ├── /user-service (express application) │ ├── /matching-service (express application) │ ├── /question-service (express application) -│ ├── /collaboration-service (express application) -│ └── /gateway (express application) ├── /frontend │ └── /pages for peerprep (NextJs application) ├── /deployment @@ -47,34 +45,33 @@ your services / frontend. ### Getting Started - Local Development: 1. Ensure that you have an `.env` file at the root directory with the following variables: - ```bash - PRISMA_DATABASE_URL= - MONGO_ATLAS_URL= - FIREBASE_SERVICE_ACCOUNT= - NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG={"apiKey": ,"authDomain": ,"projectId": ,"storageBucket": ,"messagingSenderId": ,"appId": } - TWILIO_ACCOUNT_SID= - TWILIO_API_KEY= - TWILIO_API_SECRET= - ``` -Note: For `NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG`, the JSON should not have newlines since Next.js may not process it correctly. -The difference between it and `FIREBASE_SERVICE_ACCOUNT` are shown below: - -| Variable | Purpose | -| -------- | ------- | -| FIREBASE_SERVICE_ACCOUNT | For backend verification and administrative tasks | -| NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG | For the frontend to connect to Firebase | + `bash + PRISMA_DATABASE_URL= + MONGO_ATLAS_URL= + FIREBASE_SERVICE_ACCOUNT= + NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG={"apiKey": ,"authDomain": ,"projectId": ,"storageBucket": ,"messagingSenderId": ,"appId": } + TWILIO_ACCOUNT_SID= + TWILIO_API_KEY= + TWILIO_API_SECRET= + ` + Note: For `NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG`, the JSON should not have newlines since Next.js may not process it correctly. + The difference between it and `FIREBASE_SERVICE_ACCOUNT` are shown below: + +| Variable | Purpose | +| ------------------------------------ | ------------------------------------------------- | +| FIREBASE_SERVICE_ACCOUNT | For backend verification and administrative tasks | +| NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG | For the frontend to connect to Firebase | 2. **Installing secret detection hooks:** From the root directory, run: - ```bash - pip install pre-commit - pre-commit install - ``` - + ```bash + pip install pre-commit + pre-commit install + ``` + **Disclaimer:** There is no guarantee that all secrets will be detected. As a tip, if you think a file will eventually store secrets, immediately add it to .gitignore upon creating it in case you forget later on when you have a lot more files to commit. - 3. **Installing Dependencies:** From the root directory (`/peerprep`), run: ```bash @@ -125,35 +122,41 @@ it in case you forget later on when you have a lot more files to commit. 8. **Running everything at once:** To run everything at once and still maintain the ability to hot-reload your changes, use: - ```bash - ./start-app-no-docker.sh # on mac /linus - - # You can also use the above command on Windows with Git Bash - - ``` + ```bash + ./start-app-no-docker.sh # on mac /linus + + # You can also use the above command on Windows with Git Bash + + ``` ### Getting Started - Docker: -Docker and Docker Compose are used to set up a simulated production build (meaning that the Docker images and + +Docker and Docker Compose are used to set up a simulated production build (meaning that the Docker images and containers that will be spun up locally are almost identical to those in the production environment, with the exception of some environment variables). 1. **Run yarn docker:build:** From the root repo, run ```bash -yarn docker:build +yarn docker:build ``` + This will create new Docker images. 2. **Run yarn docker:devup:** From the root repo, run + ```bash -yarn docker:devup +yarn docker:devup ``` + This will start all the containers. 3. **Once done, run yarn docker:devdown:** From the root repo, run + ```bash -yarn docker:devdown +yarn docker:devdown ``` + This will stop and delete all the containers. #### If you want to do all the above steps at once, see the below section @@ -165,6 +168,7 @@ This will stop and delete all the containers. # You can also use the above command on Windows with Git Bash ``` + This will create new Docker images everytime it is run. Be careful of how much disk space you have left. Any edits you make to the source code will not be automatically reflected on the site. We recommend using Docker @@ -195,19 +199,23 @@ Next steps: ``` ### Firebase Local Emulator Suite + The [Firebase Local Emulator Suite](https://firebase.google.com/docs/emulator-suite) is used to support automated testing of any Firebase-related functionality. The following files at the project root define the Firebase project as well as the emulators used: -* `.firebaserc` - The Firebase project definitions -* `firebase.json` - The emulators that are used + +- `.firebaserc` - The Firebase project definitions +- `firebase.json` - The emulators that are used For local testing, the file used for passing in environment variables has to be named: + ``` .env.firebase_emulators_test ``` This file should contain the following environment variables: + ``` FIREBASE_AUTH_EMULATOR_HOST="127.0.0.1:9099" FIREBASE_SERVICE_ACCOUNT={insert secret JSON value here} diff --git a/deployment/build-export-prod-images.sh b/deployment/build-export-prod-images.sh deleted file mode 100644 index f859d851..00000000 --- a/deployment/build-export-prod-images.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -# Build root docker image with context set to be parent directory -docker build -t peerprep-base -f ../Dockerfile .. - -# Create array of services -declare -a service_array -service_array=("admin-service" "collaboration-service" "gateway" "matching-service" "question-service" "user-service") - -# Build and publish backend prod images with context set to be parent directory -for i in ${!service_array[@]}; do - docker build \ - --tag $GKE_REGION-docker.pkg.dev/$PROJECT_ID/$ARTIFACT_REPOSITORY_NAME/${service_array[$i]}:latest \ - --file prod-dockerfiles/Dockerfile.${service_array[$i]} .. - docker push $GKE_REGION-docker.pkg.dev/$PROJECT_ID/$ARTIFACT_REPOSITORY_NAME/${service_array[$i]}:latest -done - -# Build and publish frontend prod image -docker build \ - --build-arg="NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG_ARG=$NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG" \ - --build-arg="NEXT_PUBLIC_HTTP_PROXY_GATEWAY_ADDRESS_ARG=$NEXT_PUBLIC_HTTP_PROXY_GATEWAY_ADDRESS" \ - --build-arg="NEXT_PUBLIC_WS_MATCH_PROXY_GATEWAY_ADDRESS_ARG=$NEXT_PUBLIC_WS_MATCH_PROXY_GATEWAY_ADDRESS" \ - --build-arg="NEXT_PUBLIC_WS_COLLABORATION_PROXY_GATEWAY_ADDRESS_ARG=$NEXT_PUBLIC_WS_COLLABORATION_PROXY_GATEWAY_ADDRESS" \ - --tag $GKE_REGION-docker.pkg.dev/$PROJECT_ID/$ARTIFACT_REPOSITORY_NAME/frontend:latest \ - --file prod-dockerfiles/Dockerfile.frontend .. -docker push $GKE_REGION-docker.pkg.dev/$PROJECT_ID/$ARTIFACT_REPOSITORY_NAME/frontend:latest diff --git a/deployment/gke-prod-manifests/admin-service-autoscaling.yaml b/deployment/gke-prod-manifests/admin-service-autoscaling.yaml deleted file mode 100644 index 9b007ece..00000000 --- a/deployment/gke-prod-manifests/admin-service-autoscaling.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: admin-service-autoscaling -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: admin-service - minReplicas: 1 - maxReplicas: 2 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 50 diff --git a/deployment/gke-prod-manifests/admin-service-deployment.yaml b/deployment/gke-prod-manifests/admin-service-deployment.yaml deleted file mode 100644 index 7a28ae35..00000000 --- a/deployment/gke-prod-manifests/admin-service-deployment.yaml +++ /dev/null @@ -1,41 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - io.kompose.service: admin-service - name: admin-service - namespace: default -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: admin-service - strategy: {} - template: - metadata: - labels: - io.kompose.network/cs3219-project-default: "true" - io.kompose.service: admin-service - spec: - containers: - - env: - - name: FIREBASE_SERVICE_ACCOUNT - valueFrom: - secretKeyRef: - name: firebase-service-account - key: firebase-service-account - - name: PORT - value: "5005" - image: asia-southeast1-docker.pkg.dev/peerprep-group11-prod/codeparty-prod-images/admin-service:latest - name: admin-service - ports: - - containerPort: 5005 - hostPort: 5005 - protocol: TCP - resources: - # You must specify requests for CPU to autoscale - # based on CPU utilization - requests: - cpu: "100m" - restartPolicy: Always -status: {} diff --git a/deployment/gke-prod-manifests/admin-service-service.yaml b/deployment/gke-prod-manifests/admin-service-service.yaml deleted file mode 100644 index fc14bd05..00000000 --- a/deployment/gke-prod-manifests/admin-service-service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - io.kompose.service: admin-service - name: admin-service - namespace: default -spec: - ports: - - name: "5005" - port: 5005 - targetPort: 5005 - selector: - io.kompose.service: admin-service -status: - loadBalancer: {} diff --git a/deployment/gke-prod-manifests/collaboration-service-autoscaling.yaml b/deployment/gke-prod-manifests/collaboration-service-autoscaling.yaml deleted file mode 100644 index 524b4f91..00000000 --- a/deployment/gke-prod-manifests/collaboration-service-autoscaling.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: collaboration-service-autoscaling -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: collaboration-service - minReplicas: 1 - maxReplicas: 2 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 50 diff --git a/deployment/gke-prod-manifests/collaboration-service-deployment.yaml b/deployment/gke-prod-manifests/collaboration-service-deployment.yaml deleted file mode 100644 index b3da11ca..00000000 --- a/deployment/gke-prod-manifests/collaboration-service-deployment.yaml +++ /dev/null @@ -1,58 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - io.kompose.service: collaboration-service - name: collaboration-service - namespace: default -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: collaboration-service - strategy: {} - template: - metadata: - labels: - io.kompose.network/cs3219-project-default: "true" - io.kompose.service: collaboration-service - spec: - containers: - - env: - - name: FRONTEND_ADDRESS - value: "https://www.codeparty.org" - - name: PRISMA_DATABASE_URL - valueFrom: - secretKeyRef: - name: prisma-database-url - key: prisma-database-url - - name: PORT - value: "5003" - - name: TWILIO_ACCOUNT_SID - valueFrom: - secretKeyRef: - name: twilio-account-sid - key: twilio-account-sid - - name: TWILIO_API_KEY - valueFrom: - secretKeyRef: - name: twilio-api-key - key: twilio-api-key - - name: TWILIO_API_SECRET - valueFrom: - secretKeyRef: - name: twilio-api-secret - key: twilio-api-secret - image: asia-southeast1-docker.pkg.dev/peerprep-group11-prod/codeparty-prod-images/collaboration-service:latest - name: collaboration-service - ports: - - containerPort: 5003 - hostPort: 5003 - protocol: TCP - resources: - # You must specify requests for CPU to autoscale - # based on CPU utilization - requests: - cpu: "100m" - restartPolicy: Always -status: {} diff --git a/deployment/gke-prod-manifests/collaboration-service-service.yaml b/deployment/gke-prod-manifests/collaboration-service-service.yaml deleted file mode 100644 index 028a83dc..00000000 --- a/deployment/gke-prod-manifests/collaboration-service-service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - io.kompose.service: collaboration-service - name: collaboration-service - namespace: default -spec: - ports: - - name: "5003" - port: 5003 - targetPort: 5003 - selector: - io.kompose.service: collaboration-service -status: - loadBalancer: {} diff --git a/deployment/gke-prod-manifests/frontend-autoscaling.yaml b/deployment/gke-prod-manifests/frontend-autoscaling.yaml deleted file mode 100644 index 5a178863..00000000 --- a/deployment/gke-prod-manifests/frontend-autoscaling.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: frontend-autoscaling -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: frontend - minReplicas: 1 - maxReplicas: 2 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 50 diff --git a/deployment/gke-prod-manifests/frontend-deployment.yaml b/deployment/gke-prod-manifests/frontend-deployment.yaml deleted file mode 100644 index 4e299d0c..00000000 --- a/deployment/gke-prod-manifests/frontend-deployment.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - io.kompose.service: frontend - name: frontend - namespace: default -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: frontend - strategy: {} - template: - metadata: - labels: - io.kompose.network/cs3219-project-default: "true" - io.kompose.service: frontend - spec: - containers: - - image: asia-southeast1-docker.pkg.dev/peerprep-group11-prod/codeparty-prod-images/frontend:latest - name: frontend - ports: - - containerPort: 3000 - hostPort: 3000 - protocol: TCP - resources: - # You must specify requests for CPU to autoscale - # based on CPU utilization - requests: - cpu: "100m" - restartPolicy: Always -status: {} diff --git a/deployment/gke-prod-manifests/frontend-ingress.yaml b/deployment/gke-prod-manifests/frontend-ingress.yaml deleted file mode 100644 index c99b9e91..00000000 --- a/deployment/gke-prod-manifests/frontend-ingress.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: frontend-ingress - annotations: - networking.gke.io/managed-certificates: gke-managed-cert - kubernetes.io/ingress.class: "gce" -spec: - defaultBackend: - service: - name: frontend - port: - number: 3000 diff --git a/deployment/gke-prod-manifests/frontend-service.yaml b/deployment/gke-prod-manifests/frontend-service.yaml deleted file mode 100644 index a72a5a12..00000000 --- a/deployment/gke-prod-manifests/frontend-service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - io.kompose.service: frontend - name: frontend - namespace: default -spec: - ports: - - name: "3000" - port: 3000 - targetPort: 3000 - selector: - io.kompose.service: frontend -status: - loadBalancer: {} diff --git a/deployment/gke-prod-manifests/gateway-autoscaling.yaml b/deployment/gke-prod-manifests/gateway-autoscaling.yaml deleted file mode 100644 index 8386b65b..00000000 --- a/deployment/gke-prod-manifests/gateway-autoscaling.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: gateway-autoscaling -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: gateway - minReplicas: 1 - maxReplicas: 2 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 50 diff --git a/deployment/gke-prod-manifests/gateway-backend-config.yaml b/deployment/gke-prod-manifests/gateway-backend-config.yaml deleted file mode 100644 index d390dc11..00000000 --- a/deployment/gke-prod-manifests/gateway-backend-config.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: cloud.google.com/v1 -kind: BackendConfig -metadata: - name: gateway-backend-config -spec: - healthCheck: - timeoutSec: 3 - type: HTTP - requestPath: /healthcheck - port: 4000 - customResponseHeaders: - headers: - - "Access-Control-Allow-Origin: https://www.codeparty.org" - - "Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS" diff --git a/deployment/gke-prod-manifests/gateway-deployment.yaml b/deployment/gke-prod-manifests/gateway-deployment.yaml deleted file mode 100644 index b5781d5f..00000000 --- a/deployment/gke-prod-manifests/gateway-deployment.yaml +++ /dev/null @@ -1,57 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - io.kompose.service: gateway - name: gateway - namespace: default -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: gateway - strategy: {} - template: - metadata: - labels: - io.kompose.network/cs3219-project-default: "true" - io.kompose.service: gateway - spec: - containers: - - env: - - name: FIREBASE_SERVICE_ACCOUNT - valueFrom: - secretKeyRef: - name: firebase-service-account - key: firebase-service-account - - name: HTTP_PROXY_PORT - value: "4000" - - name: WS_MATCH_PROXY_PORT - value: "4002" - - name: WS_COLLABORATION_PROXY_PORT - value: "4003" - - name: FRONTEND_ADDRESS - value: "https://www.codeparty.org" - image: asia-southeast1-docker.pkg.dev/peerprep-group11-prod/codeparty-prod-images/gateway:latest - name: gateway - ports: - - containerPort: 4000 - hostPort: 4000 - protocol: TCP - - containerPort: 4002 - hostPort: 4002 - protocol: TCP - - containerPort: 4003 - hostPort: 4003 - protocol: TCP - # Needed for health check - - containerPort: 8080 - hostPort: 8080 - protocol: TCP - resources: - # You must specify requests for CPU to autoscale - # based on CPU utilization - requests: - cpu: "100m" - restartPolicy: Always -status: {} diff --git a/deployment/gke-prod-manifests/gateway-http-ingress.yaml b/deployment/gke-prod-manifests/gateway-http-ingress.yaml deleted file mode 100644 index cb6434ae..00000000 --- a/deployment/gke-prod-manifests/gateway-http-ingress.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: gateway-http-ingress - annotations: - networking.gke.io/managed-certificates: gke-managed-cert - kubernetes.io/ingress.class: "gce" -spec: - defaultBackend: - service: - name: gateway - port: - number: 4000 diff --git a/deployment/gke-prod-manifests/gateway-service.yaml b/deployment/gke-prod-manifests/gateway-service.yaml deleted file mode 100644 index 0d9ffc66..00000000 --- a/deployment/gke-prod-manifests/gateway-service.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - io.kompose.service: gateway - name: gateway - namespace: default - annotations: - cloud.google.com/backend-config: '{"default": "gateway-backend-config"}' -spec: - ports: - - name: "4000" - port: 4000 - targetPort: 4000 - - name: "4002" - port: 4002 - targetPort: 4002 - - name: "4003" - port: 4003 - targetPort: 4003 - # Needed for health check - - name: "8080" - port: 8080 - targetPort: 8080 - selector: - io.kompose.service: gateway -status: - loadBalancer: {} diff --git a/deployment/gke-prod-manifests/gateway-wscollaboration-ingress.yaml b/deployment/gke-prod-manifests/gateway-wscollaboration-ingress.yaml deleted file mode 100644 index b3900773..00000000 --- a/deployment/gke-prod-manifests/gateway-wscollaboration-ingress.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: gateway-wscollaboration-ingress - annotations: - networking.gke.io/managed-certificates: gke-managed-cert - kubernetes.io/ingress.class: "gce" -spec: - defaultBackend: - service: - name: gateway - port: - number: 4003 diff --git a/deployment/gke-prod-manifests/gateway-wsmatch-ingress.yaml b/deployment/gke-prod-manifests/gateway-wsmatch-ingress.yaml deleted file mode 100644 index 5aea8575..00000000 --- a/deployment/gke-prod-manifests/gateway-wsmatch-ingress.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: gateway-wsmatch-ingress - annotations: - networking.gke.io/managed-certificates: gke-managed-cert - kubernetes.io/ingress.class: "gce" -spec: - defaultBackend: - service: - name: gateway - port: - number: 4002 diff --git a/deployment/gke-prod-manifests/gke-managed-cert.yaml b/deployment/gke-prod-manifests/gke-managed-cert.yaml deleted file mode 100644 index 4feaab2e..00000000 --- a/deployment/gke-prod-manifests/gke-managed-cert.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: networking.gke.io/v1 -kind: ManagedCertificate -metadata: - name: gke-managed-cert -spec: - domains: - - www.codeparty.org - - api.codeparty.org - - wsmatch.codeparty.org - - wscollab.codeparty.org diff --git a/deployment/gke-prod-manifests/matching-service-autoscaling.yaml b/deployment/gke-prod-manifests/matching-service-autoscaling.yaml deleted file mode 100644 index 0e4c037d..00000000 --- a/deployment/gke-prod-manifests/matching-service-autoscaling.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: matching-service-autoscaling -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: matching-service - minReplicas: 1 - maxReplicas: 2 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 50 diff --git a/deployment/gke-prod-manifests/matching-service-deployment.yaml b/deployment/gke-prod-manifests/matching-service-deployment.yaml deleted file mode 100644 index ff4afc6d..00000000 --- a/deployment/gke-prod-manifests/matching-service-deployment.yaml +++ /dev/null @@ -1,45 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - io.kompose.service: matching-service - name: matching-service - namespace: default -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: matching-service - strategy: {} - template: - metadata: - labels: - io.kompose.network/cs3219-project-default: "true" - io.kompose.service: matching-service - spec: - containers: - - env: - - name: FRONTEND_ADDRESS - value: "https://www.codeparty.org" - - name: QUESTION_SERVICE_HOSTNAME - value: "question-service" - - name: PRISMA_DATABASE_URL - valueFrom: - secretKeyRef: - name: prisma-database-url - key: prisma-database-url - - name: PORT - value: "5002" - image: asia-southeast1-docker.pkg.dev/peerprep-group11-prod/codeparty-prod-images/matching-service:latest - name: matching-service - ports: - - containerPort: 5002 - hostPort: 5002 - protocol: TCP - resources: - # You must specify requests for CPU to autoscale - # based on CPU utilization - requests: - cpu: "100m" - restartPolicy: Always -status: {} diff --git a/deployment/gke-prod-manifests/matching-service-service.yaml b/deployment/gke-prod-manifests/matching-service-service.yaml deleted file mode 100644 index 09c84100..00000000 --- a/deployment/gke-prod-manifests/matching-service-service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - io.kompose.service: matching-service - name: matching-service - namespace: default -spec: - ports: - - name: "5002" - port: 5002 - targetPort: 5002 - selector: - io.kompose.service: matching-service -status: - loadBalancer: {} diff --git a/deployment/gke-prod-manifests/question-service-autoscaling.yaml b/deployment/gke-prod-manifests/question-service-autoscaling.yaml deleted file mode 100644 index e9f91e48..00000000 --- a/deployment/gke-prod-manifests/question-service-autoscaling.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: question-service-autoscaling -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: question-service - minReplicas: 1 - maxReplicas: 2 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 50 diff --git a/deployment/gke-prod-manifests/question-service-deployment.yaml b/deployment/gke-prod-manifests/question-service-deployment.yaml deleted file mode 100644 index c3492713..00000000 --- a/deployment/gke-prod-manifests/question-service-deployment.yaml +++ /dev/null @@ -1,41 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - io.kompose.service: question-service - name: question-service - namespace: default -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: question-service - strategy: {} - template: - metadata: - labels: - io.kompose.network/cs3219-project-default: "true" - io.kompose.service: question-service - spec: - containers: - - env: - - name: MONGO_ATLAS_URL - valueFrom: - secretKeyRef: - name: mongo-atlas-url - key: mongo-atlas-url - - name: PORT - value: "5004" - image: asia-southeast1-docker.pkg.dev/peerprep-group11-prod/codeparty-prod-images/question-service:latest - name: question-service - ports: - - containerPort: 5004 - hostPort: 5004 - protocol: TCP - resources: - # You must specify requests for CPU to autoscale - # based on CPU utilization - requests: - cpu: "100m" - restartPolicy: Always -status: {} diff --git a/deployment/gke-prod-manifests/question-service-service.yaml b/deployment/gke-prod-manifests/question-service-service.yaml deleted file mode 100644 index 3850127e..00000000 --- a/deployment/gke-prod-manifests/question-service-service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - io.kompose.service: question-service - name: question-service - namespace: default -spec: - ports: - - name: "5004" - port: 5004 - targetPort: 5004 - selector: - io.kompose.service: question-service -status: - loadBalancer: {} diff --git a/deployment/gke-prod-manifests/user-service-autoscaling.yaml b/deployment/gke-prod-manifests/user-service-autoscaling.yaml deleted file mode 100644 index 05061a0b..00000000 --- a/deployment/gke-prod-manifests/user-service-autoscaling.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: user-service-autoscaling -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: user-service - minReplicas: 1 - maxReplicas: 2 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 50 diff --git a/deployment/gke-prod-manifests/user-service-deployment.yaml b/deployment/gke-prod-manifests/user-service-deployment.yaml deleted file mode 100644 index cca09053..00000000 --- a/deployment/gke-prod-manifests/user-service-deployment.yaml +++ /dev/null @@ -1,41 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - io.kompose.service: user-service - name: user-service - namespace: default -spec: - replicas: 1 - selector: - matchLabels: - io.kompose.service: user-service - strategy: {} - template: - metadata: - labels: - io.kompose.network/cs3219-project-default: "true" - io.kompose.service: user-service - spec: - containers: - - env: - - name: PRISMA_DATABASE_URL - valueFrom: - secretKeyRef: - name: prisma-database-url - key: prisma-database-url - - name: PORT - value: "5001" - image: asia-southeast1-docker.pkg.dev/peerprep-group11-prod/codeparty-prod-images/user-service:latest - name: user-service - ports: - - containerPort: 5001 - hostPort: 5001 - protocol: TCP - resources: - # You must specify requests for CPU to autoscale - # based on CPU utilization - requests: - cpu: "100m" - restartPolicy: Always -status: {} diff --git a/deployment/gke-prod-manifests/user-service-service.yaml b/deployment/gke-prod-manifests/user-service-service.yaml deleted file mode 100644 index 37bae945..00000000 --- a/deployment/gke-prod-manifests/user-service-service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - io.kompose.service: user-service - name: user-service - namespace: default -spec: - ports: - - name: "5001" - port: 5001 - targetPort: 5001 - selector: - io.kompose.service: user-service -status: - loadBalancer: {} diff --git a/deployment/prod-dockerfiles/Dockerfile.admin-service b/deployment/prod-dockerfiles/Dockerfile.admin-service index 84ae7943..7a67b32e 100644 --- a/deployment/prod-dockerfiles/Dockerfile.admin-service +++ b/deployment/prod-dockerfiles/Dockerfile.admin-service @@ -17,8 +17,5 @@ RUN yarn prisma generate # Compile service from TypeScript to JavaScript RUN yarn build -# Re-install production-only dependencies -RUN yarn install --frozen-lockfile --production --cwd /app - # Run service CMD [ "yarn", "workspace", "admin-service", "start" ] diff --git a/deployment/prod-dockerfiles/Dockerfile.collaboration-service b/deployment/prod-dockerfiles/Dockerfile.collaboration-service deleted file mode 100644 index 85457b6e..00000000 --- a/deployment/prod-dockerfiles/Dockerfile.collaboration-service +++ /dev/null @@ -1,28 +0,0 @@ -# Use the base image -FROM peerprep-base:latest - -# Copy utils -COPY utils /app/utils/ - -# Set working directory -WORKDIR /app/services/collaboration-service - -# Copy the entire services directory and prisma -COPY services/collaboration-service /app/services/collaboration-service -COPY prisma ./prisma/ -COPY utils /app/utils/ - -# Install all dependencies using Yarn Workspaces -RUN yarn install --frozen-lockfile --cwd /app - -# Generate the prisma client -RUN yarn prisma generate - -# Compile service from TypeScript to JavaScript -RUN yarn build - -# Re-install production-only dependencies -RUN yarn install --frozen-lockfile --production --cwd /app - -# Run service -CMD [ "yarn", "workspace", "collaboration-service", "start" ] diff --git a/deployment/prod-dockerfiles/Dockerfile.frontend b/deployment/prod-dockerfiles/Dockerfile.frontend index 974aca87..243f27e3 100644 --- a/deployment/prod-dockerfiles/Dockerfile.frontend +++ b/deployment/prod-dockerfiles/Dockerfile.frontend @@ -23,20 +23,8 @@ RUN yarn prisma generate ARG NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG_ARG ENV NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG=$NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG_ARG -ARG NEXT_PUBLIC_HTTP_PROXY_GATEWAY_ADDRESS_ARG -ENV NEXT_PUBLIC_HTTP_PROXY_GATEWAY_ADDRESS=$NEXT_PUBLIC_HTTP_PROXY_GATEWAY_ADDRESS_ARG - -ARG NEXT_PUBLIC_WS_MATCH_PROXY_GATEWAY_ADDRESS_ARG -ENV NEXT_PUBLIC_WS_MATCH_PROXY_GATEWAY_ADDRESS=$NEXT_PUBLIC_WS_MATCH_PROXY_GATEWAY_ADDRESS_ARG - -ARG NEXT_PUBLIC_WS_COLLABORATION_PROXY_GATEWAY_ADDRESS_ARG -ENV NEXT_PUBLIC_WS_COLLABORATION_PROXY_GATEWAY_ADDRESS=$NEXT_PUBLIC_WS_COLLABORATION_PROXY_GATEWAY_ADDRESS_ARG - RUN yarn build -# Re-install production-only dependencies -RUN yarn install --frozen-lockfile --production --cwd /app - # Start command for the frontend CMD [ "yarn", "workspace", "frontend", "start" ] diff --git a/deployment/prod-dockerfiles/Dockerfile.gateway b/deployment/prod-dockerfiles/Dockerfile.gateway deleted file mode 100644 index a243c5c8..00000000 --- a/deployment/prod-dockerfiles/Dockerfile.gateway +++ /dev/null @@ -1,24 +0,0 @@ -# Use the base image -FROM peerprep-base:latest - -# Set working directory -WORKDIR /app/services/gateway - -# Copy the entire services directory and prisma -COPY services/gateway /app/services/gateway -COPY prisma ./prisma/ - -# Install all dependencies using Yarn Workspaces -RUN yarn install --frozen-lockfile --cwd /app - -# Generate the prisma client -RUN yarn prisma generate - -# Compile service from TypeScript to JavaScript -RUN yarn build - -# Re-install production-only dependencies -RUN yarn install --frozen-lockfile --production --cwd /app - -# Run service -CMD [ "yarn", "workspace", "gateway", "start" ] diff --git a/deployment/prod-dockerfiles/Dockerfile.matching-service b/deployment/prod-dockerfiles/Dockerfile.matching-service index 91a5ffc4..07388d8a 100644 --- a/deployment/prod-dockerfiles/Dockerfile.matching-service +++ b/deployment/prod-dockerfiles/Dockerfile.matching-service @@ -17,8 +17,5 @@ RUN yarn prisma generate # Compile service from TypeScript to JavaScript RUN yarn build -# Re-install production-only dependencies -RUN yarn install --frozen-lockfile --production --cwd /app - # Run service CMD [ "yarn", "workspace", "matching-service", "start" ] diff --git a/deployment/prod-dockerfiles/Dockerfile.question-service b/deployment/prod-dockerfiles/Dockerfile.question-service index 375efea8..049a7186 100644 --- a/deployment/prod-dockerfiles/Dockerfile.question-service +++ b/deployment/prod-dockerfiles/Dockerfile.question-service @@ -17,8 +17,5 @@ RUN yarn prisma generate # Compile service from TypeScript to JavaScript RUN yarn build -# Re-install production-only dependencies -RUN yarn install --frozen-lockfile --production --cwd /app - # Run service CMD [ "yarn", "workspace", "question-service", "start" ] diff --git a/deployment/prod-dockerfiles/Dockerfile.user-service b/deployment/prod-dockerfiles/Dockerfile.user-service index 49d84fb2..544edc19 100644 --- a/deployment/prod-dockerfiles/Dockerfile.user-service +++ b/deployment/prod-dockerfiles/Dockerfile.user-service @@ -17,8 +17,5 @@ RUN yarn prisma generate # Compile service from TypeScript to JavaScript RUN yarn build -# Re-install production-only dependencies -RUN yarn install --frozen-lockfile --production --cwd /app - # Run service CMD [ "yarn", "workspace", "user-service", "start" ] diff --git a/docker-compose.yml b/docker-compose.yml index 0a9c5259..d95048a2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,20 +24,6 @@ services: PRISMA_DATABASE_URL: ${PRISMA_DATABASE_URL} QUESTION_SERVICE_HOSTNAME: "question-service" - collaboration-service: - build: - context: . - dockerfile: deployment/prod-dockerfiles/Dockerfile.collaboration-service - container_name: collaboration-service - ports: - - "5003:5003" - environment: - PORT: 5003 - TWILIO_ACCOUNT_SID: ${TWILIO_ACCOUNT_SID} - TWILIO_API_KEY: ${TWILIO_API_KEY} - TWILIO_API_SECRET: ${TWILIO_API_SECRET} - PRISMA_DATABASE_URL: ${PRISMA_DATABASE_URL} - question-service: build: context: . @@ -60,31 +46,12 @@ services: PORT: 5005 FIREBASE_SERVICE_ACCOUNT: ${FIREBASE_SERVICE_ACCOUNT} - gateway: - build: - context: . - dockerfile: deployment/prod-dockerfiles/Dockerfile.gateway - container_name: gateway - ports: - - "4000:4000" - - "4002:4002" - - "4003:4003" - environment: - HTTP_PROXY_PORT: 4000 - WS_MATCH_PROXY_PORT: 4002 - WS_COLLABORATION_PROXY_PORT: 4003 - FIREBASE_SERVICE_ACCOUNT: ${FIREBASE_SERVICE_ACCOUNT} - FRONTEND_ADDRESS: "http://localhost:3000" - frontend: build: context: . dockerfile: deployment/prod-dockerfiles/Dockerfile.frontend args: NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG_ARG: ${NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG} - NEXT_PUBLIC_HTTP_PROXY_GATEWAY_ADDRESS_ARG: "http://localhost:4000/" - NEXT_PUBLIC_WS_MATCH_PROXY_GATEWAY_ADDRESS_ARG: "http://localhost:4002/" - NEXT_PUBLIC_WS_COLLABORATION_PROXY_GATEWAY_ADDRESS_ARG: "http://localhost:4003/" container_name: frontend ports: - "3000:3000" diff --git a/frontend/src/gateway-address/gateway-address.ts b/frontend/src/gateway-address/gateway-address.ts index 22a736d6..9bff2917 100644 --- a/frontend/src/gateway-address/gateway-address.ts +++ b/frontend/src/gateway-address/gateway-address.ts @@ -5,20 +5,10 @@ * - Leave NEXT_PUBLIC_GATEWAY_ADDRESS empty for dev environments * - For prod, pass in a separate address to NEXT_PUBLIC_GATEWAY_ADDRESS */ -const httpProxyGatewayAddress = - process.env.NEXT_PUBLIC_HTTP_PROXY_GATEWAY_ADDRESS || - "http://localhost:4000/"; -export const wsMatchProxyGatewayAddress = - process.env.NEXT_PUBLIC_WS_MATCH_PROXY_GATEWAY_ADDRESS || - "http://localhost:4002"; -export const wsCollaborationProxyGatewayAddress = - process.env.NEXT_PUBLIC_WS_COLLABORATION_PROXY_GATEWAY_ADDRESS || - "http://localhost:4003"; +export const wsMatchProxyGatewayAddress = "http://localhost:5002"; -export const userApiPathAddress = httpProxyGatewayAddress + "api/user-service/"; +export const userApiPathAddress = "http://localhost:5001/api/user-service/"; export const questionApiPathAddress = - httpProxyGatewayAddress + "api/question-service/"; + "http://localhost:5004/api/question-service/"; export const matchApiPathAddress = - httpProxyGatewayAddress + "api/matching-service/"; -export const collaborationApiPathAddress = - httpProxyGatewayAddress + "api/collaboration-service/"; + "http://localhost:5002/api/matching-service/"; diff --git a/frontend/src/hooks/useCollaboration.tsx b/frontend/src/hooks/useCollaboration.tsx deleted file mode 100644 index 0b678849..00000000 --- a/frontend/src/hooks/useCollaboration.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import { useEffect, useState, useRef, use, useContext } from "react"; -import { io, Socket } from "socket.io-client"; -import { debounce } from "lodash"; -import { - TextOperationSetWithCursor, - createTextOpFromTexts, -} from "../../../utils/shared-ot"; -import { TextOp } from "ot-text-unicode"; -import { Room, connect } from "twilio-video"; -import { wsCollaborationProxyGatewayAddress } from "@/gateway-address/gateway-address"; -import { AuthContext } from "@/contexts/AuthContext"; -import { toast } from "react-toastify"; -import { useRouter } from "next/router"; -import { fetchRoomData } from "@/pages/api/collaborationHandler"; - -type UseCollaborationProps = { - roomId: string; - userId: string; - disableVideo?: boolean; -}; - -enum SocketEvents { - ROOM_JOIN = "api/collaboration-service/room/join", - ROOM_UPDATE = "api/collaboration-service/room/update", - ROOM_SAVE = "api/collaboration-service/room/save", - ROOM_LOAD = "api/collaboration-service/room/load", - QUESTION_SET = "api/collaboration-service/question/set", -} - -var vers = 0; - -const useCollaboration = ({ - roomId, - userId, - disableVideo, -}: UseCollaborationProps) => { - const [socket, setSocket] = useState(null); - const [text, setText] = useState("#Write your solution here"); - const [cursor, setCursor] = useState( - "#Write your solution here".length - ); - const [room, setRoom] = useState(null); // twilio room - const [questionId, setQuestionId] = useState(""); - const textRef = useRef(text); - const cursorRef = useRef(cursor); - const prevCursorRef = useRef(cursor); - const prevTextRef = useRef(text); - const awaitingAck = useRef(false); // ack from sending update - const awaitingSync = useRef(false); // synced with server - const twilioTokenRef = useRef(""); - const { user: currentUser } = useContext(AuthContext); - - const router = useRouter(); - const { id } = router.query; - - // useEffect(() => { - // if (id && currentUser) { - // try { - // const response = fetchRoomData(id?.toString(), currentUser); - // response.then((res) => { - // if (res.message === "Room exists") { - // console.log(res); - // setQuestionId(res.questionId); - // } - // }); - // } catch (err) { - // toast.error((err as Error).message); - // } - // } - // }, [id, currentUser]); - - useEffect(() => { - if (currentUser && roomId) { - currentUser.getIdToken(true).then((token) => { - const socketConnection = io(wsCollaborationProxyGatewayAddress, { - extraHeaders: { - "User-Id-Token": token, - }, - }); - setSocket(socketConnection); - - socketConnection.emit(SocketEvents.ROOM_JOIN, roomId, userId); - if ( - questionId !== "" && - questionId !== undefined && - questionId !== null - ) { - socketConnection.emit(SocketEvents.QUESTION_SET, questionId); - } - - socketConnection.on("twilio-token", (token: string) => { - twilioTokenRef.current = token; - if (disableVideo) return; - connect(token, { - name: roomId, - audio: true, - video: { width: 640, height: 480, frameRate: 24 }, - }) - .then((room) => { - console.log("Connected to Room"); - room.localParticipant.videoTracks.forEach((publication) => { - publication.track.disable(); - }); - room.localParticipant.audioTracks.forEach((publication) => { - publication.track.disable(); - }); - setRoom(room); - }) - .catch((err) => { - console.log(err, token, userId, roomId); - }); - }); - - socketConnection.on( - SocketEvents.ROOM_UPDATE, - ({ - version, - text, - cursor, - }: { - version: number; - text: string; - cursor: number | undefined | null; - }) => { - prevCursorRef.current = cursorRef.current; - console.log("prevCursor: " + prevCursorRef.current); - - console.log("cursor: " + cursor); - - console.log("Update vers to " + version); - vers = version; - - if (awaitingAck.current) return; - - textRef.current = text; - prevTextRef.current = text; - setText(text); - if (cursor && cursor > -1) { - console.log("Update cursor to " + cursor); - cursorRef.current = cursor; - setCursor(cursor); - } else { - cursorRef.current = prevCursorRef.current; - cursor = prevCursorRef.current; - console.log("Update cursor to " + prevCursorRef.current); - setCursor(prevCursorRef.current); - } - awaitingSync.current = false; - } - ); - - return () => { - socketConnection.disconnect(); - if (room) { - room.disconnect(); - } - }; - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [roomId, userId, questionId]); - - useEffect(() => { - textRef.current = text; - }, [text]); - - useEffect(() => { - cursorRef.current = cursor; - }, [cursor]); - - useEffect(() => { - if (!socket) return; - - if (prevTextRef.current === textRef.current) return; - - if (awaitingAck.current || awaitingSync.current) return; - - awaitingAck.current = true; - - console.log("prevtext: " + prevTextRef.current); - console.log("currenttext: " + textRef.current); - console.log("version: " + vers); - const textOp: TextOp = createTextOpFromTexts( - prevTextRef.current, - textRef.current - ); - - prevTextRef.current = textRef.current; - - console.log(textOp); - - const textOperationSet: TextOperationSetWithCursor = { - version: vers, - operations: textOp, - cursor: cursorRef.current, - }; - - socket.emit(SocketEvents.ROOM_UPDATE, textOperationSet, () => { - awaitingAck.current = false; - }); - }, [text, socket]); - - const disconnect = () => { - if (socket) { - socket.disconnect(); - } - }; - - return { - text, - setText, - cursor, - setCursor, - room, - questionId, - setQuestionId, - disconnect, - }; -}; - -export default useCollaboration; diff --git a/frontend/src/pages/api/collaborationHandler.ts b/frontend/src/pages/api/collaborationHandler.ts deleted file mode 100644 index c1789a26..00000000 --- a/frontend/src/pages/api/collaborationHandler.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { collaborationApiPathAddress } from "@/gateway-address/gateway-address"; - -export const fetchRoomData = async (roomId: string, user: any) => { - try { - const url = `${collaborationApiPathAddress}room/${roomId}`; - const idToken = await user.getIdToken(true); - - const response = await fetch(url, { - method: "GET", - mode: "cors", - headers: { - "Content-Type": "application/json", - "User-Id-Token": idToken, - }, - }); - - const data = await response.json(); - - if (data && data.room_id) { - return { - message: data.message, - roomId: data.room_id, - questionId: data.questionId, - info: data.info, - }; - } else { - throw new Error("Invalid data format from the server"); - } - } catch (error) { - console.error("There was an error fetching the room data", error); - throw error; - } -}; diff --git a/frontend/src/pages/interviews/match-found.tsx b/frontend/src/pages/interviews/match-found.tsx index bc2bdda9..a21559e3 100644 --- a/frontend/src/pages/interviews/match-found.tsx +++ b/frontend/src/pages/interviews/match-found.tsx @@ -88,7 +88,7 @@ export default function MatchFound() { }; const onClickAccept = () => { - router.push(`/room/${match?.roomId}`); + router.push(`/`); }; return ( diff --git a/frontend/src/pages/room/[id].tsx b/frontend/src/pages/room/[id].tsx deleted file mode 100644 index 118b480a..00000000 --- a/frontend/src/pages/room/[id].tsx +++ /dev/null @@ -1,187 +0,0 @@ -import CodeEditor from "@/components/room/code-editor"; -import Description from "@/components/room/description"; -import useCollaboration from "@/hooks/useCollaboration"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { TypographyBody } from "@/components/ui/typography"; -import { useRouter } from "next/router"; -import VideoRoom from "../../components/room/video-room"; -import { Difficulty, Question } from "../../types/QuestionTypes"; -import { Match } from "../../types/MatchTypes"; -import { useQuestions } from "@/hooks/useQuestions"; -import { useMatch } from "@/hooks/useMatch"; -import { useContext, useEffect, useState } from "react"; -import { MrMiyagi } from "@uiball/loaders"; -import { useMatchmaking } from "@/hooks/useMatchmaking"; -import Solution from "@/components/room/solution"; -import { AuthContext } from "@/contexts/AuthContext"; -import { toast } from "react-toastify"; - -export default function Room() { - const router = useRouter(); - const roomId = router.query.id as string; - const { user: currentUser } = useContext(AuthContext); - const userId = (currentUser?.uid as string) || "user1"; - const disableVideo = - (router.query.disableVideo as string)?.toLowerCase() === "true"; - - const { - text, - setText, - cursor, - setCursor, - room, - questionId, - setQuestionId, - disconnect, - } = useCollaboration({ - roomId: roomId as string, - userId, - disableVideo, - }); - - const [question, setQuestion] = useState(null); - const [loading, setLoading] = useState(true); // to be used later for loading states - - const { fetchQuestion, fetchRandomQuestion } = useQuestions(); - const { updateQuestionIdInMatch } = useMatch(); - const { match, leaveMatch } = useMatchmaking(); - - useEffect(() => { - if (match && match.questionId !== null) { - const questionId = match.questionId; - setQuestionId(questionId); - } - - if (questionId !== "") { - fetchQuestion(questionId).then((fetchQuestion) => { - if (fetchQuestion != null) { - setQuestion(fetchQuestion); - } - }); - } - - if (!match) { - // leave room and redirect to interviews page - leaveMatch(); - toast.info("Other user has left"); - router.push("/interviews"); - } - - setLoading(false); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [match, questionId]); - - function handleSwapQuestionClick(): void { - if (match) { - setLoading(true); - const difficulty = (match.chosenDifficulty || "easy") as Difficulty; - fetchRandomQuestion(difficulty) - .then((question) => { - if (question) { - updateQuestionIdInMatch(roomId, question.id); - setQuestion(question); - setQuestionId(question.id); - } - }) - .catch((err) => { - console.log(err); - router.push("/"); - }) - .finally(() => { - setLoading(false); - }); - } - } - - function onLeaveRoomClick(): void { - disconnect(); - leaveMatch(); - router.push("/interviews"); - } - - return ( -
- {!router.isReady ? ( -
- -
- ) : ( -
-
-
- - - - Description - - - Solution - - - - {loading ? ( -
- -
- ) : question !== null ? ( - - ) : ( -
- -
- )} -
- {loading ? ( -
- -
- ) : question != null && "solution" in question ? ( - - - - ) : ( -
- -
- )} -
-
- -
-
- -
-
- )} -
- ); -} diff --git a/frontend/src/providers/MatchmakingProvider.tsx b/frontend/src/providers/MatchmakingProvider.tsx index a5b7ea12..c5b3aeea 100644 --- a/frontend/src/providers/MatchmakingProvider.tsx +++ b/frontend/src/providers/MatchmakingProvider.tsx @@ -83,7 +83,7 @@ export const MatchmakingProvider: React.FC = ({ router.route !== "/interviews/match-found" && router.route !== "/interviews/find-match" ) { - router.push(`/room/${match?.roomId}`); + router.push(`/`); } }, [match]); diff --git a/services/collaboration-service/.gitignore b/services/collaboration-service/.gitignore deleted file mode 100644 index ce597ce5..00000000 --- a/services/collaboration-service/.gitignore +++ /dev/null @@ -1,64 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Ignore built ts files -dist/**/* - -# Dependency directories -node_modules/ -jspm_packages/ - -# Typescript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - -# next.js build output -.next diff --git a/services/collaboration-service/README.md b/services/collaboration-service/README.md deleted file mode 100644 index f6777371..00000000 --- a/services/collaboration-service/README.md +++ /dev/null @@ -1,39 +0,0 @@ -## Collaboration Service - -### Docs - -Visit http://localhost:5003/docs for REST API docs. - -WebSocket Events (refer to `routes/room.ts#initSocketListeners`): - -```typescript - socket.on("/room/join", (room_id: string, user_id: string) => {...}; - - socket.on("/room/update", (text: string) => roomUpdate(io, socket, room_id, text)); - - socket.on("/room/save", (text: string) => saveText(room_id, text)); - - socket.on("/room/load", () => loadTextFromDb(io, socket, room_id)); - - socket.disconnect() -``` - -- /room/join - Join a room and get twilio video access token, same room_id gets connected together. user_id if get details of room. -- /room/update - Update the room after text change -- /room/save - Save current text -- /room/load - Load previously saved text (calls /room/update after retrieving text from db) - -On disconnect, removes users from session db and change status to inactive if no users are present. -To reconnect, simply join the same room again. - -### Demo - -To test out or see implementation example: See `demo.html`. The steps below assume you are doing localhost development -and testing. - -Visit http://localhost:5003/demo/?room=1&user=user1 -to set room and user. Open multiple tabs, and those with the same room will have same content. - -To test using REST API, -Visit http://localhost:5003/demo/?room=1&user=user1&api=rest -to use GET API instead of socket emitters to join room. (Possible concurrency risk?) diff --git a/services/collaboration-service/package.json b/services/collaboration-service/package.json deleted file mode 100644 index 1e8a801d..00000000 --- a/services/collaboration-service/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "collaboration-service", - "version": "0.0.0", - "private": true, - "main": "src/app.ts", - "scripts": { - "lint": "eslint src/**/*.{ts,js} swagger-doc-gen.ts", - "build": "yarnpkg run swagger-autogen && tsc", - "start": "node ./dist/src/app.js", - "dev": "ts-node-dev src/app.ts", - "dev:local": "dotenv -e ../../.env -c development -- yarnpkg dev", - "swagger-autogen": "ts-node swagger-doc-gen.ts" - }, - "dependencies": { - "body-parser": "^1.20.2", - "cookie-parser": "~1.4.4", - "debug": "~2.6.9", - "diff-match-patch": "^1.0.5", - "express-openapi": "^12.1.3", - "json0-ot-diff": "^1.1.2", - "morgan": "~1.9.1", - "ot-json1": "^1.0.2", - "ot-text-unicode": "^4.0.0", - "socket.io": "^4.7.2", - "twilio": "^4.18.1", - "uuid": "^9.0.1" - }, - "devDependencies": { - "@types/cookie-parser": "^1.4.4", - "@types/cors": "^2.8.14", - "@types/diff-match-patch": "^1.0.34", - "@types/morgan": "^1.9.5", - "@types/socket.io": "^3.0.2", - "@types/uuid": "^9.0.4" - } -} diff --git a/services/collaboration-service/src/app.ts b/services/collaboration-service/src/app.ts deleted file mode 100644 index b8d1ee58..00000000 --- a/services/collaboration-service/src/app.ts +++ /dev/null @@ -1,42 +0,0 @@ -import express, { Express } from "express"; -import path from "path"; -import cookieParser from "cookie-parser"; -import logger from "morgan"; -import http, { Server as HTTPServer } from "http"; -import { Server as SocketIOServer } from "socket.io"; -import swaggerUi from "swagger-ui-express"; -import swaggerFile from "./swagger-output.json"; -import bodyParser from "body-parser"; -import roomRouter, { roomApiRouter } from "./routes/room"; -import demoRouter from "./routes/demo"; - -const app: Express = express(); -const server: HTTPServer = http.createServer(app); -const socketIoOptions: any = { - cors: { - origin: process.env.FRONTEND_ADDRESS || "http://localhost:3000", - methods: ["GET", "POST"], - }, -}; -const io: SocketIOServer = new SocketIOServer(server, socketIoOptions); - -const PORT: number = parseInt(process.env.PORT || "5003"); - -/* Middlewares */ -app.use(logger("dev")); -app.use(express.json()); -app.use(bodyParser.json()); -app.use(express.urlencoded({ extended: false })); -app.use(cookieParser()); -app.use(express.static(path.join(__dirname, "public"))); - -/* Routers */ -app.use("/demo", demoRouter); -app.use("/api/collaboration-service/room", roomRouter(io)); -app.use("/api/collaboration-service/room", roomApiRouter); - -app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerFile)); - -server.listen(PORT, () => { - console.log(`collaboration-service listening on port ${PORT}`); -}); diff --git a/services/collaboration-service/src/db/prisma-db.ts b/services/collaboration-service/src/db/prisma-db.ts deleted file mode 100644 index c5b8b059..00000000 --- a/services/collaboration-service/src/db/prisma-db.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { AppUser, PrismaClient, Room } from "@prisma/client"; - -const prisma = new PrismaClient(); - -export async function isRoomExists(room_id: string) { - const room = await prisma.room.findFirst({ - where: { - room_id: room_id, - }, - }); - return room != null; -} - -export async function getRoom(room_id: string): Promise { - const room = await prisma.room.findUnique({ - where: { - room_id: room_id, - }, - }); - return room!; -} - -export async function getRoomText(room_id: string): Promise { - const room = await prisma.room.findUnique({ - where: { - room_id: room_id, - }, - }); - if (room) { - return room.text; - } else { - return ""; - } -} - -export async function getSavedRoomText( - room_id: string -): Promise { - const room = await prisma.room.findUnique({ - where: { - room_id: room_id, - }, - }); - if (room) { - return room.saved_text; - } else { - return null; - } -} - -export async function updateRoomStatus(room_id: string): Promise { - const room = await prisma.room.findUnique({ - where: { - room_id: room_id, - }, - }); - if (!room) return; - - if (room.active_users.length === 0) { - room.status = "inactive"; - saveAttempt(room_id); - } else { - room.status = "active"; - } - await prisma.room.update({ - where: { - room_id: room_id, - }, - data: { - status: room.status, - }, - }); -} - -export async function saveAttempt(room_id: string): Promise { - const room = await prisma.room.findUnique({ - where: { - room_id: room_id, - }, - }); - - const attempt_id = room!.attempt_id; - const answer = room!.text; - const question_id = room!.question_id ?? ""; - - const users: AppUser[] = await prisma.appUser.findMany({ - where: { - uid: { - in: room!.users, - }, - }, - }); - - if (attempt_id) { - await prisma.attempt.update({ - where: { - id: attempt_id, - }, - data: { - users: { - connect: users.map((user) => ({ - uid: user.uid as string, - })), - }, - answer: answer, - time_saved_at: new Date(), - }, - }); - return; - } - - await prisma.attempt.create({ - data: { - users: { - connect: users.map((user) => ({ - uid: user.uid as string, - })), - }, - question_id: question_id, - answer: answer, - room_id: room_id, - room: { - connect: { - room_id: room_id, - }, - }, - }, - }); -} - -export async function setRoomQuestion( - room_id: string, - question_id: string -): Promise { - await prisma.room.update({ - where: { - room_id: room_id, - }, - data: { - question_id: question_id, - }, - }); -} - -export async function createOrUpdateRoomWithUser( - room_id: string, - user_id: string -): Promise { - let users: string[] = []; - let active_users: string[] = []; - const room = await prisma.room.findUnique({ - where: { - room_id: room_id, - }, - select: { - users: true, - active_users: true, - }, - }); - if (room) { - users = room.users; - active_users = room.active_users; - if (users.indexOf(user_id) === -1) { - users.push(user_id); - } - if (active_users.indexOf(user_id) === -1) { - active_users.push(user_id); - } - } - - await prisma.room.upsert({ - where: { - room_id: room_id, - }, - update: { - status: "active", - users: { - set: users, - }, - active_users: { - set: active_users, - }, - }, - create: { - room_id: room_id, - text: "", - status: "active", - users: [user_id], - active_users: [user_id], - }, - }); -} - -export async function updateRoomText( - room_id: string, - text: string -): Promise { - if (room_id == null) return; - await prisma.room.update({ - where: { - room_id: room_id, - }, - data: { - text: text, - }, - }); -} - -export async function saveRoomText( - room_id: string, - text: string -): Promise { - await prisma.room.update({ - where: { - room_id: room_id, - }, - data: { - text: text, - saved_text: text, - }, - }); -} - -export async function removeUserFromRoom( - room_id: string, - user_id: string -): Promise { - const existingRoom = await prisma.room.findUnique({ - where: { - room_id: room_id, - }, - }); - - if (!existingRoom) return; - - const userIndex = existingRoom.active_users.indexOf(user_id); - - if (userIndex > -1) { - existingRoom.active_users.splice(userIndex, 1); - - await prisma.room.update({ - where: { - room_id: room_id, - }, - data: { - active_users: { - set: existingRoom.active_users, - }, - }, - }); - } -} diff --git a/services/collaboration-service/src/ot.ts b/services/collaboration-service/src/ot.ts deleted file mode 100644 index 00fba37c..00000000 --- a/services/collaboration-service/src/ot.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { diff_match_patch } from "diff-match-patch"; -import { type, insert, remove, TextOp } from "ot-text-unicode"; - -export interface TextOperationSet { - version: number; - operations: TextOp; -} - -export interface TextOperationSetWithCursor extends TextOperationSet { - cursor?: number; -} - -class CircularArray { - private array: Array; - private last: number; // index of last element - - constructor(capacity: number) { - this.array = new Array(capacity); - this.last = -1; - } - - public add(value: T): void { - this.last = (this.last + 1) % this.array.length; - this.array[this.last] = value; - } - - public getLatest(): T { - return this.array[this.last]; - } - - public search(predicate: (value: T) => boolean): T | null { - for (let i = 0; i < this.array.length; i++) { - const index = (this.last - i) % this.array.length; - if (predicate(this.array[index])) { - return this.array[index]; - } - } - return null; - } - - public reduceFromMatchedPredicateToLatest( - predicate: (value: T) => boolean, - callbackFn: (previousValue: T, currentValue: T) => T, - initialValue: T - ): T { - for (let i = 0; i < this.array.length; i++) { - const index = (this.last - i + this.array.length) % this.array.length; - if (predicate(this.array[index])) { - const startIndex = index; - const endIndex = this.last; - if (startIndex <= endIndex) { - return this.array - .slice(startIndex, endIndex + 1) - .reduce(callbackFn, initialValue); - } else { - return this.array - .slice(startIndex) - .concat(this.array.slice(0, endIndex + 1)) - .reduce(callbackFn, initialValue); - } - } - } - const start = (this.last + 1) % this.array.length; - return this.array - .slice(start) - .concat(this.array.slice(0, this.last + 1)) - .reduce(callbackFn, initialValue); - } - - public get length(): number { - return this.array.length; - } -} - -export class OpHistoryMap { - private map: Record> = {}; - - public add(room_id: string, opHistory: TextOperationSet): void { - if (!this.map[room_id]) { - this.map[room_id] = new CircularArray(10); - } - this.map[room_id].add(opHistory); - } - - public getLatest(room_id: string): TextOperationSet | null { - if (!this.map[room_id]) { - return null; - } - return this.map[room_id].getLatest(); - } - - public getCombinedTextOpFromVersionToLatest( - room_id: string, - version: number - ): TextOp { - const room = this.map[room_id]; - const latestVersion = room.getLatest().version; - - if (version - 1 === latestVersion) { - return room.getLatest()!.operations; - } - - // Combine operations from the given version to the latest version - return room.reduceFromMatchedPredicateToLatest( - (opHistory) => { - return version === opHistory.version; - }, - (x, y) => { - return { - operations: type.compose(x.operations, y.operations), - version: y.version, - }; - }, - { operations: [], version: 0 } - ).operations; - } - - public checkIfLatestVersion(room_id: string, version: number): boolean { - if (!this.map[room_id]) { - return true; - } - const latest = this.map[room_id].getLatest(); - if (!latest) { - return true; - } - return latest.version === version; - } - - public search(room_id: string, version: number): TextOperationSet | null { - if (!this.map[room_id]) { - return null; - } - return this.map[room_id].search( - (opHistory) => opHistory.version === version - ); - } -} - -export function createTextOpFromTexts(text1: string, text2: string): TextOp { - const dmp = new diff_match_patch(); - const diffs = dmp.diff_main(text1, text2); - //dmp.diff_cleanupSemantic(diffs); - - var textop: TextOp = []; - - var skipn: number = 0; - - for (const [operation, text] of diffs) { - if (operation === 0) { - skipn += text.length; - } else if (operation === -1) { - textop = [...textop, ...remove(skipn, text)]; - skipn = 0; - } else { - textop = [...textop, ...insert(skipn, text)]; - skipn = 0; - } - } - return textop; -} - -/** - * Returns transformed operations - * @param latestOp Text 1 to 3 - * @param mergedOp Text 1 to 2 - * @returns (transformed Text 1 to 3, transformed Text 1 to 2) - * Transformed text 1 to 3 to apply to text 2 - * Transformed text 1 to 2 to apply to text 3 - */ -export function getTransformedOperations(latestOp: TextOp, mergedOp: TextOp) { - return [ - type.transform(latestOp, mergedOp, "left"), - type.transform(mergedOp, latestOp, "right"), - ]; -} - -export function transformPosition(cursor: number, op: TextOp): number { - return type.transformPosition(cursor, op); -} - -function test() { - const text1 = "hello world"; - const text2 = "good day hi everyone and the world"; - const text3 = "good morning to the world and all who are in it"; - const expected = - "hi everyone good morning to the world and all who are in it"; /// or some gibberish similiar to this - - const history_db = new OpHistoryMap(); - - history_db.add("room1", { - version: 0.1, - operations: insert(0, text1), - }); - - /// Text 3 sent on version 0.1 - history_db.search("room1", 0.1); - - const text1to2op = createTextOpFromTexts(text1, text2); - console.log(text1to2op); - - history_db.add("room1", { - version: 0.2, - operations: text1to2op, - }); - - const text1to3op = createTextOpFromTexts(text1, text3); - console.log(text1to3op); - - const newOp = type.transform(text1to3op, text1to2op, "left"); - console.log(newOp); - console.log(type.apply(text2, newOp)); - - const newOp2 = type.transform(text1to2op, text1to3op, "right"); - console.log(newOp2); - console.log(type.apply(text3, newOp2)); - - // favour text1to3op over text1to2op on side param - // outcome is same - - console.log("------------------"); - - //Compose - const x = createTextOpFromTexts("Hi", "hai"); - const y = createTextOpFromTexts("hai", "hbaye"); - console.log(x); - console.log(y); - const z = type.compose(x, y); - console.log(z); - console.log(type.apply("Hi", z)); - - const z2 = type.compose(y, x); - console.log(z2); - console.log(type.apply("Hi", z2)); - - console.log("------------------"); - - console.log(text1to2op); - console.log(text1to3op); - const newOp3 = type.compose(text1to2op, text1to3op); - console.log(newOp3); - console.log(type.apply(text1, newOp3)); -} - -if (require.main === module) { - test(); -} diff --git a/services/collaboration-service/src/routes/demo.html b/services/collaboration-service/src/routes/demo.html deleted file mode 100644 index e22d18a7..00000000 --- a/services/collaboration-service/src/routes/demo.html +++ /dev/null @@ -1,136 +0,0 @@ - - - - - Code Collaboration Room - - -

Text Collaboration Room

-

Enter in textbox the TextOperation in this format:

-
    -
  • - Ops are lists of components which iterate over the document. Components - are either: -
  • -
      -
    • A number N: Skip N characters in the original document
    • -
    • "str": Insert "str" at the current position in the document
    • -
    • - {d:N}: Delete N characters at the current position in the document -
    • -
    • - {d:"str"}: Delete "str" at the current position in the document. This - is equivalent to {d:N} but provides extra information for operation - invertability. -
    • -
    -
  • Eg: [3, 'hi', 5, {d:8}]
  • -
- - - - - - - - - diff --git a/services/collaboration-service/src/routes/demo.ts b/services/collaboration-service/src/routes/demo.ts deleted file mode 100644 index 223db9ee..00000000 --- a/services/collaboration-service/src/routes/demo.ts +++ /dev/null @@ -1,9 +0,0 @@ -import express, { Request, Response } from "express"; - -const router = express.Router(); - -router.get("/", (req: Request, res: Response) => { - res.sendFile(__dirname + "/demo.html"); -}); - -export default router; diff --git a/services/collaboration-service/src/routes/room.ts b/services/collaboration-service/src/routes/room.ts deleted file mode 100644 index 7b5a30e5..00000000 --- a/services/collaboration-service/src/routes/room.ts +++ /dev/null @@ -1,308 +0,0 @@ -import express, { Request, Response } from "express"; -import { type } from "ot-text-unicode"; -import { Socket, Server } from "socket.io"; - -import { - createOrUpdateRoomWithUser, - removeUserFromRoom, - updateRoomText, - updateRoomStatus, - getRoomText, - saveRoomText, - isRoomExists, - getRoom, - getSavedRoomText, - saveAttempt, - setRoomQuestion, -} from "../db/prisma-db"; - -import { - OpHistoryMap, - TextOperationSet, - TextOperationSetWithCursor, - getTransformedOperations, - transformPosition, -} from "../ot"; - -interface SocketDetails { - room_id: string; - user_id: string; -} - -enum SocketEvents { - ROOM_JOIN = "api/collaboration-service/room/join", - ROOM_UPDATE = "api/collaboration-service/room/update", - ROOM_SAVE = "api/collaboration-service/room/save", - ROOM_LOAD = "api/collaboration-service/room/load", - QUESTION_SET = "api/collaboration-service/question/set", -} - -const socketMap: Record = {}; -const opMap: OpHistoryMap = new OpHistoryMap(); - -const AccessToken = require("twilio").jwt.AccessToken; -const VideoGrant = AccessToken.VideoGrant; - -const TWILIO_ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID; -const TWILIO_API_KEY = process.env.TWILIO_API_KEY; -const TWILIO_API_SECRET = process.env.TWILIO_API_SECRET; - -// Data Access Layer -function mapSocketToRoomAndUser( - socket_id: string, - room_id: string, - user_id: string -) { - socketMap[socket_id] = { - room_id: room_id, - user_id: user_id, - }; -} - -async function updateStatus(socket_id: string) { - if (!socketMap[socket_id]) { - return; - } - const { room_id } = socketMap[socket_id]; - await updateRoomStatus(room_id); -} - -async function disconnectUserFromDb(socket_id: string): Promise { - if (!socketMap[socket_id]) { - return; - } - const { room_id, user_id } = socketMap[socket_id]; - await removeUserFromRoom(room_id, user_id); -} - -// Socket callbacks -function roomUpdate( - io: Server, - socket: Socket, - room_id: string, - text: string -): void { - console.log(room_id + " " + socket.id + " text changed:", text); - const version = opMap.getLatest(room_id)?.version ?? 1; - io.to(room_id).emit(SocketEvents.ROOM_UPDATE, { version, text }); - updateRoomText(room_id, text); -} - -function roomUpdateWithCursor( - io: Server, - socket: Socket, - room_id: string, - text: string, - cursor: number -): void { - console.log( - room_id + " " + socket.id + " text changed:", - text, - " cursor:" + cursor - ); - const version = opMap.getLatest(room_id)?.version ?? 1; - socket.broadcast - .to(room_id) - .emit(SocketEvents.ROOM_UPDATE, { version, text }); - socket.emit(SocketEvents.ROOM_UPDATE, { version, text, cursor }); - updateRoomText(room_id, text); -} - -async function handleTextOp( - textOpSet: TextOperationSetWithCursor, - room_id: string -): Promise<{ text: string; cursor: number }> { - console.log(textOpSet); - console.log(opMap.getLatest(room_id)?.version); - var resultTextOps = textOpSet.operations; - - if (opMap.checkIfLatestVersion(room_id, textOpSet.version)) { - textOpSet.version++; - opMap.add(room_id, textOpSet); - } else { - const latestOp = textOpSet.operations; - const mergedOp = opMap.getCombinedTextOpFromVersionToLatest( - room_id, - textOpSet.version + 1 - ); - const [transformedLatestOp, transformedMergedOp] = getTransformedOperations( - latestOp, - mergedOp - ); - opMap.add(room_id, { - version: textOpSet.version + 1, - operations: transformedLatestOp, - }); - console.log(transformedLatestOp); - resultTextOps = transformedLatestOp; - } - - return getRoomText(room_id).then((text) => { - var resultText = text; - - try { - resultText = type.apply(text, resultTextOps); - } catch (error) { - // gracefully skip transforming - console.log(error); - } - - return { - text: resultText, - cursor: textOpSet.cursor - ? transformPosition(textOpSet.cursor, resultTextOps) - : -1, - }; - }); -} - -async function roomUpdateWithTextFromDb( - io: Server, - socket: Socket, - room_id: string -): Promise { - await getRoomText(room_id).then((text) => { - roomUpdate(io, socket, room_id, text); - }); -} - -async function loadTextFromDb( - io: Server, - socket: Socket, - room_id: string -): Promise { - await getSavedRoomText(room_id).then((text) => { - if (text) { - roomUpdate(io, socket, room_id, text); - } - }); -} - -async function userDisconnect(socket: Socket): Promise { - console.log("User disconnected:", socket.id); - await disconnectUserFromDb(socket.id).then(() => updateStatus(socket.id)); -} - -function initSocketListeners(io: Server, socket: Socket, room_id: string) { - socket.on( - SocketEvents.ROOM_UPDATE, - async (textOpSet: TextOperationSetWithCursor, ackCallback) => { - await handleTextOp(textOpSet, room_id).then(({ text, cursor }) => { - if (cursor > -1) { - roomUpdateWithCursor(io, socket, room_id, text, cursor); - } else { - roomUpdate(io, socket, room_id, text); - } - ackCallback(); - }); - } - ); - - socket.on(SocketEvents.ROOM_SAVE, (text: string) => - saveRoomText(room_id, text).then(() => saveAttempt(room_id)) - ); - - socket.on(SocketEvents.ROOM_LOAD, () => loadTextFromDb(io, socket, room_id)); - - socket.on(SocketEvents.QUESTION_SET, (question: string) => { - setRoomQuestion(room_id, question).then(() => { - console.log("Question set:", question); - }); - io.to(room_id).emit(SocketEvents.QUESTION_SET, question); - }); -} - -function getTwilioAccessToken(room_id: string, user_id: string): string { - const videoGrant = new VideoGrant({ room: room_id }); - const token = new AccessToken( - TWILIO_ACCOUNT_SID, - TWILIO_API_KEY, - TWILIO_API_SECRET, - { identity: user_id, ttl: 60 * 60 * 12 } - ); - token.addGrant(videoGrant); - return token.toJwt(); -} - -export const roomApiRouter = () => { - const router = express.Router(); - - router.get("/:room_id", async (req: Request, res: Response) => { - const room_id = req.params.room_id as string; - - if (!isRoomExists(room_id)) { - return res.status(404).json({ error: "Room not found" }); - } - - return res.status(200).json({ - message: "Room exists", - room_id: room_id, - questionId: await getRoom(room_id).then((room) => room.question_id), - info: await getRoom(room_id), - }); - }); -}; - -export const roomRouter = (io: Server) => { - const router = express.Router(); - - router.get("/:room_id", async (req: Request, res: Response) => { - const room_id = req.params.room_id as string; - - if (!isRoomExists(room_id)) { - return res.status(404).json({ error: "Room not found" }); - } - - return res.status(200).json({ - message: "Room exists", - room_id: room_id, - questionId: await getRoom(room_id).then((room) => room.question_id), - info: await getRoom(room_id), - }); - }); - - router.post("/save", async (req: Request, res: Response) => { - try { - const room_id = req.body.room_id as string; - const text = req.body.text as string; - - if (!isRoomExists(room_id)) { - return res.status(400).json({ error: "Invalid roomId provided" }); - } - - await saveRoomText(room_id, text) - .then(async () => await saveAttempt(room_id)) - .then(async () => { - res.status(201).json({ - message: "Room saved successfully", - info: await getRoom(room_id), - }); - }); - } catch (error) { - console.error(error); - res.status(500).json({ message: "Error saving room" }); - } - }); - - // WebSocket style API - io.on("connection", (socket: Socket) => { - console.log("Room.ts: User connected:", socket.id); - - socket.on(SocketEvents.ROOM_JOIN, (room_id: string, user_id: string) => { - socket.join(room_id); - console.log(socket.id + " joined room:", room_id); - createOrUpdateRoomWithUser(room_id, user_id); - mapSocketToRoomAndUser(socket.id, room_id, user_id); - roomUpdateWithTextFromDb(io, socket, room_id); - socket.emit("twilio-token", getTwilioAccessToken(room_id, user_id)); - - initSocketListeners(io, socket, room_id); - }); - - socket.on("disconnect", async () => userDisconnect(socket)); - }); - - return router; -}; - -export default roomRouter; diff --git a/services/collaboration-service/src/swagger-output.json b/services/collaboration-service/src/swagger-output.json deleted file mode 100644 index d968c3e9..00000000 --- a/services/collaboration-service/src/swagger-output.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Collaboration Service", - "description": "Provides the mechanism for real-time collaboration (e.g., concurrent code editing) between the authenticated and matched users in the collaborative space", - "version": "1.0.0" - }, - "servers": [ - { - "url": "http://localhost:5003/" - } - ], - "paths": { - "/demo/": { - "get": { - "description": "", - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/collaboration-service/room/{room_id}": { - "get": { - "description": "", - "parameters": [ - { - "name": "room_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "404": { - "description": "Not Found" - } - } - } - }, - "/api/collaboration-service/room/save": { - "post": { - "description": "", - "responses": { - "201": { - "description": "Created" - }, - "400": { - "description": "Bad Request" - }, - "500": { - "description": "Internal Server Error" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "room_id": { - "example": "any" - }, - "text": { - "example": "any" - } - } - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/services/collaboration-service/swagger-doc-gen.ts b/services/collaboration-service/swagger-doc-gen.ts deleted file mode 100644 index 5bd7f79a..00000000 --- a/services/collaboration-service/swagger-doc-gen.ts +++ /dev/null @@ -1,25 +0,0 @@ -import swaggerAutogen from "swagger-autogen"; - -const doc = { - info: { - title: "Collaboration Service", - description: - "Provides the mechanism for real-time collaboration (e.g., concurrent code editing) between the authenticated and matched users in the collaborative space", - }, - host: "localhost:5003", - schemes: ["http"], -}; - -const outputFile = "./src/swagger-output.json"; -const endpointsFiles = ["./src/app.ts"]; - -/* NOTE: if you use the express Router, you must pass in the - 'endpointsFiles' only the root file where the route starts, - such as index.js, app.js, routes.js, ... */ - -swaggerAutogen({ openapi: "3.0.0" })(outputFile, endpointsFiles, doc); -/*.then( - async () => { - await import("./src/app"); // Your project's root file - } - );*/ // to run it after swagger-autogen diff --git a/services/collaboration-service/tsconfig.json b/services/collaboration-service/tsconfig.json deleted file mode 100644 index 22f4d5c5..00000000 --- a/services/collaboration-service/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2019", - "module": "commonjs", - "rootDir": ".", - "outDir": "./dist", - "strict": true, - "esModuleInterop": true, - "resolveJsonModule": true, - "types": ["node"] - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "src/**/__mocks__/*.ts"] -} diff --git a/services/gateway/.gitignore b/services/gateway/.gitignore deleted file mode 100644 index ce597ce5..00000000 --- a/services/gateway/.gitignore +++ /dev/null @@ -1,64 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Ignore built ts files -dist/**/* - -# Dependency directories -node_modules/ -jspm_packages/ - -# Typescript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - -# next.js build output -.next diff --git a/services/gateway/README.md b/services/gateway/README.md deleted file mode 100644 index cb3a461e..00000000 --- a/services/gateway/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# Gateway - -## API Route Proxy -Much of the proxy functionality was adapted from [this tutorial](https://medium.com/geekculture/create-an-api-gateway-using-nodejs-and-express-933d1ca23322 -). - -The below code shows a sample route that is being proxied from the frontend to the backend through the gateway: -``` -{ - url: "/api/user-service", - admin_required_methods: [], // Empty, so no admin verification is done for all methods to the user-service - user_match_required_methods: ["PUT", "DELETE"], - // PUT and DELETE require checking that the user is only updating/deleting their own data - rateLimit: { - windowMs: 15 * 60 * 1000, - max: 5, - }, - proxy: { - target: userServiceAddress, - changeOrigin: true, - }, -}, -``` - -This code is part of the `http_proxied_routes` list in `src/proxied_routes/proxied_routes.ts` file. - -Explanation: -* `url` - The initial path. Assuming that the gateway address is `YYY://localhost:4000`, the frontend would call `YYY://localhost:4000/api/user-service` -* `admin_required_methods` - a list of methods in which admin role is required to access the resource -* `user_match_required_methods` - a list of methods in which the `uid` in the `"User-Id"` header of the request must be checked against the current user in Firebase -* `rateLimit` - currently unused. May be removed if not needed -* `proxy` - an object for routing the request to the user service. The underlying dependency used is [`http-proxy-middleware`](https://github.com/chimurai/http-proxy-middleware) - -### Required headers -The required headers are as follows: -* `User-Id-Token` - the id token obtained by calling [`getIdToken()` on the current Firebase user](https://firebase.google.com/docs/reference/js/v8/firebase.User#getidtoken) -* `User-Id` - if user matching is done, the `uid` for which the request is being made to. Usually, requests requiring -the `uid` check will have the `uid` in the path param. So the `uid` value in `User-Id` and the path param must be the same. - - -## Required environment variables -The Gateway requires the following environment variables: - -| Environment variable file | File location | Environment Variable Name | Explanation | -|------------------------------------------------------| --- | --- |---------------------------------------------------------------------------------------------------------------------------| -| `.env` | Project root | `FIREBASE_SERVICE_ACCOUNT` | The service account corresponding to the app on Firebase. This is needed for API calls. | -| `.env.development.local` (already in source control) | Project root | `ENVIRONMENT_TYPE` | Set this to `local-dev` for `localhost` testing. In other environments like Docker and Kubernetes, this file is not read. | - - -## Local development and testing of the Gateway -Steps: -1) Add an `.env` file at the project root with the above-mentioned variable at the project root. -2) At the project root, run `yarn workspace gateway dev:local` diff --git a/services/gateway/package.json b/services/gateway/package.json deleted file mode 100644 index 1e7a5183..00000000 --- a/services/gateway/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "gateway", - "version": "1.0.0", - "private": true, - "description": "Gateway Service between frontend and backend", - "main": "src/app.ts", - "scripts": { - "lint": "eslint src/**/*.{ts,js}", - "dev:local": "dotenv -e ../../.env -c development -- yarnpkg dev", - "dev": "ts-node-dev src/app.ts", - "build": "tsc", - "start": "node dist/src/app.js", - "test": "echo \"Error: no test specified\" && exit 1" - }, - "dependencies": { - "cors": "^2.8.5", - "express-healthcheck": "^0.1.0", - "firebase-admin": "^11.10.1", - "http-proxy-middleware": "^2.0.6", - "morgan": "^1.10.0" - }, - "devDependencies": { - "@types/cors": "^2.8.14", - "@types/express-healthcheck": "^0.1.2", - "@types/morgan": "^1.9.6" - } -} diff --git a/services/gateway/src/app.ts b/services/gateway/src/app.ts deleted file mode 100644 index d9967b3c..00000000 --- a/services/gateway/src/app.ts +++ /dev/null @@ -1,81 +0,0 @@ -import express, { Express } from "express"; -import cors from "cors"; -import { setupLogging } from "./logging/logging"; -import { setupAdmin, setupUserIdMatch, setupIsLoggedIn } from "./auth/auth"; -import { setupProxies } from "./proxy/proxy"; -import { - http_proxied_routes, - wsCollaborationProxiedRoutes, - wsMatchProxiedRoutes, -} from "./proxied_routes/proxied_routes"; -import { frontendAddress } from "./proxied_routes/service_names"; -import { createProxyMiddleware } from "http-proxy-middleware"; -import healthCheck from "express-healthcheck"; - -const httpApp: Express = express(); -const wsMatchApp: Express = express(); -const wsCollaborationApp: Express = express(); - -const corsOptions = { - origin: frontendAddress, - methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], -}; - -const httpProxyPort: number = parseInt(process.env.HTTP_PROXY_PORT || "4000"); -const wsMatchProxyPort: number = parseInt( - process.env.WS_MATCH_PROXY_PORT || "4002" -); -const wsCollaborationProxyPort: number = parseInt( - process.env.WS_COLLABORATION_PROXY_PORT || "4003" -); - -httpApp.use(cors(corsOptions)); -wsMatchApp.use(cors(corsOptions)); -wsCollaborationApp.use(cors(corsOptions)); - -// Health check -httpApp.use("/healthcheck", healthCheck()); - -/** - * WARNING: Do not add body parsing middleware to the Gateway. - * Otherwise, proxying POST requests with request body would not work. - */ -setupLogging(httpApp); -setupLogging(wsMatchApp); -setupLogging(wsCollaborationApp); - -setupIsLoggedIn(httpApp, http_proxied_routes); -setupIsLoggedIn(wsMatchApp, wsMatchProxiedRoutes); -setupIsLoggedIn(wsCollaborationApp, wsCollaborationProxiedRoutes); - -setupUserIdMatch(httpApp, http_proxied_routes); -setupAdmin(httpApp, http_proxied_routes); -setupProxies(httpApp, http_proxied_routes); - -const wsMatchProxyMiddleware = createProxyMiddleware( - wsMatchProxiedRoutes[0].proxy -); -wsMatchApp.use(wsMatchProxiedRoutes[0].url, wsMatchProxyMiddleware); -const wsCollaborationProxyMiddleware = createProxyMiddleware( - wsCollaborationProxiedRoutes[0].proxy -); -wsCollaborationApp.use( - wsCollaborationProxiedRoutes[0].url, - wsCollaborationProxyMiddleware -); - -httpApp.listen(httpProxyPort, () => { - console.log(`Gateway HTTP proxy listening on port ${httpProxyPort}`); -}); - -wsMatchApp.listen(wsMatchProxyPort, () => { - console.log( - `Gateway WebSockets Match Proxy listening on port ${wsMatchProxyPort}` - ); -}); - -wsCollaborationApp.listen(wsCollaborationProxyPort, () => { - console.log( - `Gateway WebSockets Collaboration Proxy listening on port ${wsCollaborationProxyPort}` - ); -}); diff --git a/services/gateway/src/auth/auth.ts b/services/gateway/src/auth/auth.ts deleted file mode 100644 index 74c8dedf..00000000 --- a/services/gateway/src/auth/auth.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { promiseVerifyIsLoggedIn, promiseVerifyIsCorrectUser, promiseVerifyIsAdmin } from './firebase'; -import express, {Express} from "express"; -import {frontendAddress} from "../proxied_routes/service_names"; - -const redirectLink = frontendAddress; -const userIdTokenHeader = "User-Id-Token"; -const userIdHeader = "User-Id"; - -export const setupIsLoggedIn = (app : Express, routes : any[]) => { - routes.forEach(r => { - app.use(r.url, function(req : express.Request, res : express.Response, next : express.NextFunction) { - const idToken = req.get(userIdTokenHeader); - if (!idToken) { - res.redirect(redirectLink); - } else { - promiseVerifyIsLoggedIn(idToken as string).then((uid) => { - if (uid) { - req.headers["user-id"] = uid; - next(); - } else { - res.redirect(redirectLink) - } - }).catch((error) => { - console.error(error); - res.status(500).send("A server-side error occurred! Contact the admin for help."); - }); - } - }); - }); -} - -export const setupUserIdMatch = (app : Express, routes : any[]) => { - routes.forEach(r => { - app.use(r.url, function(req : express.Request, res : express.Response, next : express.NextFunction) { - if (r.user_match_required_methods.includes(req.method)) { - const idToken = req.get(userIdTokenHeader); - const paramUid = req.get(userIdHeader); - if (!idToken || !paramUid) { - res.redirect(redirectLink) - } else { - promiseVerifyIsCorrectUser(idToken as string, paramUid).then((isCorrectUid) => { - if (isCorrectUid) { - next(); - } else { - res.redirect(redirectLink); - } - }).catch((error) => { - console.error(error); - res.status(500).send("A server-side error occurred! Contact the admin for help."); - }); - } - } else { - // Skip - next(); - } - }); - }); -} - -export const setupAdmin = (app : Express, routes : any[]) => { - // If admin access is required, check that the firebase ID token has an admin claim - routes.forEach(r => { - app.use(r.url, function(req : express.Request, res : express.Response, next : express.NextFunction) { - if (r.admin_required_methods.includes(req.method)) { - // Pass in the user as a header of the request - const idToken = req.get(userIdTokenHeader); - if (!idToken) { - res.redirect(redirectLink); - } else { - promiseVerifyIsAdmin(idToken as string).then((isAdmin) => { - if (isAdmin) { - next(); - } else { - res.status(403).send("You are not admin."); - } - }).catch((error) => { - console.error(error); - res.status(500).send("A server-side error occurred! Contact the admin for help."); - }) - } - } else { - // Skip - next(); - } - }); - }); -}; diff --git a/services/gateway/src/auth/firebase.ts b/services/gateway/src/auth/firebase.ts deleted file mode 100644 index 026076de..00000000 --- a/services/gateway/src/auth/firebase.ts +++ /dev/null @@ -1,50 +0,0 @@ -import admin from "firebase-admin"; -import { Auth, getAuth } from "firebase-admin/auth"; -import process from "process"; -import { App } from "firebase-admin/lib/app"; - -const serviceAccount = process.env.FIREBASE_SERVICE_ACCOUNT - ? JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT as string) - : {}; - -const firebaseApp: App = admin.initializeApp({ - credential: admin.credential.cert(serviceAccount), -}); - -const firebaseAuth: Auth = getAuth(firebaseApp); - -export function promiseVerifyIsLoggedIn(idToken: string) { - return firebaseAuth - .verifyIdToken(idToken, true) - .then((decodedToken) => { - return decodedToken.sub; - }) - .catch(() => { - return ""; - }); -} - -export function promiseVerifyIsCorrectUser(idToken: string, paramUid: string) { - return firebaseAuth - .verifyIdToken(idToken, true) - .then((decodedToken) => { - const uid = decodedToken.uid; - return uid === paramUid; - }) - .catch(() => { - return false; - }); -} - -export function promiseVerifyIsAdmin(idToken: string) { - return firebaseAuth - .verifyIdToken(idToken, true) - .then((claims) => { - return !!claims.admin; - }) - .catch((error) => { - // Handle error - console.log(error); - return false; - }); -} diff --git a/services/gateway/src/logging/logging.ts b/services/gateway/src/logging/logging.ts deleted file mode 100644 index c947ff5f..00000000 --- a/services/gateway/src/logging/logging.ts +++ /dev/null @@ -1,6 +0,0 @@ -import morgan from "morgan"; -import {Express} from "express"; - -export const setupLogging = (app : Express) => { - app.use(morgan('combined')); -} diff --git a/services/gateway/src/proxied_routes/proxied_route_type.ts b/services/gateway/src/proxied_routes/proxied_route_type.ts deleted file mode 100644 index a284e7a7..00000000 --- a/services/gateway/src/proxied_routes/proxied_route_type.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {Options} from "http-proxy-middleware"; - -export type ProxiedRoute = { - url: string; - admin_required_methods: string[]; - user_match_required_methods: string[]; - rateLimit?: { - windowMs: number; - max: number; - }, - proxy: Options -} diff --git a/services/gateway/src/proxied_routes/proxied_routes.ts b/services/gateway/src/proxied_routes/proxied_routes.ts deleted file mode 100644 index 1ffe07d9..00000000 --- a/services/gateway/src/proxied_routes/proxied_routes.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { ProxiedRoute } from "./proxied_route_type"; -import { - adminServiceAddress, - collaborationServiceAddress, - matchingServiceAddress, - questionServiceAddress, - userServiceAddress, -} from "./service_names"; - -export const http_proxied_routes: ProxiedRoute[] = [ - { - url: "/api/user-service", - admin_required_methods: [], // Empty, so no admin verification is done for all methods to the user-service - user_match_required_methods: ["PUT", "DELETE"], - // PUT and DELETE require checking that the user is only updating/deleting their own data - rateLimit: { - windowMs: 15 * 60 * 1000, - max: 5, - }, - proxy: { - target: userServiceAddress, - changeOrigin: true, - }, - }, - { - url: "/api/admin-service", - admin_required_methods: ["GET", "POST", "PUT", "DELETE"], // All routes in admin service can only be accessed by admins - user_match_required_methods: [], // No need for exact user match here - proxy: { - target: adminServiceAddress, - changeOrigin: true, - }, - }, - { - url: "/api/question-service", - admin_required_methods: ["POST", "PUT", "DELETE"], // Only admins can create, update or delete questions - user_match_required_methods: [], // No need for exact user match here - proxy: { - target: questionServiceAddress, - changeOrigin: true, - }, - }, - { - url: "/api/matching-service", - admin_required_methods: [], - user_match_required_methods: [], // No need for exact user match here - proxy: { - target: matchingServiceAddress, - changeOrigin: true, - }, - }, - { - url: "/api/collaboration-service", - admin_required_methods: [], - user_match_required_methods: [], // No need for exact user match here - proxy: { - target: collaborationServiceAddress, - changeOrigin: true, - }, - }, -]; - -export const wsMatchProxiedRoutes: ProxiedRoute[] = [ - { - url: "/", - admin_required_methods: [], - user_match_required_methods: [], // No need for exact user match here - proxy: { - target: matchingServiceAddress, - changeOrigin: true, - ws: true - }, - }, -] - -export const wsCollaborationProxiedRoutes: ProxiedRoute[] = [ - { - url: "/", - admin_required_methods: [], - user_match_required_methods: [], // No need for exact user match here - proxy: { - target: collaborationServiceAddress, - changeOrigin: true, - ws: true - }, - }, -] diff --git a/services/gateway/src/proxied_routes/service_names.ts b/services/gateway/src/proxied_routes/service_names.ts deleted file mode 100644 index 4379c837..00000000 --- a/services/gateway/src/proxied_routes/service_names.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * File for defining the addresses of other services - * - * How to use: - * - For localhost development, set ENVIRONMENT_TYPE environment variable to "local-dev" - * - For other environments like Docker or Kubernetes, use name resolution - */ - -const isLocal : boolean = (process.env.ENVIRONMENT_TYPE === "local-dev"); - -export const userServiceAddress : string = (isLocal) - ? "http://localhost:5001/" - : "http://user-service:5001/"; - -export const matchingServiceAddress : string = (isLocal) - ? "http://localhost:5002/" - : "http://matching-service:5002/"; - -export const collaborationServiceAddress : string = (isLocal) - ? "http://localhost:5003/" - : "http://collaboration-service:5003/"; - -export const questionServiceAddress : string = (isLocal) - ? "http://localhost:5004/" - : "http://question-service:5004/"; - -export const adminServiceAddress : string = (isLocal) - ? "http://localhost:5005/" - : "http://admin-service:5005/"; - -export const frontendAddress : string = (isLocal) - ? "http://localhost:3000" - : process.env.FRONTEND_ADDRESS as string; -// This is used in CORS origin checking, so the address cannot have a trailing forward slash diff --git a/services/gateway/src/proxy/proxy.ts b/services/gateway/src/proxy/proxy.ts deleted file mode 100644 index 484f7abc..00000000 --- a/services/gateway/src/proxy/proxy.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createProxyMiddleware } from 'http-proxy-middleware'; -import {Express} from "express"; - -export const setupProxies = (app : Express, routes : any[]) => { - var proxyMiddlewareArray : any[] = [] - for (let i = 0; i < routes.length; i++) { - const proxyMiddleware = createProxyMiddleware(routes[i].proxy); - app.use(routes[i].url, proxyMiddleware); - if (routes[i].proxy.ws) { - proxyMiddlewareArray.push(proxyMiddleware); - } - } - return proxyMiddlewareArray; -} diff --git a/services/gateway/tsconfig.json b/services/gateway/tsconfig.json deleted file mode 100644 index bb2254c4..00000000 --- a/services/gateway/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "target": "es2016", - "module": "commonjs", - "rootDir": ".", - "outDir": "./dist", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "src/**/__mocks__/*.ts"] -} diff --git a/start-app-no-docker.sh b/start-app-no-docker.sh index 85342f8f..8ca859d3 100755 --- a/start-app-no-docker.sh +++ b/start-app-no-docker.sh @@ -11,8 +11,6 @@ prepend() { (yarnpkg workspace frontend dev:local | prepend "frontend: ") & \ (yarnpkg workspace user-service dev:local | prepend "user-service: ") & \ (yarnpkg workspace admin-service dev:local | prepend "admin-service: ") & \ - (yarnpkg workspace collaboration-service dev:local | prepend "collaboration-service: ") & \ (yarnpkg workspace matching-service dev:local | prepend "matching-service: ") & \ (yarnpkg workspace question-service dev:local | prepend "question-service: ") & \ - (yarnpkg workspace gateway dev:local | prepend "gateway: ") & \ wait) diff --git a/yarn.lock b/yarn.lock index 7db30759..fcb14e2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2896,13 +2896,6 @@ dependencies: "@types/node" "*" -"@types/express-healthcheck@^0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@types/express-healthcheck/-/express-healthcheck-0.1.2.tgz#1f1aa8846d5e0fb9f71a9acad8ac2f1806bf6fd6" - integrity sha512-enSA1JuIErGJqfCCDmNAq6N8OIBhgdg+mBvHUOTrjy7zdrDEKJ4sJr97MoFZNIDh32bUII1F+uPmqgC9H8b4mQ== - dependencies: - "@types/express" "*" - "@types/express-serve-static-core@^4.17.33": version "4.17.39" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.39.tgz#2107afc0a4b035e6cb00accac3bdf2d76ae408c8" @@ -2950,13 +2943,6 @@ resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.3.tgz#c54e61f79b3947d040f150abd58f71efb422ff62" integrity sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA== -"@types/http-proxy@^1.17.8": - version "1.17.13" - resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.13.tgz#dd3a4da550580eb0557d4c7128a2ff1d1a38d465" - integrity sha512-GkhdWcMNiR5QSQRYnJ+/oXzu0+7JJEPC8vkWXK351BkhjraZF+1W13CUYARUvX9+NqIU2n6YHA4iwywsc/M6Sw== - dependencies: - "@types/node" "*" - "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.6": version "7.0.14" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.14.tgz#74a97a5573980802f32c8e47b663530ab3b6b7d1" @@ -3024,7 +3010,7 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== -"@types/morgan@^1.9.5", "@types/morgan@^1.9.6": +"@types/morgan@^1.9.5": version "1.9.7" resolved "https://registry.yarnpkg.com/@types/morgan/-/morgan-1.9.7.tgz#ba1e980841be06cd164eedfba7e3e1e2f4d0c911" integrity sha512-4sJFBUBrIZkP5EvMm1L6VCXp3SQe8dnXqlVpe1jsmTjS1JQVmSjnpMNs8DosQd6omBi/K7BSKJ6z/Mc3ki0K9g== @@ -3149,13 +3135,6 @@ dependencies: socket.io-client "*" -"@types/socket.io@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/socket.io/-/socket.io-3.0.2.tgz#606c9639e3f93bb8454cba8f5f0a283d47917759" - integrity sha512-pu0sN9m5VjCxBZVK8hW37ZcMe8rjn4HHggBN5CbaRTvFwv5jOmuIRZEuddsBPa9Th0ts0SIo3Niukq+95cMBbQ== - dependencies: - socket.io "*" - "@types/strip-bom@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" @@ -3204,11 +3183,6 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc" integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA== -"@types/uuid@^9.0.4": - version "9.0.6" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.6.tgz#c91ae743d8344a54b2b0c691195f5ff5265f6dfb" - integrity sha512-BT2Krtx4xaO6iwzwMFUYvWBWkV2pr37zD68Vmp1CDV196MzczBRxuEpD6Pr395HAgebC/co7hOphs53r8V7jew== - "@types/webidl-conversions@*": version "7.0.2" resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-7.0.2.tgz#d703e2bf61d8b77a7669adcd8fdf98108155d594" @@ -3429,7 +3403,7 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -ajv-formats@^2.0.2, ajv-formats@^2.1.0: +ajv-formats@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== @@ -3446,7 +3420,7 @@ ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.6: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.1.0, ajv@^8.3.0, ajv@^8.4.0: +ajv@^8.0.0, ajv@^8.3.0: version "8.12.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -3824,13 +3798,6 @@ axe-core@^4.6.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.8.2.tgz#2f6f3cde40935825cf4465e3c1c9e77b240ff6ae" integrity sha512-/dlp0fxyM3R8YW7MFzaHWXrf4zzbr0vaYb23VBFCl83R7nWNPg/yaQw2Dc8jzCMmDVLhSdzH8MjrsuIUuvX+6g== -axios@^0.26.1: - version "0.26.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" - integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== - dependencies: - follow-redirects "^1.14.8" - axobject-query@^3.1.1: version "3.2.1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" @@ -3957,7 +3924,7 @@ body-parser@1.20.1: type-is "~1.6.18" unpipe "1.0.0" -body-parser@^1.18.3, body-parser@^1.19.0, body-parser@^1.20.2: +body-parser@^1.18.3, body-parser@^1.19.0: version "1.20.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== @@ -4684,11 +4651,6 @@ date-fns@^2.30.0: dependencies: "@babel/runtime" "^7.21.0" -dayjs@^1.11.9: - version "1.11.10" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" - integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== - debug@2.6.9, debug@~2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -4731,18 +4693,6 @@ deep-eql@^4.1.3: dependencies: type-detect "^4.0.0" -deep-equal@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" - integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== - dependencies: - is-arguments "^1.0.4" - is-date-object "^1.0.1" - is-regex "^1.0.4" - object-is "^1.0.1" - object-keys "^1.1.1" - regexp.prototype.flags "^1.2.0" - deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -4867,13 +4817,6 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -difunc@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/difunc/-/difunc-0.0.4.tgz#09322073e67f82effd2f22881985e7d3e441b3ac" - integrity sha512-zBiL4ALDmviHdoLC0g0G6wVme5bwAow9WfhcZLLopXCAWgg3AEf7RYTs2xugszIGulRHzEVDF/SHl9oyQU07Pw== - dependencies: - esprima "^4.0.0" - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -5515,11 +5458,6 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== -eventemitter3@^4.0.0: - version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== - events-listener@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/events-listener/-/events-listener-1.1.0.tgz#dd49b4628480eba58fde31b870ee346b3990b349" @@ -5565,25 +5503,6 @@ exponential-backoff@^3.1.1: resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== -express-healthcheck@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/express-healthcheck/-/express-healthcheck-0.1.0.tgz#cabec78129c4cb90cd7fb894dfae21b82e27cb07" - integrity sha512-FKQVgDo1FMSOYflEq4g6CvNk6stbpkuX0MWXmul8dSICuw/b+3JgoYOq/aiDcYid5k42jh/4HYLYC/M/qDBEuQ== - -express-normalize-query-params-middleware@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/express-normalize-query-params-middleware/-/express-normalize-query-params-middleware-0.5.1.tgz#dbe1e8139aecb234fb6adb5c0059c75db9733d2a" - integrity sha512-KUBjEukYL9KJkrphVX3ZgMHgMTdgaSJe+FIOeWwJIJpCw8UZQPIylt0MYddSyUwbms4LQ8RC4wmavcLUP9uduA== - -express-openapi@^12.1.3: - version "12.1.3" - resolved "https://registry.yarnpkg.com/express-openapi/-/express-openapi-12.1.3.tgz#a05633a01a6541a650915ad19cf16fb9ee39e55a" - integrity sha512-F570dVC5ENSkLu1SpDFPRQ13Y3a/7Udh0rfHyn3O1QrE81fPmlhnAo1JRgoNtbMRJ6goHNymxU1TVSllgFZBlQ== - dependencies: - express-normalize-query-params-middleware "^0.5.0" - openapi-framework "^12.1.3" - openapi-types "^12.1.3" - express@^4.16.4, express@^4.18.2: version "4.18.2" resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" @@ -5920,11 +5839,6 @@ fn.name@1.x.x: resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== -follow-redirects@^1.0.0, follow-redirects@^1.14.8: - version "1.15.3" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" - integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== - for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -6035,11 +5949,6 @@ fs-readdir-recursive@^1.1.0: resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" integrity sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA== -fs-routes@^12.1.3: - version "12.1.3" - resolved "https://registry.yarnpkg.com/fs-routes/-/fs-routes-12.1.3.tgz#6c41eb370bf35dcfb2d0cebffe53f61093bbcc93" - integrity sha512-Vwxi5StpKj/pgH7yRpNpVFdaZr16z71KNTiYuZEYVET+MfZ31Zkf7oxUmNgyZxptG8BolRtdMP90agIhdyiozg== - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -6221,17 +6130,6 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@*, glob@^10.2.2: - version "10.3.10" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" - integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== - dependencies: - foreground-child "^3.1.0" - jackspeak "^2.3.5" - minimatch "^9.0.1" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry "^1.10.1" - glob@7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -6256,6 +6154,17 @@ glob@7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^10.2.2: + version "10.3.10" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" + integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.3.5" + minimatch "^9.0.1" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry "^1.10.1" + glob@^7.0.0, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.7, glob@^7.2.0, glob@^7.2.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -6678,26 +6587,6 @@ http-proxy-agent@^7.0.0: agent-base "^7.1.0" debug "^4.3.4" -http-proxy-middleware@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" - integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== - dependencies: - "@types/http-proxy" "^1.17.8" - http-proxy "^1.18.1" - is-glob "^4.0.1" - is-plain-obj "^3.0.0" - micromatch "^4.0.2" - -http-proxy@^1.18.1: - version "1.18.1" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" - integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== - dependencies: - eventemitter3 "^4.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -6987,11 +6876,6 @@ is-decimal@^1.0.0: resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== -is-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-dir/-/is-dir-1.0.0.tgz#41d37f495fccacc05a4778d66e83024c292ba3ff" - integrity sha512-vLwCNpTNkFC5k7SBRxPubhOCryeulkOsSkjbGyZ8eOzZmzMS+hSEO/Kn9ZOVhFNAlRZTFc4ZKql48hESuYUPIQ== - is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -7083,11 +6967,6 @@ is-path-inside@^3.0.2, is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== -is-plain-obj@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" - integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== - is-plain-obj@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" @@ -7098,7 +6977,7 @@ is-plain-object@^5.0.0: resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== -is-regex@^1.0.4, is-regex@^1.1.4: +is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== @@ -7285,7 +7164,7 @@ jose@^4.14.6: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.10.0, js-yaml@^3.13.1: +js-yaml@^3.13.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -7397,13 +7276,6 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json0-ot-diff@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/json0-ot-diff/-/json0-ot-diff-1.1.2.tgz#3565b8b016992b750c364558f5b5ffd56a0749c2" - integrity sha512-je6cDbmPc+BkbfyvKo7y1jgQLTrX81L8fkKEIPXRUGFSxK4HTSF6u44ELR35i12tEIWh5+8KfIJH2aJgzKrFww== - dependencies: - deep-equal "^1.0.1" - json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" @@ -7696,7 +7568,7 @@ lodash.isstring@^4.0.1: resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== -lodash.merge@^4.6.1, lodash.merge@^4.6.2: +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== @@ -8157,7 +8029,7 @@ micromark@^4.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: +micromatch@^4.0.4, micromatch@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== @@ -8538,14 +8410,6 @@ object-inspect@^1.13.1, object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== -object-is@^1.0.1: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" - integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -8653,89 +8517,6 @@ open@^6.3.0: dependencies: is-wsl "^1.1.0" -openapi-default-setter@^12.1.3: - version "12.1.3" - resolved "https://registry.yarnpkg.com/openapi-default-setter/-/openapi-default-setter-12.1.3.tgz#9457f55de0a9da9224918969896af35162dd02ac" - integrity sha512-wHKwvEuOWwke5WcQn8pyCTXT5WQ+rm9FpJmDeEVECEBWjEyB/MVLYfXi+UQeSHTTu2Tg4VDHHmzbjOqN6hYeLQ== - dependencies: - openapi-types "^12.1.3" - -openapi-framework@^12.1.3: - version "12.1.3" - resolved "https://registry.yarnpkg.com/openapi-framework/-/openapi-framework-12.1.3.tgz#11220cb2c91b4927b5b19de4caa12470e2d06443" - integrity sha512-p30PHWVXda9gGxm+t/1X2XvEcufW1YhzeDQwc5SsgDnBXt8gkuu1SwrioGJ66wxVYEzfSRTTf/FMLhI49ut8fQ== - dependencies: - difunc "0.0.4" - fs-routes "^12.1.3" - glob "*" - is-dir "^1.0.0" - js-yaml "^3.10.0" - openapi-default-setter "^12.1.3" - openapi-request-coercer "^12.1.3" - openapi-request-validator "^12.1.3" - openapi-response-validator "^12.1.3" - openapi-schema-validator "^12.1.3" - openapi-security-handler "^12.1.3" - openapi-types "^12.1.3" - ts-log "^2.1.4" - -openapi-jsonschema-parameters@^12.1.3: - version "12.1.3" - resolved "https://registry.yarnpkg.com/openapi-jsonschema-parameters/-/openapi-jsonschema-parameters-12.1.3.tgz#4d06ea53abdc25070f6700150046ed76ec12ec05" - integrity sha512-aHypKxWHwu2lVqfCIOCZeJA/2NTDiP63aPwuoIC+5ksLK5/IQZ3oKTz7GiaIegz5zFvpMDxDvLR2DMQQSkOAug== - dependencies: - openapi-types "^12.1.3" - -openapi-request-coercer@^12.1.3: - version "12.1.3" - resolved "https://registry.yarnpkg.com/openapi-request-coercer/-/openapi-request-coercer-12.1.3.tgz#7a3344e78c3b028763707093f1ea4d96f61434c1" - integrity sha512-CT2ZDhBmAZpHhAzHhEN+/J5oMK3Ds99ayLLdXh2Aw1DCcn72EM8VuIGVwG5fSjvkMsgtn7FgltFosHqeM6PRFQ== - dependencies: - openapi-types "^12.1.3" - ts-log "^2.1.4" - -openapi-request-validator@^12.1.3: - version "12.1.3" - resolved "https://registry.yarnpkg.com/openapi-request-validator/-/openapi-request-validator-12.1.3.tgz#bae467b5c9856e12024e7b50b4c4e54f28c461f4" - integrity sha512-HW1sG00A9Hp2oS5g8CBvtaKvRAc4h5E4ksmuC5EJgmQ+eAUacL7g+WaYCrC7IfoQaZrjxDfeivNZUye/4D8pwA== - dependencies: - ajv "^8.3.0" - ajv-formats "^2.1.0" - content-type "^1.0.4" - openapi-jsonschema-parameters "^12.1.3" - openapi-types "^12.1.3" - ts-log "^2.1.4" - -openapi-response-validator@^12.1.3: - version "12.1.3" - resolved "https://registry.yarnpkg.com/openapi-response-validator/-/openapi-response-validator-12.1.3.tgz#f883a0b1dbb17b929b0c37e3d6c6cebffb9a1806" - integrity sha512-beZNb6r1SXAg1835S30h9XwjE596BYzXQFAEZlYAoO2imfxAu5S7TvNFws5k/MMKMCOFTzBXSjapqEvAzlblrQ== - dependencies: - ajv "^8.4.0" - openapi-types "^12.1.3" - -openapi-schema-validator@^12.1.3: - version "12.1.3" - resolved "https://registry.yarnpkg.com/openapi-schema-validator/-/openapi-schema-validator-12.1.3.tgz#c9234af67b00cdbbecfdd4eb546d7006bacfe518" - integrity sha512-xTHOmxU/VQGUgo7Cm0jhwbklOKobXby+/237EG967+3TQEYJztMgX9Q5UE2taZKwyKPUq0j11dngpGjUuxz1hQ== - dependencies: - ajv "^8.1.0" - ajv-formats "^2.0.2" - lodash.merge "^4.6.1" - openapi-types "^12.1.3" - -openapi-security-handler@^12.1.3: - version "12.1.3" - resolved "https://registry.yarnpkg.com/openapi-security-handler/-/openapi-security-handler-12.1.3.tgz#767e7c26f4a4fc0a3db6e6f9508176b10e71d729" - integrity sha512-25UTAflxqqpjCLrN6rRhINeM1L+MCDixMltiAqtBa9Zz/i7UkWwYwdzqgZY3Cx3vRZElFD09brYxo5VleeP3HQ== - dependencies: - openapi-types "^12.1.3" - -openapi-types@^12.1.3: - version "12.1.3" - resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3" - integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw== - openapi3-ts@^3.1.1: version "3.2.0" resolved "https://registry.yarnpkg.com/openapi3-ts/-/openapi3-ts-3.2.0.tgz#7e30d33c480e938e67e809ab16f419bc9beae3f8" @@ -8787,14 +8568,7 @@ os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== -ot-json1@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/ot-json1/-/ot-json1-1.0.2.tgz#319c98d29af2d0344b84c9b99cbbd95826b16ef7" - integrity sha512-IhxkqVWQqlkWULoi/Q2AdzKk0N5vQRbUMUwubFXFCPcY4TsOZjmp2YKrk0/z1TeiECPadWEK060sdFdQ3Grokg== - dependencies: - ot-text-unicode "4" - -ot-text-unicode@4, ot-text-unicode@^4.0.0: +ot-text-unicode@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/ot-text-unicode/-/ot-text-unicode-4.0.0.tgz#778a327535c81ed265b36ebe1bd677f31bae1e32" integrity sha512-W7ZLU8QXesY2wagYFv47zErXud3E93FGImmSGJsQnBzE+idcPPyo2u2KMilIrTwBh4pbCizy71qRjmmV6aDhcQ== @@ -9295,7 +9069,7 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" -qs@^6.11.0, qs@^6.6.0, qs@^6.9.4: +qs@^6.11.0, qs@^6.6.0: version "6.11.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== @@ -9307,11 +9081,6 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== -querystringify@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" - integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== - queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -9591,7 +9360,7 @@ regenerator-transform@^0.15.2: dependencies: "@babel/runtime" "^7.8.4" -regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: +regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg== @@ -9699,11 +9468,6 @@ require-from-string@^2.0.2: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== - requizzle@^0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/requizzle/-/requizzle-0.2.4.tgz#319eb658b28c370f0c20f968fa8ceab98c13d27c" @@ -9890,11 +9654,6 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" -scmp@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/scmp/-/scmp-2.1.0.tgz#37b8e197c425bdeb570ab91cc356b311a11f9c9a" - integrity sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q== - semver-diff@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" @@ -10089,7 +9848,7 @@ socket.io-parser@~4.2.4: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" -socket.io@*, socket.io@^4.7.2: +socket.io@^4.7.2: version "4.7.2" resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.2.tgz#22557d76c3f3ca48f82e73d68b7add36a22df002" integrity sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw== @@ -10732,11 +10491,6 @@ ts-interface-checker@^0.1.9: resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== -ts-log@^2.1.4: - version "2.2.5" - resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.2.5.tgz#aef3252f1143d11047e2cb6f7cfaac7408d96623" - integrity sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA== - ts-node-dev@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-2.0.0.tgz#bdd53e17ab3b5d822ef519928dc6b4a7e0f13065" @@ -10819,20 +10573,6 @@ twilio-video@^2.28.1: ws "^7.4.6" xmlhttprequest "^1.8.0" -twilio@^4.18.1: - version "4.19.0" - resolved "https://registry.yarnpkg.com/twilio/-/twilio-4.19.0.tgz#b0cc25eb397490ed3e41f031ab5c79697a9065ee" - integrity sha512-4tM1LNM5LeUvnko4kIqIreY6vmjIo5Ag5jMEhjTDPj+GES82MnkfSkJv8N1k5/ZmeSvIdk5hjI87GB/DpDDePQ== - dependencies: - axios "^0.26.1" - dayjs "^1.11.9" - https-proxy-agent "^5.0.0" - jsonwebtoken "^9.0.0" - qs "^6.9.4" - scmp "^2.1.0" - url-parse "^1.5.9" - xmlbuilder "^13.0.2" - 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" @@ -11126,14 +10866,6 @@ url-join@0.0.1: resolved "https://registry.yarnpkg.com/url-join/-/url-join-0.0.1.tgz#1db48ad422d3402469a87f7d97bdebfe4fb1e3c8" integrity sha512-H6dnQ/yPAAVzMQRvEvyz01hhfQL5qRWSEt7BX8t9DqnPw9BjMb64fjIRq76Uvf1hkHp+mTZvEVJ5guXOT0Xqaw== -url-parse@^1.5.9: - version "1.5.10" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" - integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - use-callback-ref@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5" @@ -11180,7 +10912,7 @@ uuid@^8.0.0, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^9.0.0, uuid@^9.0.1: +uuid@^9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== @@ -11533,11 +11265,6 @@ xdg-basedir@^4.0.0: resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== -xmlbuilder@^13.0.2: - version "13.0.2" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-13.0.2.tgz#02ae33614b6a047d1c32b5389c1fdacb2bce47a7" - integrity sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ== - xmlcreate@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-2.0.4.tgz#0c5ab0f99cdd02a81065fa9cd8f8ae87624889be" From 5db960bee9278f0e649de418392d04e68c05254b Mon Sep 17 00:00:00 2001 From: Gabriel Goh <77230723+gycgabriel@users.noreply.github.com> Date: Sun, 12 Nov 2023 17:27:27 +0800 Subject: [PATCH 02/19] Add matching queue debug for assignment 5 --- .../matching-service/src/controllers/matchingController.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/matching-service/src/controllers/matchingController.ts b/services/matching-service/src/controllers/matchingController.ts index 3ea1a409..6f1b1699 100644 --- a/services/matching-service/src/controllers/matchingController.ts +++ b/services/matching-service/src/controllers/matchingController.ts @@ -182,6 +182,7 @@ export function handleLooking( if (!foundMatch) { console.log(`Queued user ${userId}.`); + console.log("In queue:", await prisma.waitingUser.findMany()); return; } @@ -203,6 +204,8 @@ export function handleLooking( } and difficulty ${foundMatch.chosenDifficulty}` ); + console.log("In queue:", await prisma.waitingUser.findMany()); + // Inform both users of the match socket.emit("matchFound", foundMatch); io.to(matchingUser?.socketId || "").emit("matchFound", foundMatch); @@ -212,6 +215,7 @@ export function handleLooking( export function handleCancelLooking(userId: string): () => Promise { return async () => { console.log(`User ${userId} is no longer looking for a match`); + console.log("In queue:", await prisma.waitingUser.findMany()); await prisma.waitingUser.deleteMany({ where: { userId: userId, From e82129c3c356207ead38778d6ef95b88265ba97a Mon Sep 17 00:00:00 2001 From: Gabriel Goh <77230723+gycgabriel@users.noreply.github.com> Date: Sun, 12 Nov 2023 17:50:34 +0800 Subject: [PATCH 03/19] Delete N5 Code editor (#245) --- frontend/package.json | 2 - frontend/src/components/room/code-editor.tsx | 200 ------------------- frontend/src/components/room/video-room.tsx | 162 --------------- frontend/src/pages/attempt/[id]/index.tsx | 91 --------- frontend/src/pages/questions/[id]/index.tsx | 10 - yarn.lock | 24 --- 6 files changed, 489 deletions(-) delete mode 100644 frontend/src/components/room/video-room.tsx delete mode 100644 frontend/src/pages/attempt/[id]/index.tsx diff --git a/frontend/package.json b/frontend/package.json index 8e3d0f22..0b13351f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,7 +15,6 @@ "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@hookform/resolvers": "^3.3.1", - "@monaco-editor/react": "^4.5.2", "@mui/material": "^5.14.14", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-avatar": "^1.0.4", @@ -39,7 +38,6 @@ "firebase": "^10.4.0", "lodash": "^4.17.21", "lucide-react": "^0.279.0", - "monaco-editor": "^0.43.0", "next": "13.4.19", "ot-text-unicode": "^4.0.0", "postcss": "8.4.29", diff --git a/frontend/src/components/room/code-editor.tsx b/frontend/src/components/room/code-editor.tsx index 4046a112..08f10ebb 100644 --- a/frontend/src/components/room/code-editor.tsx +++ b/frontend/src/components/room/code-editor.tsx @@ -1,46 +1,3 @@ -import * as React from "react"; -import { - Check, - ChevronsUpDown, - Undo, - Redo, - Settings, - Play, -} from "lucide-react"; -import Editor, { OnMount } from "@monaco-editor/react"; - -import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, -} from "@/components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { Card } from "../ui/card"; -import { editor } from "monaco-editor"; - -type CodeEditorProps = { - theme?: string; - language?: string; - height?: string; - defaultValue?: string; - className?: string; - text: string; - cursor?: number; - onChange: React.Dispatch>; - onCursorChange?: React.Dispatch>; - hasRoom?: boolean; - onSubmitClick?: (param: string) => void; - onLeaveRoomClick?: () => void; -}; - export const languages = [ { value: "python", @@ -55,160 +12,3 @@ export const languages = [ label: "c++", }, ]; - -export default function CodeEditor({ - theme = "vs-dark", - language = "python", - height = "70vh", - defaultValue = "#Write your solution here", - className, - text, - cursor, - onChange, - onCursorChange, - hasRoom = true, - onSubmitClick = () => {}, - onLeaveRoomClick = () => {}, -}: CodeEditorProps) { - const [open, setOpen] = React.useState(false); - const [value, setValue] = React.useState(""); - const [isSubmitting, setIsSubmitting] = React.useState(false); - - const [monacoInstance, setMonacoInstance] = - React.useState(null); - - const editorMount: OnMount = (editorL: editor.IStandaloneCodeEditor) => { - setMonacoInstance(editorL); - }; - - const setCursorPosition = React.useCallback( - (cursor: number) => { - if (!monacoInstance) return; - - const position = monacoInstance.getModel()!.getPositionAt(cursor); - monacoInstance.setPosition(position); - }, - [monacoInstance] - ); - - React.useEffect(() => { - if (cursor !== undefined) { - setCursorPosition(cursor); - } - }, [cursor, setCursorPosition]); - - const editorOnChange = React.useCallback( - (value: string | undefined) => { - if (!monacoInstance) return; - if (value === undefined) return; - if (onCursorChange === undefined) return; - - if (monacoInstance.getPosition()) { - const cursor = monacoInstance - .getModel()! - .getOffsetAt(monacoInstance.getPosition()!); - onCursorChange(cursor); - } - onChange(value); - }, - [onChange, onCursorChange, monacoInstance] - ); - - const handleOnSubmitClick = async () => { - if (isSubmitting) { - return; // Do nothing if a submission is already in progress. - } - setIsSubmitting(true); - try { - onSubmitClick(monacoInstance?.getValue() ?? value); - } catch (error) { - console.log(error); - } - }; - - return ( -
-
- - - - - - - - No framework found. - - {languages.map((framework) => ( - { - setValue(currentValue === value ? "" : currentValue); - setOpen(false); - }} - > - - {framework.label} - - ))} - - - - -
- - - -
-
- editorOnChange(e)} - onMount={editorMount} - /> - -
- {hasRoom ? ( - - ) : ( - - )} -
-
-
- ); -} diff --git a/frontend/src/components/room/video-room.tsx b/frontend/src/components/room/video-room.tsx deleted file mode 100644 index 5a322815..00000000 --- a/frontend/src/components/room/video-room.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { LocalParticipant, LocalVideoTrack, Participant, RemoteParticipant, RemoteAudioTrack, RemoteVideoTrack, Room, Track } from 'twilio-video'; -import { Button } from '../ui/button'; -import { Mic, MicOff, Video, VideoOff } from 'lucide-react'; - -interface VideoRoomProps { - room: Room | null; - className?: string; -} - -function SingleVideoTrack({ track, userId, isLocal, isMute, toggleMute, isCameraOn, toggleCamera }: - { - track: RemoteVideoTrack | LocalVideoTrack, userId: string, isLocal: boolean, - isMute: boolean, toggleMute: () => void, - isCameraOn: boolean, toggleCamera: () => void - }) { - const videoContainer = useRef(null); - useEffect(() => { - const videoElement = track.attach(); - videoElement.classList.add("w-full", "h-full", "items-center", "justify-center", "flex"); - videoContainer.current?.appendChild(videoElement); - return () => { - track.detach().forEach(element => element.remove()); - videoElement.remove(); - }; - }, [isLocal, track]); - return (
-
-
-
-

{userId}

- {isLocal ?
- - -
: null} -
-
-
); -} - -function SingleAudioTrack({track}: {track: RemoteAudioTrack}) { - const audioContainer = useRef(null); - useEffect(() => { - const audioElement = track.attach(); - audioContainer.current?.appendChild(audioElement); - return () => { - track.detach().forEach(element => element.remove()); - audioElement.remove(); - }; - }, [track]); - return (
); -} - -const VideoRoom: React.FC = ({ room, className }) => { - const [isCameraOn, setIsCameraOn] = useState(false); - const [isMute, setIsMute] = useState(true); - const [participants, setParticipants] = useState([]); - const [localParticipant, setLocalParticipant] = useState(null); - - - const handleNewParticipant = (participant: RemoteParticipant) => { - - participant.on('trackSubscribed', track => { - setParticipants(p => [...p]) - }); - - participant.on('trackUnsubscribed', track => { - setParticipants(p => [...p]) - }); - }; - - const participantConnected = (participant: RemoteParticipant) => { - console.log('Participant "%s" connected,', participant.identity); - - setParticipants(participants => [...participants, participant]); - - handleNewParticipant(participant); - }; - - const participantDisconnected = (participant: RemoteParticipant) => { - console.log('Participant "%s" disconnected', participant.identity); - participant.removeAllListeners(); - setParticipants(participants.filter(p => p.identity !== participant.identity)); - }; - - const toggleCamera = () => { - room?.localParticipant.videoTracks.forEach(publication => { - if (publication.track) { - publication.track.enable(!isCameraOn); - setIsCameraOn(!isCameraOn); - } - }); - }; - - const toggleMute = () => { - room?.localParticipant.audioTracks.forEach(publication => { - if (publication.track) { - publication.track.enable(isMute); - setIsMute(!isMute); - } - }); - }; - - useEffect(() => { - if (!room) return; - - room.on('participantConnected', participantConnected); - room.on('participantDisconnected', participantDisconnected); - room.once('disconnected', error => room.participants.forEach(participantDisconnected)); - - setLocalParticipant(room.localParticipant); - - room.participants.forEach(handleNewParticipant); - - setParticipants(Array.from(room.participants.values())); - - return () => { - room.disconnect(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [room]); - - return ( -
-
- {localParticipant ? Array.from(localParticipant.videoTracks.values()).map(publication => { - if (publication.track.kind === 'video') { - return ; - } else { return null; } - }) : null} - {participants.flatMap(participant => { - return Array.from(participant.videoTracks.values()).map(publication => { - if (publication.track?.kind === 'video') { - return ; - } else { - return null; - } - }); - })} - {participants.flatMap(participant => { - return Array.from(participant.audioTracks.values()).map(audioPublication => { - if (audioPublication.track?.kind === 'audio') { - return ; - } else { - return null; - } - }); - })} -
- ); -}; - -export default VideoRoom; diff --git a/frontend/src/pages/attempt/[id]/index.tsx b/frontend/src/pages/attempt/[id]/index.tsx deleted file mode 100644 index 6633ddf1..00000000 --- a/frontend/src/pages/attempt/[id]/index.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; -import { TypographyBody, TypographyCode, TypographyH2 } from "@/components/ui/typography"; -import { ArrowLeft } from "lucide-react"; -import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; -import { Attempt } from "@/types/UserTypes"; -import { useHistory } from "@/hooks/useHistory"; -import { useQuestions } from "@/hooks/useQuestions"; -import { Question } from "@/types/QuestionTypes"; -import { DotWave } from "@uiball/loaders"; - -export default function Page() { - const router = useRouter(); - const attemptId = router.query.id; - const { fetchAttempt } = useHistory(); - const { fetchQuestion } = useQuestions(); - const [attempt, setAttempt] = useState(); - const [question, setQuestion] = useState(); - const [loadingState, setLoadingState] = useState<"loading" | "error" | "success">("loading"); - - useEffect(() => { - if (attemptId === undefined || Array.isArray(attemptId)) { - router.push("/profile"); - return; - } - fetchAttempt(attemptId).then((attempt) => { - if (attempt) { - setAttempt(attempt); - return fetchQuestion(attempt.question_id); - } else { - throw new Error("Attempt not found"); - } - }).then((question) => { - if (question) { - setQuestion(question); - setLoadingState("success"); - } else { - throw new Error("Question not found"); - } - }).catch((err: any) => { - setLoadingState("error"); - console.log(err); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [attemptId]); - - if (attemptId === undefined || Array.isArray(attemptId)) { - return null; - } - - return ( -
-
- - Attempt -
- - { loadingState === "loading" ?
-
: loadingState === "error" ? Error : <> -
- - {question?.title} -
- -
- - {attempt?.time_updated.toLocaleString()} -
- -
- - {attempt?.room_id ? "Interview" : "Solo"} -
- -
- - {attempt?.solved ? "Solved": "Unsolved"} - -
} -
- ) -} diff --git a/frontend/src/pages/questions/[id]/index.tsx b/frontend/src/pages/questions/[id]/index.tsx index 14ac3ac0..95b00931 100644 --- a/frontend/src/pages/questions/[id]/index.tsx +++ b/frontend/src/pages/questions/[id]/index.tsx @@ -1,4 +1,3 @@ -import CodeEditor from "@/components/room/code-editor"; import Description from "@/components/room/description"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { TypographyBody } from "@/components/ui/typography"; @@ -92,15 +91,6 @@ export default function Questions() { /> -
- -
)} diff --git a/yarn.lock b/yarn.lock index fcb14e2c..970c9122 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1896,20 +1896,6 @@ dependencies: lodash "^4.17.21" -"@monaco-editor/loader@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.4.0.tgz#f08227057331ec890fa1e903912a5b711a2ad558" - integrity sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg== - dependencies: - state-local "^1.0.6" - -"@monaco-editor/react@^4.5.2": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.6.0.tgz#bcc68671e358a21c3814566b865a54b191e24119" - integrity sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw== - dependencies: - "@monaco-editor/loader" "^1.4.0" - "@mongodb-js/saslprep@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@mongodb-js/saslprep/-/saslprep-1.1.0.tgz#022fa36620a7287d17acd05c4aae1e5f390d250d" @@ -8188,11 +8174,6 @@ mlly@^1.2.0, mlly@^1.4.0: pkg-types "^1.0.3" ufo "^1.3.0" -monaco-editor@^0.43.0: - version "0.43.0" - resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.43.0.tgz#cb02a8d23d1249ad00b7cffe8bbecc2ac09d4baf" - integrity sha512-cnoqwQi/9fml2Szamv1XbSJieGJ1Dc8tENVMD26Kcfl7xGQWp7OBKMjlwKVGYFJ3/AXJjSOGvcqK7Ry/j9BM1Q== - 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" @@ -9964,11 +9945,6 @@ stackback@0.0.2: resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== -state-local@^1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5" - integrity sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w== - statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" From c604c96e8a2fe6fd1fc27facb12850b8401b1073 Mon Sep 17 00:00:00 2001 From: Gabriel Goh <77230723+gycgabriel@users.noreply.github.com> Date: Sun, 12 Nov 2023 18:14:09 +0800 Subject: [PATCH 04/19] Delete N2 History (#246) --- .../interviews/leaderboard/columns.tsx | 53 ----------- .../interviews/leaderboard/data-table.tsx | 89 ------------------ frontend/src/components/profile/columns.tsx | 49 ---------- frontend/src/hooks/useHistory.tsx | 38 -------- frontend/src/pages/api/historyHandler.ts | 84 ----------------- frontend/src/pages/interviews/index.tsx | 52 ----------- frontend/src/pages/profile/[id]/index.tsx | 28 +----- frontend/src/pages/profile/_profile.tsx | 54 ----------- frontend/src/pages/profile/index.tsx | 34 +++---- frontend/src/pages/questions/[id]/index.tsx | 17 ---- frontend/src/types/UserTypes.ts | 11 --- services/user-service/src/db/functions.ts | 85 ++---------------- services/user-service/src/routes/index.ts | 66 +------------- services/user-service/src/swagger-output.json | 90 +------------------ 14 files changed, 21 insertions(+), 729 deletions(-) delete mode 100644 frontend/src/components/interviews/leaderboard/columns.tsx delete mode 100644 frontend/src/components/interviews/leaderboard/data-table.tsx delete mode 100644 frontend/src/components/profile/columns.tsx delete mode 100644 frontend/src/hooks/useHistory.tsx delete mode 100644 frontend/src/pages/api/historyHandler.ts diff --git a/frontend/src/components/interviews/leaderboard/columns.tsx b/frontend/src/components/interviews/leaderboard/columns.tsx deleted file mode 100644 index 0f8d7b74..00000000 --- a/frontend/src/components/interviews/leaderboard/columns.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Button } from "@/components/ui/button"; - -import { ColumnDef } from "@tanstack/react-table"; - -export type PublicUser = { - displayName: string; - attempts: number; - photoURL: string; -}; - -const getInitials = (name: string) => { - const names = name.split(" "); - let initials = ""; - names.forEach((n) => { - initials += n[0].toUpperCase(); - }); - return initials; -}; - -export const columns: ColumnDef[] = [ - { - accessorKey: "displayName", - header: "User", - cell: ({ row }) => { - const displayName = row.getValue("displayName") as string; - const photoURL = row.original.photoURL; - - return ( - - ); - }, - }, - { - accessorKey: "attempts", - header: "Solved", - invertSorting: true, - }, -]; diff --git a/frontend/src/components/interviews/leaderboard/data-table.tsx b/frontend/src/components/interviews/leaderboard/data-table.tsx deleted file mode 100644 index c6a543b9..00000000 --- a/frontend/src/components/interviews/leaderboard/data-table.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import * as React from "react" - -import { - ColumnDef, - SortingState, - flexRender, - getCoreRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table" - -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table" - -interface DataTableProps { - columns: ColumnDef[] - data: TData[] -} - -export function DataTable({ - columns, - data, -}: DataTableProps) { - const [sorting, setSorting] = React.useState([{ id: "attempts", desc: false }]) - - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - onSortingChange: setSorting, - getSortedRowModel: getSortedRowModel(), - state: { - sorting, - }, - }) - - return ( -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - - No results. - - - )} - -
-
- ) -} diff --git a/frontend/src/components/profile/columns.tsx b/frontend/src/components/profile/columns.tsx deleted file mode 100644 index 2f0cea8f..00000000 --- a/frontend/src/components/profile/columns.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Attempt } from "@/types/UserTypes" -import { ColumnDef } from "@tanstack/react-table" -import { Button } from "../ui/button"; - -export const columns: ColumnDef[] = [ - { - accessorKey: "room_id", - header: "Attempt Type", - cell: ({ row }) => { - const roomId = row.getValue("room_id") as string; - return roomId ? "Interview" : "Solo"; - }, - }, - { - accessorKey: "solved", - header: "Status", - cell: ({ row }) => { - const solved = row.getValue("solved") as boolean; - return (solved ?
Solved
: "Unsolved"); - }, - }, - { - accessorKey: "time_created", - header: "Attempted At", - cell: ({ row }) => { - const timeCreated = row.getValue("time_created") as Date; - return timeCreated.toLocaleString(); - }, - }, - { - id: "actions", - header: "Actions", - cell: ({ row }) => { - const attemptId = row.original.id; - return ( - - ); - }, - }, -] diff --git a/frontend/src/hooks/useHistory.tsx b/frontend/src/hooks/useHistory.tsx deleted file mode 100644 index 1d5a7d37..00000000 --- a/frontend/src/hooks/useHistory.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useContext } from "react"; -import { AuthContext } from "@/contexts/AuthContext"; -import { - getAttemptsOfUser, - createAttemptOfUser, - getAttemptById, -} from "@/pages/api/historyHandler"; - -type AttemptData = { - uid: string; - question_id: string; - answer: string; - solved: boolean; // just set everything as false for now (no need to check) -}; - -export const useHistory = () => { - const { user: currentUser, authIsReady } = useContext(AuthContext); - - const fetchAttempts = async (uid: string) => { - if (authIsReady) { - return getAttemptsOfUser(currentUser, uid); - } - }; - - const postAttempt = async (data: AttemptData) => { - if (authIsReady) { - return createAttemptOfUser(currentUser, data); - } - }; - - const fetchAttempt = async (attemptId: string) => { - if (authIsReady) { - return getAttemptById(currentUser, attemptId); - } - } -; - return { fetchAttempts, fetchAttempt, postAttempt }; -}; diff --git a/frontend/src/pages/api/historyHandler.ts b/frontend/src/pages/api/historyHandler.ts deleted file mode 100644 index 0ddc1a73..00000000 --- a/frontend/src/pages/api/historyHandler.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { userApiPathAddress } from "@/gateway-address/gateway-address"; -import { Attempt } from "@/types/UserTypes"; - -export const getAttemptsOfUser = async (user: any, uid: string) => { - try { - const url = `${userApiPathAddress}${uid}/attempts`; - const idToken = await user.getIdToken(true); - - const response = await fetch(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - "User-Id-Token": idToken, - }, - }); - - if (!response.ok) { - throw new Error(`Unable to get attempts: ${await response.text()}`); - } - return response.json().then((arr: Array) => { - return arr.map(obj => { - obj["time_saved_at"] = new Date(obj["time_saved_at"]); - obj["time_updated"] = new Date(obj["time_updated"]); - obj["time_created"] = new Date(obj["time_created"]); - return obj - }); - }); - } catch (error) { - console.error("There was an error fetching the attempts", error); - throw error; - } -}; - -export const getAttemptById = async (user: any, attemptId: string) => { - try { - const url = `${userApiPathAddress}attempt/${attemptId}`; - const idToken = await user.getIdToken(true); - - const response = await fetch(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - "User-Id-Token": idToken, - }, - }); - - if (!response.ok) { - throw new Error(`Unable to get attempt: ${await response.text()}`); - } - return response.json().then(val => { - val["time_saved_at"] = new Date(val["time_saved_at"]); - val["time_updated"] = new Date(val["time_updated"]); - val["time_created"] = new Date(val["time_created"]); - return val - }); - } catch (error) { - console.error("There was an error fetching the attempt", error); - throw error; - } -}; - -export const createAttemptOfUser = async (user: any, data: any) => { - try { - const url = `${userApiPathAddress}attempt`; - const idToken = await user.getIdToken(true); - - const response = await fetch(url, { - method: "POST", - body: JSON.stringify(data), - headers: { - "Content-Type": "application/json", - "User-Id-Token": idToken, - }, - }); - - if (!response.ok) { - throw new Error(`Unable to create attempt: ${await response.text()}`); - } - return response.json(); - } catch (error) { - console.error("There was an error creating the attempt", error); - throw error; - } -}; diff --git a/frontend/src/pages/interviews/index.tsx b/frontend/src/pages/interviews/index.tsx index 5ea7b28f..fa600a91 100644 --- a/frontend/src/pages/interviews/index.tsx +++ b/frontend/src/pages/interviews/index.tsx @@ -1,6 +1,4 @@ import DifficultySelector from "@/components/common/difficulty-selector"; -import { columns } from "@/components/interviews/leaderboard/columns"; -import { DataTable } from "@/components/interviews/leaderboard/data-table"; import { languages } from "@/components/room/code-editor"; import { Button } from "@/components/ui/button"; import { @@ -31,49 +29,6 @@ import { useContext, useEffect, useState } from "react"; type Difficulty = "easy" | "medium" | "hard" | "any"; -const leaderboardData = [ - { - displayName: "John Doe", - attempts: 10, - photoURL: "https://i.pravatar.cc/300", - }, - { - displayName: "Mary Jane", - attempts: 1, - photoURL: "https://i.pravatar.cc/301", - }, - { - displayName: "Mark Rober", - attempts: 98, - photoURL: "https://i.pravatar.cc/302", - }, - { - displayName: "Alice Smith", - attempts: 15, - photoURL: "https://i.pravatar.cc/303", - }, - { - displayName: "Bob Johnson", - attempts: 22, - photoURL: "https://i.pravatar.cc/304", - }, - { - displayName: "Charlie Brown", - attempts: 5, - photoURL: "https://i.pravatar.cc/305", - }, - { - displayName: "Daisy Miller", - attempts: 40, - photoURL: "https://i.pravatar.cc/306", - }, - { - displayName: "Edward Stone", - attempts: 60, - photoURL: "https://i.pravatar.cc/307", - }, -]; - export default function Interviews() { const { user: currentUser } = useContext(AuthContext); const [open, setOpen] = useState(false); @@ -102,8 +57,6 @@ export default function Interviews() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentUser]); - // sync leaderboard data - const onClickSearch = () => { try { joinQueue( @@ -207,11 +160,6 @@ export default function Interviews() { Practice with a peer! -
- Leaderboard - -
-
); diff --git a/frontend/src/pages/profile/[id]/index.tsx b/frontend/src/pages/profile/[id]/index.tsx index 20b2d856..1bdd5041 100644 --- a/frontend/src/pages/profile/[id]/index.tsx +++ b/frontend/src/pages/profile/[id]/index.tsx @@ -1,8 +1,6 @@ -import Profile, {UserProfile} from "../_profile"; +import Profile, { UserProfile } from "../_profile"; import { useContext, useEffect, useState } from "react"; import { AuthContext } from "@/contexts/AuthContext"; -import { Attempt } from "@/types/UserTypes"; -import { useHistory } from "@/hooks/useHistory"; import { useRouter } from "next/router"; import { useUser } from "@/hooks/useUser"; @@ -11,42 +9,18 @@ export default function Page() { const id = router.query.id; const { getAppUser } = useUser(); const { user: currentUser } = useContext(AuthContext); - const { fetchAttempts } = useHistory(); - const [attempts, setAttempts] = useState([]); const [user, setUser] = useState(); const [loadingState, setLoadingState] = useState< "loading" | "error" | "success" >("loading"); - useEffect(() => { - if (currentUser && typeof id === "string") { - Promise.all([getAppUser(id), fetchAttempts(id)]) - .then(([user, attempts]) => { - if (user && attempts) { - console.log(user); - setUser({...user, photoURL: user.photoUrl}); - setAttempts(attempts); - setLoadingState("success"); - } else { - throw new Error("User or attempts not found"); - } - }) - .catch((err: any) => { - setLoadingState("error"); - console.log(err); - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentUser]); - return ( user && ( ) ); diff --git a/frontend/src/pages/profile/_profile.tsx b/frontend/src/pages/profile/_profile.tsx index 5f8f0541..16605c84 100644 --- a/frontend/src/pages/profile/_profile.tsx +++ b/frontend/src/pages/profile/_profile.tsx @@ -5,15 +5,10 @@ import { TypographyH3, TypographyH2, } from "@/components/ui/typography"; -import { Attempt } from "@/types/UserTypes"; -import { User } from "firebase/auth"; import Link from "next/link"; import ActivityCalendar, { Activity } from "react-activity-calendar"; import { Tooltip as MuiTooltip } from "@mui/material"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { DataTable } from "@/components/profile/data-table"; -import { columns } from "@/components/profile/columns"; -import { DotWave } from "@uiball/loaders"; export type UserProfile = { uid: string; @@ -25,13 +20,11 @@ export type UserProfile = { type ProfileProps = { selectedUser: UserProfile; loadingState: "loading" | "error" | "success"; - attempts?: Attempt[]; isCurrentUser: boolean; }; export default function Profile({ selectedUser, - attempts, isCurrentUser, loadingState, }: ProfileProps) { @@ -57,33 +50,6 @@ export default function Profile({ [dateLastYearString]: { date: dateLastYearString, count: 0, level: 0 }, }; - if (loadingState === "success") { - // Transform attempts into activities and accumulate counts - attempts?.forEach((attempt) => { - const date = attempt.time_created.toISOString().slice(0, 10); // Format the date as yyyy-MM-dd - - if (!countsByDate[date]) { - countsByDate[date] = { date, count: 0, level: 1 }; - } - - countsByDate[date].count += 1; - - // Set the level of the activity based on the number of activities on that day - if (countsByDate[date].count === 1) { - countsByDate[date].level = 1; - } else if (countsByDate[date].count > 1 && countsByDate[date].count < 5) { - countsByDate[date].level = 2; - } else if ( - countsByDate[date].count >= 5 && - countsByDate[date].count < 10 - ) { - countsByDate[date].level = 3; - } else if (countsByDate[date].count >= 10) { - countsByDate[date].level = 4; - } - }); - } - // Extract the values from the dictionary to get the final activities array const activities = Object.values(countsByDate).sort((a, b) => a.date.localeCompare(b.date) @@ -138,26 +104,6 @@ export default function Profile({ /> - - - Attempts - - - {loadingState === "loading" ? ( -
- -
- ) : loadingState === "error" ? ( -
- - Something went wrong. Please try again later. - -
- ) : ( - - )} -
-
); diff --git a/frontend/src/pages/profile/index.tsx b/frontend/src/pages/profile/index.tsx index a8e16192..3faa0b94 100644 --- a/frontend/src/pages/profile/index.tsx +++ b/frontend/src/pages/profile/index.tsx @@ -1,32 +1,20 @@ import { useContext, useEffect, useState } from "react"; import { AuthContext } from "@/contexts/AuthContext"; import Profile from "./_profile"; -import { Attempt } from "@/types/UserTypes"; -import { useHistory } from "@/hooks/useHistory"; export default function Page() { const { user: currentUser } = useContext(AuthContext); - const { fetchAttempts } = useHistory(); - - const [attempts, setAttempts] = useState([]); - const [loadingState, setLoadingState] = useState<"loading" | "error" | "success">("loading"); - - useEffect(() => { - if (currentUser) { - fetchAttempts(currentUser.uid).then((attempts) => { - if (attempts) { - setAttempts(attempts); - setLoadingState("success"); - } - }).catch((err: any) => { - setLoadingState("error"); - console.log(err); - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentUser]); + const [loadingState, setLoadingState] = useState< + "loading" | "error" | "success" + >("loading"); return ( - (currentUser && ) - ) + currentUser && ( + + ) + ); } diff --git a/frontend/src/pages/questions/[id]/index.tsx b/frontend/src/pages/questions/[id]/index.tsx index 95b00931..15f25870 100644 --- a/frontend/src/pages/questions/[id]/index.tsx +++ b/frontend/src/pages/questions/[id]/index.tsx @@ -7,12 +7,10 @@ import { Question } from "../../../types/QuestionTypes"; import { AuthContext } from "@/contexts/AuthContext"; import { fetchQuestion } from "../../api/questionHandler"; import { MrMiyagi } from "@uiball/loaders"; -import { useHistory } from "@/hooks/useHistory"; import Solution from "@/components/room/solution"; export default function Questions() { const router = useRouter(); - const { postAttempt } = useHistory(); const questionId = router.query.id as string; const [question, setQuestion] = useState(null); const [loading, setLoading] = useState(true); // to be used later for loading states @@ -42,21 +40,6 @@ export default function Questions() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [questionId, authIsReady, currentUser]); - function onSubmitClick(value: string) { - postAttempt({ - uid: currentUser ? currentUser.uid : "user", - question_id: questionId, - answer: value || answer, - solved: true, // assume true - }) - .catch((err: any) => { - console.log(err); - }) - .finally(() => { - router.push("/profile"); - }); - } - if (question === null && !loading) return

Question not found

; return ( diff --git a/frontend/src/types/UserTypes.ts b/frontend/src/types/UserTypes.ts index bf980e18..939a3673 100644 --- a/frontend/src/types/UserTypes.ts +++ b/frontend/src/types/UserTypes.ts @@ -5,14 +5,3 @@ export type EditableUser = { matchDifficulty?: string | null; matchProgrammingLanguage?: string | null; }; - -export type Attempt = { - id: string; - question_id: string; - answer: string | null; - solved: boolean; - time_created: Date; - time_saved_at: Date; - time_updated: Date; - room_id: string | null; -}; diff --git a/services/user-service/src/db/functions.ts b/services/user-service/src/db/functions.ts index 7eaf01fe..bb41d586 100644 --- a/services/user-service/src/db/functions.ts +++ b/services/user-service/src/db/functions.ts @@ -52,84 +52,13 @@ const userDatabaseFunctions = { }); }, - async getAttemptsOfUser(uid: string) { - try { - const user = await prismaClient.appUser.findUnique({ - where: { - uid: uid, - }, - include: { - attempts: true, - }, - }); - - if (user) { - return user.attempts; - } else { - console.error(`User with uid ${uid} not found.`); - return []; - } - } catch (error: any) { - console.error(`Error retrieving attempts: ${error.message}`); - throw error; + async setMatchPreferenceOfUser( + uid: string, + data: { + matchDifficulty: string; + matchProgrammingLanguage: string; } - }, - - async getAttemptById(attemptId: string) { - try { - const attempt = await prismaClient.attempt.findUnique({ - where: { - id: attemptId, - }, - }); - return attempt; - } catch (error: any) { - console.error(`Error retrieving attempt: ${error.message}`); - throw error; - } - }, - - async createAttemptOfUser(data: { - uid: string; - question_id: string; - answer: string; - solved: boolean; - }) { - try { - const user = await prismaClient.appUser.findUnique({ - where: { - uid: data.uid, - }, - }); - - if (user) { - const attempt = await prismaClient.attempt.create({ - data: { - question_id: data.question_id, - answer: data.answer, - solved: data.solved, - users: { - connect: { - uid: data.uid, - }, - }, - }, - }); - return attempt; - } else { - console.error(`User with uid ${data.uid} not found.`); - return null; - } - } catch (error: any) { - console.error(`Error creating attempt: ${error.message}`); - throw error; - } - }, - - async setMatchPreferenceOfUser(uid: string, data: { - matchDifficulty: string; - matchProgrammingLanguage: string; - }) { + ) { try { const updatedResult = await prismaClient.appUser.update({ where: { @@ -145,7 +74,7 @@ const userDatabaseFunctions = { console.error(`Error setting match preference: ${error.message}`); throw error; } - } + }, }; export default userDatabaseFunctions; diff --git a/services/user-service/src/routes/index.ts b/services/user-service/src/routes/index.ts index 15f608c2..8d5f0f69 100644 --- a/services/user-service/src/routes/index.ts +++ b/services/user-service/src/routes/index.ts @@ -108,73 +108,9 @@ indexRouter.delete( // Server side error such as database not being available res.status(500).end(); } - }) + }); } } ); -indexRouter.get( - "/:uid/attempts", - function (req: express.Request, res: express.Response) { - userDatabaseFunctions - .getAttemptsOfUser(req.params.uid) - .then((result) => { - if (result === null) { - // res.status(404).end(); - res.send(200).json([]); - } else { - res.status(200).json(result); - } - }) - .catch((err) => { - console.log(err); - // Server side error such as database not being available - res.status(500).end(); - }); - } -); - -indexRouter.get( - "/attempt/:attempt_id", - function (req: express.Request, res: express.Response) { - userDatabaseFunctions - .getAttemptById(req.params.attempt_id) - .then((result) => { - if (result === null) { - res.status(404).end(); - } else { - res.status(200).json(result); - } - }) - .catch((err) => { - console.log(err); - // Server side error such as database not being available - res.status(500).end(); - }); - } -); - -indexRouter.post( - "/attempt", - function (req: express.Request, res: express.Response) { - const uid = req.body.uid as string; - const question_id = req.body.question_id as string; - const answer = req.body.answer as string; - const solved = (req.body.solved as boolean) ?? false; - userDatabaseFunctions - .createAttemptOfUser(req.body) - .then((result) => { - if (result === null) { - res.status(404).append("No-Such-User", "true").end(); - } else { - res.status(201).json(result); - } - }) - .catch((error) => { - console.log(error); - res.status(500).end(); - }); - } -); - export default indexRouter; diff --git a/services/user-service/src/swagger-output.json b/services/user-service/src/swagger-output.json index afcf18a9..2d77ba2e 100644 --- a/services/user-service/src/swagger-output.json +++ b/services/user-service/src/swagger-output.json @@ -106,94 +106,6 @@ } } } - }, - "/api/user-service/{uid}/attempts": { - "get": { - "description": "", - "parameters": [ - { - "name": "uid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "500": { - "description": "Internal Server Error" - } - } - } - }, - "/api/user-service/attempt/{attempt_id}": { - "get": { - "description": "", - "parameters": [ - { - "name": "attempt_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "404": { - "description": "Not Found" - }, - "500": { - "description": "Internal Server Error" - } - } - } - }, - "/api/user-service/attempt": { - "post": { - "description": "", - "responses": { - "201": { - "description": "Created" - }, - "404": { - "description": "Not Found" - }, - "500": { - "description": "Internal Server Error" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "uid": { - "example": "any" - }, - "question_id": { - "example": "any" - }, - "answer": { - "example": "any" - }, - "solved": { - "example": "any" - } - } - } - } - } - } - } } } -} \ No newline at end of file +} From 589cdfd6befbd7a34675214766c0c59d98e9935b Mon Sep 17 00:00:00 2001 From: Gabriel Goh <77230723+gycgabriel@users.noreply.github.com> Date: Sun, 12 Nov 2023 18:25:25 +0800 Subject: [PATCH 05/19] Remove shared ot --- utils/shared-ot.ts | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 utils/shared-ot.ts diff --git a/utils/shared-ot.ts b/utils/shared-ot.ts deleted file mode 100644 index a1b4c33e..00000000 --- a/utils/shared-ot.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { diff_match_patch } from "diff-match-patch"; -import { insert, remove, TextOp } from "ot-text-unicode"; - -export interface TextOperationSet { - version: number; - operations: TextOp; -} - -export interface TextOperationSetWithCursor extends TextOperationSet { - cursor?: number; -} - -export function createTextOpFromTexts( - prevText: string, - currentText: string -): TextOp { - const dmp = new diff_match_patch(); - const diffs = dmp.diff_main(prevText, currentText); - //dmp.diff_cleanupSemantic(diffs); - - var textop: TextOp = []; - - var skipn: number = 0; - - for (const [operation, text] of diffs) { - if (operation === 0) { - skipn += text.length; - } else if (operation === -1) { - textop = [...textop, ...remove(skipn, text)]; - skipn = 0; - } else { - textop = [...textop, ...insert(skipn, text)]; - skipn = 0; - } - } - return textop; -} From bf0355133869cb911d9729322b6973b99ad43e88 Mon Sep 17 00:00:00 2001 From: Tay Yi Hsuen Date: Mon, 13 Nov 2023 21:52:03 +0800 Subject: [PATCH 06/19] Remove test and workflow files (#251) --------- Co-authored-by: chunweii <47494777+chunweii@users.noreply.github.com> --- .github/workflows/integration.yml | 68 ------ .github/workflows/production.yml | 130 ------------ README.md | 3 +- .../prod-dockerfiles/Dockerfile.frontend | 3 +- firebase.json | 11 - .../components/common/difficulty-selector.tsx | 13 ++ frontend/src/components/questions/columns.tsx | 14 +- frontend/src/components/room/description.tsx | 5 +- frontend/src/pages/api/questionHandler.ts | 6 + frontend/src/pages/questions/[id]/index.tsx | 4 + .../src/providers/MatchmakingProvider.tsx | 1 + services/admin-service/README.md | 16 -- services/admin-service/systemtest/app.test.ts | 135 ------------ .../systemtest/vitest.config.system.ts | 7 - .../firebase-server/firebaseWrappers.test.ts | 70 ------ .../admin-service/test/vitest.config.unit.ts | 7 - .../src/controllers/matchingController.ts | 14 +- services/user-service/README.md | 60 ------ services/user-service/systemtest/app.test.ts | 105 --------- .../user-service-postgre-Docker-compose.yml | 11 - .../systemtest/vitest.config.system.ts | 9 - .../user-service/test/db/functions.test.ts | 41 ---- .../user-service/test/routes/index.test.ts | 200 ------------------ .../user-service/test/vitest.config.unit.ts | 7 - 24 files changed, 37 insertions(+), 903 deletions(-) delete mode 100644 .github/workflows/integration.yml delete mode 100644 .github/workflows/production.yml delete mode 100644 firebase.json delete mode 100644 services/admin-service/systemtest/app.test.ts delete mode 100644 services/admin-service/systemtest/vitest.config.system.ts delete mode 100644 services/admin-service/test/firebase-server/firebaseWrappers.test.ts delete mode 100644 services/admin-service/test/vitest.config.unit.ts delete mode 100644 services/user-service/systemtest/app.test.ts delete mode 100644 services/user-service/systemtest/user-service-postgre-Docker-compose.yml delete mode 100644 services/user-service/systemtest/vitest.config.system.ts delete mode 100644 services/user-service/test/db/functions.test.ts delete mode 100644 services/user-service/test/routes/index.test.ts delete mode 100644 services/user-service/test/vitest.config.unit.ts diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml deleted file mode 100644 index 9be0dad3..00000000 --- a/.github/workflows/integration.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Continuous Integration - -on: - push: - branches-ignore: - - gh-pages - -env: - NODE_VER: "18.x" - JAVA_DISTRIBUTION: "zulu" - JAVA_VER: 11 - FIREBASE_AUTH_EMULATOR_HOST: "127:0:0:1:9099" - FIREBASE_TOKEN: ${{ secrets.FIREBASE_CI_TOKEN }} - FIREBASE_SERVICE_ACCOUNT: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }} - PRISMA_DATABASE_URL: ${{ secrets.SYSTEMTEST_DATABASE_URL }} - PRISMA_DATABASE_PASSWORD: ${{ secrets.SYSTEMTEST_DATABASE_PASSWORD }} - NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG: ${{ secrets.FRONTEND_FIREBASE_CONFIG_DEV }} - -jobs: - mainbuild: - name: CI on Ubuntu 22.04 - runs-on: ubuntu-22.04 - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: ${{ env.NODE_VER }} - - - name: Set up JDK 11 - uses: actions/setup-java@v3 - with: - distribution: ${{ env.JAVA_DISTRIBUTION }} - java-version: ${{ env.JAVA_VER }} - - - name: Install dependencies with immutable lockfile - run: yarn install --frozen-lockfile - - - name: Run linting - run: | - yarn workspace admin-service lint - yarn workspace frontend lint - yarn workspace matching-service lint - yarn workspace question-service lint - yarn workspace user-service lint - - - name: Run unit tests - run: | - yarn workspace user-service test - yarn workspace admin-service test:ci - - - name: Run system tests - run: | - yarn workspace user-service systemtest:ci - yarn workspace admin-service systemtest:ci - - - name: Simulate production build - run: | - yarn workspace admin-service build - yarn workspace matching-service build - yarn workspace question-service build - yarn workspace user-service build - yarn workspace frontend build diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml deleted file mode 100644 index 154ebd42..00000000 --- a/.github/workflows/production.yml +++ /dev/null @@ -1,130 +0,0 @@ -# Adapted from: https://github.com/actions/starter-workflows/blob/main/deployments/google.yml - -name: Build and Deploy Production App - -on: - workflow_run: - workflows: ["Continuous Integration"] # Run only after CI passes - types: [completed] - branches: - - prod - -env: - PROJECT_ID: peerprep-group11-prod - ARTIFACT_REPOSITORY_NAME: codeparty-prod-images - GKE_CLUSTER: codeparty-g11-prod # Add your cluster name here. - GKE_REGION: asia-southeast1 # Add your cluster zone here. - FIREBASE_SERVICE_ACCOUNT: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_PROD }} - PRISMA_DATABASE_URL: ${{ secrets.PRISMA_DATABASE_URL_PROD }} - MONGO_ATLAS_URL: ${{ secrets.MONGO_ATLAS_URL_PROD }} - NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG: ${{ secrets.FRONTEND_FIREBASE_CONFIG_PROD }} - TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }} - TWILIO_API_KEY: ${{ secrets.TWILIO_API_KEY }} - TWILIO_API_SECRET: ${{ secrets.TWILIO_API_SECRET }} - -jobs: - setup-build-publish-deploy: - name: Setup, Build, Publish, and Deploy - runs-on: ubuntu-latest - environment: production - if: ${{ github.event.workflow_run.conclusion == 'success' }} - permissions: - contents: "read" - id-token: "write" - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - id: "auth" - name: Authenticate to Google Cloud - uses: "google-github-actions/auth@v1" - with: - token_format: "access_token" - workload_identity_provider: projects/345207492413/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-oidc - service_account: "github-actions-service@peerprep-group11-prod.iam.gserviceaccount.com" - - # Setup gcloud CLI - - name: Setup Google Cloud SDK - uses: google-github-actions/setup-gcloud@v1 - - # Configure Docker to login to google cloud - - name: Configure Docker - run: |- - echo ${{steps.auth.outputs.access_token}} | docker login -u oauth2accesstoken --password-stdin https://$GKE_REGION-docker.pkg.dev - - # Get the GKE credentials so that we can deploy to the cluster - - name: Get Google Kubernetes Engine credentials for production - uses: google-github-actions/get-gke-credentials@v1 - with: - cluster_name: ${{ env.GKE_CLUSTER }} - location: ${{ env.GKE_REGION }} - - # Copy the JSON secrets (Firebase configs) into JSON files - - name: Copy JSON secrets into JSON files - run: |- - echo -n "$FIREBASE_SERVICE_ACCOUNT" > ./firebase_service_account.json - echo -n "$NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG" > ./next_public_frontend_firebase_config.json - - # Set the secrets that are used as env variables in the manifest files - - name: Set kubectl secrets - run: |- - kubectl delete secret firebase-service-account \ - --ignore-not-found - kubectl create secret generic firebase-service-account \ - --from-file=firebase-service-account=./firebase_service_account.json - kubectl delete secret prisma-database-url \ - --ignore-not-found - kubectl create secret generic prisma-database-url \ - --from-literal=prisma-database-url=$PRISMA_DATABASE_URL - kubectl delete secret mongo-atlas-url \ - --ignore-not-found - kubectl create secret generic mongo-atlas-url \ - --from-literal=mongo-atlas-url=$MONGO_ATLAS_URL - kubectl delete secret frontend-firebase-config \ - --ignore-not-found - kubectl create secret generic frontend-firebase-config \ - --from-file=frontend-firebase-config=./next_public_frontend_firebase_config.json - kubectl delete secret twilio-account-sid \ - --ignore-not-found - kubectl create secret generic twilio-account-sid \ - --from-literal=twilio-account-sid=$TWILIO_ACCOUNT_SID - kubectl delete secret twilio-api-key \ - --ignore-not-found - kubectl create secret generic twilio-api-key \ - --from-literal=twilio-api-key=$TWILIO_API_KEY - kubectl delete secret twilio-api-secret \ - --ignore-not-found - kubectl create secret generic twilio-api-secret \ - --from-literal=twilio-api-secret=$TWILIO_API_SECRET - - # Remove the JSON files - - name: Delete JSON files - if: ${{ always() }} - run: |- - rm ./firebase_service_account.json - rm ./next_public_frontend_firebase_config.json - - # Install the dependencies such as prisma - - name: Install dependencies with immutable lockfile - run: yarn install --frozen-lockfile - - # Apply prisma migrations to production prisma database - - name: Apply prisma database migrations - run: |- - yarn prisma migrate deploy - - # Build the Docker images and push to Google Artifact Repository - - name: Build and push Docker images - run: |- - chmod u+x ./build-export-prod-images.sh - ./build-export-prod-images.sh - working-directory: ./deployment - - # Deploy the Docker images to the GKE cluster - - name: Deploy production application - run: |- - kubectl apply -f ./gke-prod-manifests - kubectl rollout status deployment - kubectl get services -o wide - working-directory: ./deployment diff --git a/README.md b/README.md index 9e3a6dad..7ec3b76c 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,7 @@ your services / frontend. ├── /frontend │ └── /pages for peerprep (NextJs application) ├── /deployment -│ ├── /docker -│ └── /kubernetes +│ └── /prod-dockerfiles (Images can be used with either dev or prod environments) ├── .env (not in git) ├── .env.firebase_emulators_test (not in git) └── README.md (and other root-level files & docs) diff --git a/deployment/prod-dockerfiles/Dockerfile.frontend b/deployment/prod-dockerfiles/Dockerfile.frontend index 243f27e3..c7d46d70 100644 --- a/deployment/prod-dockerfiles/Dockerfile.frontend +++ b/deployment/prod-dockerfiles/Dockerfile.frontend @@ -2,7 +2,7 @@ FROM peerprep-base:latest # Copy utils -COPY utils /app/utils/ +# COPY utils /app/utils/ # Set working directory for frontend WORKDIR /app/frontend @@ -10,7 +10,6 @@ WORKDIR /app/frontend # Copy the entire frontend directory and prisma COPY frontend /app/frontend COPY prisma ./prisma/ -COPY utils /app/utils/ # Install all dependencies using Yarn Workspaces RUN yarn install --frozen-lockfile --cwd /app diff --git a/firebase.json b/firebase.json deleted file mode 100644 index 19a66cdf..00000000 --- a/firebase.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "emulators": { - "singleProjectMode": true, - "auth": { - "port": 9099 - }, - "ui": { - "enabled": false - } - } -} diff --git a/frontend/src/components/common/difficulty-selector.tsx b/frontend/src/components/common/difficulty-selector.tsx index 5f1ea278..009e8cfd 100644 --- a/frontend/src/components/common/difficulty-selector.tsx +++ b/frontend/src/components/common/difficulty-selector.tsx @@ -47,3 +47,16 @@ export default function DifficultySelector({ ); } + +export const getDifficultyColor = (difficulty: Difficulty) => { + switch (difficulty) { + case "easy": + return "text-green-500"; + case "medium": + return "text-orange-500"; + case "hard": + return "text-red-500"; + default: + return "text-gray-500"; + } +}; diff --git a/frontend/src/components/questions/columns.tsx b/frontend/src/components/questions/columns.tsx index 230b0a69..72965f6f 100644 --- a/frontend/src/components/questions/columns.tsx +++ b/frontend/src/components/questions/columns.tsx @@ -4,6 +4,7 @@ import { Badge } from "../ui/badge"; import { Button } from "../ui/button"; import { EditIcon, PlayIcon, ArrowUpDown } from "lucide-react"; import { Difficulty, Question } from "../../types/QuestionTypes"; +import { getDifficultyColor } from "../common/difficulty-selector"; export const getColumnDefs: (isEditable: boolean) => ColumnDef[] = isEditable => [ { @@ -84,16 +85,3 @@ export const getColumnDefs: (isEditable: boolean) => ColumnDef[] = isE enableHiding: false, }, ]; - -const getDifficultyColor = (difficulty: Difficulty) => { - switch (difficulty) { - case "easy": - return "text-green-500"; - case "medium": - return "text-orange-500"; - case "hard": - return "text-red-500"; - default: - return "text-gray-500"; - } -}; diff --git a/frontend/src/components/room/description.tsx b/frontend/src/components/room/description.tsx index 612ce687..9ede381b 100644 --- a/frontend/src/components/room/description.tsx +++ b/frontend/src/components/room/description.tsx @@ -1,4 +1,4 @@ -import { Question } from "../../types/QuestionTypes"; +import { Difficulty, Question } from "@/types/QuestionTypes"; import { Badge } from "../ui/badge"; import { Button } from "../ui/button"; import { Card } from "../ui/card"; @@ -6,6 +6,7 @@ import { TypographyH2, TypographySmall } from "../ui/typography"; import sanitizeHtml from "sanitize-html"; import Markdown from 'react-markdown' import rehypeRaw from 'rehype-raw' +import { getDifficultyColor } from "../common/difficulty-selector"; type DescriptionProps = { question: Question; @@ -34,7 +35,7 @@ export default function Description({
{question.title} - + {question.difficulty} diff --git a/frontend/src/pages/api/questionHandler.ts b/frontend/src/pages/api/questionHandler.ts index 206ee7ef..26ae62ab 100644 --- a/frontend/src/pages/api/questionHandler.ts +++ b/frontend/src/pages/api/questionHandler.ts @@ -19,6 +19,7 @@ export const fetchRandomQuestion = async ( headers: { "Content-Type": "application/json", "User-Id-Token": idToken, + "user-id": (user as User).uid }, }); @@ -87,6 +88,7 @@ export const fetchQuestions = async (user: any, pageNumber: number = 1, pageSize headers: { "Content-Type": "application/json", "User-Id-Token": idToken, + "user-id": (user as User).uid }, }); @@ -123,6 +125,7 @@ export const fetchQuestion = async (currentUser: User, questionId: string) => { headers: { "Content-Type": "application/json", "User-Id-Token": idToken, + "user-id": currentUser.uid }, }); @@ -170,6 +173,7 @@ export const postQuestion = async (user: any, question: z.infer { headers: { "Content-Type": "application/json", "User-Id-Token": idToken, + "user-id": (user as User).uid }, }); diff --git a/frontend/src/pages/questions/[id]/index.tsx b/frontend/src/pages/questions/[id]/index.tsx index 15f25870..71203a4c 100644 --- a/frontend/src/pages/questions/[id]/index.tsx +++ b/frontend/src/pages/questions/[id]/index.tsx @@ -19,6 +19,10 @@ export default function Questions() { const { user: currentUser, authIsReady } = useContext(AuthContext); useEffect(() => { + if (!authIsReady || !questionId) { + console.log("auth not ready or questionId not found"); + return; + }; if (currentUser) { fetchQuestion(currentUser, questionId) .then((question) => { diff --git a/frontend/src/providers/MatchmakingProvider.tsx b/frontend/src/providers/MatchmakingProvider.tsx index c5b3aeea..76195eaa 100644 --- a/frontend/src/providers/MatchmakingProvider.tsx +++ b/frontend/src/providers/MatchmakingProvider.tsx @@ -84,6 +84,7 @@ export const MatchmakingProvider: React.FC = ({ router.route !== "/interviews/find-match" ) { router.push(`/`); + leaveMatch(); // So that we won't always be redirected. } }, [match]); diff --git a/services/admin-service/README.md b/services/admin-service/README.md index 83c34cd6..44659691 100644 --- a/services/admin-service/README.md +++ b/services/admin-service/README.md @@ -69,19 +69,3 @@ yarn workspace admin-service dev:local ``` This uses the default `.env` file which has a `FIREBASE_SERVICE_ACCOUNT` corresponding to the development app. - -## How to do automated testing -Automated testing is done using a [Firebase Local Emulator Suite](https://firebase.google.com/docs/emulator-suite). -The README file at the project root has more details on this. Be sure to read that before trying to run any tests here. - -To run the unit tests locally, run this command at the project root: -`yarn workspace admin-service test` - -To run the system tests locally, run this command at the project root: -`yarn workspace admin-service systemtest` - -You may also run them in CI. In such a case, you need to provide the environment variables manually: -``` -yarn workspace admin-service test:ci -yarn workspace admin-service systemtest:ci -``` \ No newline at end of file diff --git a/services/admin-service/systemtest/app.test.ts b/services/admin-service/systemtest/app.test.ts deleted file mode 100644 index 57298ec5..00000000 --- a/services/admin-service/systemtest/app.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import {expect, describe, it, vi, beforeAll, afterAll} from 'vitest' -import firebaseWrappers from "../src/firebase-server/firebaseWrappers"; -import {firebaseAuth} from "../src/firebase-server/firebaseAuth"; -import { UserRecord } from "firebase-admin/auth"; -import app from "../src/app"; -import request from "supertest"; - -const testAdminUid = 'TestAdminUid'; - -// Set this to be between 10 and 19 inclusive -const numberOfListedUsers = 19; - -describe('Admin service /api/admin-service/index', () => { - - describe('Sample workflow for adding admin user', () => { - afterAll(async () => { - // Delete the admin - firebaseAuth.deleteUser(testAdminUid).then().catch((error) => { - if (error.code === "auth/user-not-found") { - // Ignore because some tests do not create the admin user - return; - } - // Otherwise, just throw the error - throw error; - }); - }); - - beforeAll(async () => { - // Create the admin - await firebaseAuth.createUser({ - uid: testAdminUid, - email: 'testuser@example.com', - emailVerified: false, - displayName: 'Test Admin' - }); - }); - - it('Step 1: Add admin rights to admin user', async () => { - const response = await request(app).put(`/api/admin-service/setAdmin/${testAdminUid}`).send(); - - const userClaims = await firebaseAuth.getUser(testAdminUid).then((userRecord) => { - return userRecord.customClaims; - }); - - const isAdmin = userClaims.admin && userClaims.admin === true; - - expect(response.status).toStrictEqual(200); - expect(response.body).toStrictEqual({ - "providerId": testAdminUid - }); - expect(isAdmin).toStrictEqual(true); - }); - - it('Step 2: Remove admin rights from admin user', async () => { - const response = await request(app).put(`/api/admin-service/removeAdmin/${testAdminUid}`).send(); - - const userClaims = await firebaseAuth.getUser(testAdminUid).then((userRecord) => { - return userRecord.customClaims; - }); - - expect(response.status).toStrictEqual(200); - expect(response.body).toStrictEqual({ - "providerId": testAdminUid - }); - expect(userClaims).toStrictEqual({}); - }); - }) - - describe('Adding and removing admin to/from non-existent user', () => { - it('Add admin rights to non-existent user', async () => { - const response = await request(app).put(`/api/admin-service/setAdmin/${testAdminUid}`).send(); - - expect(response.status).toStrictEqual(404); - }); - - it('Step 2: Remove admin rights from non-existent user', async () => { - const response = await request(app).put(`/api/admin-service/removeAdmin/${testAdminUid}`).send(); - - expect(response.status).toStrictEqual(404); - }); - }) - - describe('Sample workflow for listing users in database', () => { - // Delete the listed users - afterAll(async () => { - for (let i = 0; i < numberOfListedUsers; i++) { - firebaseAuth.deleteUser(`testUser${i}`).then().catch((error) => { - if (error.code === "auth/user-not-found") { - // Ignore because some tests do not create the user - return; - } - // Otherwise, just throw the error - throw error; - }); - } - }); - - beforeAll(async () => { - // Create the other users - for (let i = 0; i < numberOfListedUsers; i++) { - await firebaseAuth.createUser({ - uid: `testUser${i}`, - email: `testGet${i}@example.com`, - emailVerified: false, - displayName: `Test User ${i}` - }); - } - }); - - it(`List ${numberOfListedUsers} users in the database`, async () => { - const firstResponse = await request(app).get(`/api/admin-service/listUsers`).send() - expect(firstResponse.status).toStrictEqual(200); - - const firstResponseBody = firstResponse.body; - const firstUserList = firstResponseBody.users; - const nextPageToken = firstResponseBody.pageToken; - - expect(firstUserList.length).toStrictEqual(10); - expect(nextPageToken).toBeTruthy(); // next page token is a string - - const secondResponse = await request(app).get(`/api/admin-service/listUsers`).set('Next-Page-Token', nextPageToken).send(); - expect(secondResponse.status).toStrictEqual(200); - - const secondResponseBody = secondResponse.body; - const secondUserList = secondResponseBody.users; - const secondNextPageToken = secondResponseBody.pageToken; - - // The last user in one page is carried over to be the first user in the next page - - expect(secondUserList.length).toStrictEqual(numberOfListedUsers - 10); - expect(secondNextPageToken).toBeFalsy(); // Should not have next page token - - }); - }) -}) diff --git a/services/admin-service/systemtest/vitest.config.system.ts b/services/admin-service/systemtest/vitest.config.system.ts deleted file mode 100644 index 09e28285..00000000 --- a/services/admin-service/systemtest/vitest.config.system.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - include: ['./systemtest/**/*.test.ts'] - } -}) diff --git a/services/admin-service/test/firebase-server/firebaseWrappers.test.ts b/services/admin-service/test/firebase-server/firebaseWrappers.test.ts deleted file mode 100644 index 4e4cd7fc..00000000 --- a/services/admin-service/test/firebase-server/firebaseWrappers.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {expect, describe, it, vi, afterEach} from 'vitest' -import firebaseWrappers from "../../src/firebase-server/firebaseWrappers"; -import {firebaseAuth} from "../../src/firebase-server/firebaseAuth"; -import { UserRecord } from "firebase-admin/auth"; - -const testAdminUid = 'TestAdminUid'; - -describe('Firebase Wrappers', () => { - - describe('Set and remove admin', () => { - afterEach(async () => { - firebaseAuth.deleteUser(testAdminUid).then().catch((error) => { - if (error.code === "auth/user-not-found") { - // Ignore because some tests do not create the admin user - return; - } - // Otherwise, just throw the error - throw error; - }); - }) - - it('Set user that exists as admin', async () => { - const newUser : UserRecord = await firebaseAuth.createUser({ - uid: testAdminUid, - email: 'testuser@example.com', - emailVerified: false, - displayName: 'Test Admin' - }); - - const didOperationSucceed : boolean = await firebaseWrappers.setFirebaseUidAsAdmin(testAdminUid); - const userClaims = await firebaseAuth.getUser(newUser.uid).then((userRecord) => { - return userRecord.customClaims; - }); - - const isAdmin = userClaims.admin && userClaims.admin === true; - - expect(didOperationSucceed).toStrictEqual(true); - expect(isAdmin).toStrictEqual(true); - }) - - it('Set and remove admin from user that exists', async () => { - const newUser : UserRecord = await firebaseAuth.createUser({ - uid: testAdminUid, - email: 'testuser@example.com', - emailVerified: false, - displayName: 'Test Admin' - }); - - await firebaseAuth.setCustomUserClaims(newUser.uid, { admin: true }) - - const didOperationSucceed : boolean = await firebaseWrappers.removeAdminFromFirebaseUid(testAdminUid); - const userClaims = await firebaseAuth.getUser(newUser.uid).then((userRecord) => { - return userRecord.customClaims; - }); - - expect(didOperationSucceed).toStrictEqual(true); - expect(userClaims).toStrictEqual({}); - }) - - it('Set admin on non-existent user', async () => { - const didOperationSucceed : boolean = await firebaseWrappers.setFirebaseUidAsAdmin(testAdminUid); - expect(didOperationSucceed).toStrictEqual(false); - }) - - it('Remove admin on non-existent user', async () => { - const didOperationSucceed : boolean = await firebaseWrappers.removeAdminFromFirebaseUid(testAdminUid); - expect(didOperationSucceed).toStrictEqual(false); - }) - }) -}) diff --git a/services/admin-service/test/vitest.config.unit.ts b/services/admin-service/test/vitest.config.unit.ts deleted file mode 100644 index 340e63f1..00000000 --- a/services/admin-service/test/vitest.config.unit.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - include: ['./test/**/*.test.ts'] - } -}) diff --git a/services/matching-service/src/controllers/matchingController.ts b/services/matching-service/src/controllers/matchingController.ts index 6f1b1699..f0d0a345 100644 --- a/services/matching-service/src/controllers/matchingController.ts +++ b/services/matching-service/src/controllers/matchingController.ts @@ -123,6 +123,8 @@ export function handleLooking( return; } + console.log(`User ${userId} is looking for a match with difficulties ${difficulties} and language ${programmingLang}. Initial state of queue: `, await prisma.waitingUser.findMany()); + let { newMatch: foundMatch, matchingUser } = await prisma.$transaction( async (tx) => { const matchingUser = await tx.waitingUser.findFirst({ @@ -181,8 +183,7 @@ export function handleLooking( ); if (!foundMatch) { - console.log(`Queued user ${userId}.`); - console.log("In queue:", await prisma.waitingUser.findMany()); + console.log(`No match found yet. Queued user ${userId}. Current queue: `, await prisma.waitingUser.findMany()); return; } @@ -201,11 +202,11 @@ export function handleLooking( console.log( `Match found for user ${userId} with user ${ foundMatch.userId1 === userId ? foundMatch.userId2 : foundMatch.userId1 - } and difficulty ${foundMatch.chosenDifficulty}` + }, difficulty ${foundMatch.chosenDifficulty} and language ${foundMatch.chosenProgrammingLanguage}. + Current queue: `, + await prisma.waitingUser.findMany() ); - console.log("In queue:", await prisma.waitingUser.findMany()); - // Inform both users of the match socket.emit("matchFound", foundMatch); io.to(matchingUser?.socketId || "").emit("matchFound", foundMatch); @@ -214,13 +215,12 @@ export function handleLooking( export function handleCancelLooking(userId: string): () => Promise { return async () => { - console.log(`User ${userId} is no longer looking for a match`); - console.log("In queue:", await prisma.waitingUser.findMany()); await prisma.waitingUser.deleteMany({ where: { userId: userId, }, }); + console.log(`User ${userId} is no longer looking for a match. In queue now: `, await prisma.waitingUser.findMany()); }; } diff --git a/services/user-service/README.md b/services/user-service/README.md index 55e28098..ff0d7560 100644 --- a/services/user-service/README.md +++ b/services/user-service/README.md @@ -41,63 +41,3 @@ yarn workspace user-service dev:local ``` 4) The user-service will run on port 5001. You can test the API using Postman - -## How to run automated tests: - -### Unit Testing -In unit testing, each file is tested in isolation. -For example, while API routes are normally connected to the Prisma client functions, during unit testing, the API routes will be connected to a mock Prisma client. - -From the root of the project directory, run: -``` -yarn workspace user-service test -``` - -There is no need to set up a database for unit testing as mocking the database is done using [Vitest](https://vitest.dev/) and [vitest-mock-extended](https://www.npmjs.com/package/vitest-mock-extended). - -The above command can also be run in a CI workflow. - -### System Testing -In system testing, the entire microservice (including a real but temporary database) is run with all the components working together. - -From the root of the project directory, run: -``` -yarn workspace user-service systemtest -``` - -What this command does: -1) Read in a secret file stored in `user-service/systemtest/secrets/.env.user-service-system-test` to use as environment variables -2) Setup a Docker container for the temporary database -3) Apply Prisma migrations to that container using `yarn prisma migrate deploy` -4) Run the system test files -5) Teardown the Docker container - -You need to pass in the following environment variables through the above-mentioned `.env`-type file: -``` -PRISMA_DATABASE_URL="postgresql://postgres:${password}@localhost:5430/peerprepdb-user-service-systemtest?schema=public" -PRISMA_DATABASE_PASSWORD="${The password you want to pass in. This must match the password in the above variable}" -``` - -If you want to run this in a CI workflow, run: -``` -yarn workspace user-service systemtest:ci -``` - -This would do everything above except reading the environment variables from the `.env`-type file. -This also means that you need to pass in the environment variables to the CI workflow separately. - -#### Warning about system tests -During system testing, a live database is used (although it only exists for the duration of the test). - -In the current implementation of system test, the database is never cleared during the entire testing process, meaning that each test depends on the state of the previous test. - -This also means that if you abort the system test (or it fails), re-running the system test is not guaranteed to succeed again after fixing the failure cause. - -To be safe, any time the system test fails or is otherwise aborted, run: -``` -yarn workspace user-service systemtest:docker:down -``` -and then re-run: -``` -yarn workspace user-service systemtest -``` diff --git a/services/user-service/systemtest/app.test.ts b/services/user-service/systemtest/app.test.ts deleted file mode 100644 index cb249dd7..00000000 --- a/services/user-service/systemtest/app.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import {expect, describe, it} from 'vitest' - -import app from "../src/app" - -import request from 'supertest'; - -const fullNewUser = { uid: '1', displayName: 'Test User', photoUrl: "fakeUrl", matchDifficulty: "easy", - matchProgrammingLanguage: "python" }; - -const updatedNewUser = { uid: '1', displayName: 'Test User', photoUrl: "fakeUrl", matchDifficulty: "medium", - matchProgrammingLanguage: "python"}; - -const updatePayload = { matchDifficulty: "medium" }; - -const userIdHeader = "User-Id"; - -describe('/index', () => { - describe('Sample App Workflow', () => { - it('Step 1: Add user 1 to database should pass with status 201', async () => { - // The function being tested - const response = await request(app).post('/api/user-service').send(fullNewUser); - expect(response.status).toStrictEqual(201); - expect(response.body).toStrictEqual(fullNewUser); - }) - - it('Step 2: Retrieve details of user 1 from database should pass', async () => { - // The function being tested - const response = await request(app).get('/api/user-service/1').send(); - expect(response.status).toStrictEqual(200); - expect(response.body).toStrictEqual(fullNewUser); - }) - - it('Step 3a: Update details of user 1 from database by user 2 should fail with error 400', async () => { - // The function being tested - const response = await request(app) - .put('/api/user-service/1') - .set(userIdHeader, "2") - .send(updatePayload); - expect(response.status).toStrictEqual(400); - }) - - it('Step 3: Update details of user 1 from database should pass', async () => { - // The function being tested - const response = await request(app) - .put('/api/user-service/1') - .set(userIdHeader, "1") - .send(updatePayload); - expect(response.status).toStrictEqual(200); - expect(response.body).toStrictEqual(updatedNewUser); - }) - - it('Step 4: Retrieve details of updated user 1 from database should pass', async () => { - // The function being tested - const response = await request(app).get('/api/user-service/1').send(); - expect(response.status).toStrictEqual(200); - expect(response.body).toStrictEqual(updatedNewUser); - }) - - it('Step 5: Attempt to add duplicate user 1 to database should give status 200', async () => { - const response = await request(app).post('/api/user-service').send(fullNewUser); - expect(response.status).toStrictEqual(200); - }) - - it('Step 6a: Delete user 1 from database by user 2 should fail with status 400', async () => { - const response = await request(app) - .delete('/api/user-service/1') - .set(userIdHeader, "2") - .send(); - expect(response.status).toStrictEqual(400); - }) - - it('Step 6: Delete user 1 from database', async () => { - const response = await request(app) - .delete('/api/user-service/1') - .set(userIdHeader, "1") - .send(); - expect(response.status).toStrictEqual(204); - }) - - it('Step 7: Retrieve details of now deleted user 1 should fail', async () => { - // The function being tested - const response = await request(app).get('/api/user-service/1').send(); - expect(response.status).toStrictEqual(404); - }) - - it('Step 8: Update details of now deleted user 1 should fail', async () => { - // The function being tested - const response = await request(app) - .put('/api/user-service/1') - .set(userIdHeader, "1") - .send(updatePayload); - expect(response.status).toStrictEqual(404); - }) - - it('Step 9: Deleting the now deleted user 1 should fail', async () => { - // The function being tested - const response = await request(app) - .delete('/api/user-service/1') - .set(userIdHeader, "1") - .send(); - expect(response.status).toStrictEqual(404); - }) - }) - -}) diff --git a/services/user-service/systemtest/user-service-postgre-Docker-compose.yml b/services/user-service/systemtest/user-service-postgre-Docker-compose.yml deleted file mode 100644 index 22a76f8e..00000000 --- a/services/user-service/systemtest/user-service-postgre-Docker-compose.yml +++ /dev/null @@ -1,11 +0,0 @@ -version: "3" - -services: - postgres-db: - image: postgres - ports: - - "5430:5432" - environment: - POSTGRES_USER: postgres - POSTGRES_DB: peerprepdb-user-service-systemtest - POSTGRES_PASSWORD: ${PRISMA_DATABASE_PASSWORD} diff --git a/services/user-service/systemtest/vitest.config.system.ts b/services/user-service/systemtest/vitest.config.system.ts deleted file mode 100644 index 95453fc7..00000000 --- a/services/user-service/systemtest/vitest.config.system.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - include: ['./systemtest/**/*.test.ts'], - exclude: ['./test/**/*'], - threads: false - } -}) \ No newline at end of file diff --git a/services/user-service/test/db/functions.test.ts b/services/user-service/test/db/functions.test.ts deleted file mode 100644 index 4e7d02c2..00000000 --- a/services/user-service/test/db/functions.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import {beforeEach, expect, describe, it, vi} from 'vitest' -import userDatabaseFunctions from '../../src/db/functions' -import prismaMock from '../../src/db/__mocks__/prismaClient' - -vi.mock('../../src/db/prismaClient') - -const fullNewUser = { uid: '1', displayName: 'Test User', photoUrl: "fakeUrl", matchDifficulty: "easy", - matchProgrammingLanguage: "python" }; - -const partialNewUser = { uid: '1'}; - -describe('functions', () => { - beforeEach(() => { - vi.restoreAllMocks(); - }) - describe('createUser', () => { - it('createUser should return the generated user if its uid does not exist in database yet', async () => { - - // Used to add the user - prismaMock.appUser.create.mockResolvedValueOnce(fullNewUser); - - // The function being tested - const user = await userDatabaseFunctions.createUser(fullNewUser); - expect(user).toStrictEqual(fullNewUser); - }) - - it('createUser should return null if user with uid already exists in database', async () => { - // Used to simulate finding that the user is in the database - prismaMock.appUser.findUnique.mockResolvedValueOnce(fullNewUser); - - const user = await userDatabaseFunctions.createUser(fullNewUser); - expect(user).toStrictEqual(null); - }) - - it('createUser should only need uid to work', async () => { - prismaMock.appUser.create.mockResolvedValueOnce(partialNewUser); - const user = await userDatabaseFunctions.createUser(partialNewUser); - expect(user).toStrictEqual(partialNewUser); - }) - }) -}) diff --git a/services/user-service/test/routes/index.test.ts b/services/user-service/test/routes/index.test.ts deleted file mode 100644 index 0720302d..00000000 --- a/services/user-service/test/routes/index.test.ts +++ /dev/null @@ -1,200 +0,0 @@ -import {beforeEach, expect, describe, it, vi} from 'vitest' -import indexRouter from '../../src/routes/index' -import userDatabaseFunctionsMock from '../../src/db/__mocks__/functions' -import express from 'express'; -import {PrismaClientKnownRequestError} from "@prisma/client/runtime/library"; - -import request from 'supertest'; - -vi.mock('../../src/db/functions') - -const app = express(); -const userIdHeader = "User-Id"; -app.use(indexRouter); - -const fullNewUser = { uid: '1', displayName: 'Test User', photoUrl: "fakeUrl", matchDifficulty: "easy", - matchProgrammingLanguage: "python" }; - -describe('/index', () => { - /** - * Note: Since this test is for testing the index.ts file, the /api/user-service is not prepended - * to the routes. - */ - beforeEach(() => { - vi.restoreAllMocks(); - }) - - describe('createUser', () => { - it('[POST] to / with no new user yet', async () => { - - // Used to add the user - userDatabaseFunctionsMock.createUser.mockResolvedValueOnce(fullNewUser); - - // The function being tested - const response = await request(app).post('/').send(fullNewUser); - expect(response.status).toStrictEqual(201); - expect(response.body).toStrictEqual(fullNewUser); - }) - - it('[POST] to / with uid already in database', async () => { - - // Simulate a return null for user already in database - userDatabaseFunctionsMock.createUser.mockResolvedValueOnce(null); - - // The function being tested - const response = await request(app).post('/').send(fullNewUser); - expect(response.status).toStrictEqual(200); - }) - - it('[POST] to / when database is unavailable', async () => { - // Simulate a database error - userDatabaseFunctionsMock.createUser.mockRejectedValueOnce(new Error()); - - // The function being tested - const response = await request(app).post('/').send(fullNewUser); - expect(response.status).toStrictEqual(500); - }) - }) - - describe('getUserByUid', () => { - it('[GET] /1', async () => { - - // Used to get back the user - userDatabaseFunctionsMock.getUserByUid.mockResolvedValueOnce(fullNewUser); - - // The function being tested - const response = await request(app).get('/1').send(); - expect(response.status).toStrictEqual(200); - expect(response.body).toStrictEqual(fullNewUser); - }) - - it('[GET] /1 but user does not exist', async () => { - // Used to get back no user - userDatabaseFunctionsMock.getUserByUid.mockResolvedValueOnce(null); - - // The function being tested - const response = await request(app).get('/1').send(); - expect(response.status).toStrictEqual(404); - }) - - it('[GET] /1 when database is unavailable', async () => { - // Simulate a database error - userDatabaseFunctionsMock.getUserByUid.mockRejectedValueOnce(new Error()); - - // The function being tested - const response = await request(app).get('/1').send(); - expect(response.status).toStrictEqual(500); - }) - }) - - describe('updateUserByUid', () => { - it('[PUT] /1', async () => { - - // Used to get back the user - userDatabaseFunctionsMock.updateUserByUid.mockResolvedValueOnce(fullNewUser); - - // The function being tested - const response = await request(app) - .put('/1') - .set(userIdHeader, "1") - .send(); - expect(response.status).toStrictEqual(200); - expect(response.body).toStrictEqual(fullNewUser); - }) - - it('[PUT] /1 but user does not exist', async () => { - // Used to get back no user - userDatabaseFunctionsMock.updateUserByUid.mockRejectedValueOnce(new PrismaClientKnownRequestError('',{ - code: "P2025", - clientVersion: "Not important" - })); - - // The function being tested - const response = await request(app) - .put('/1') - .set(userIdHeader, "1") - .send(); - expect(response.status).toStrictEqual(404); - }) - - it('[PUT] /1 when database is unavailable', async () => { - // Simulate a database error - userDatabaseFunctionsMock.updateUserByUid.mockRejectedValueOnce(new Error()); - - // The function being tested - const response = await request(app) - .put('/1') - .set(userIdHeader, "1") - .send(); - expect(response.status).toStrictEqual(500); - }) - - it('[PUT] /1 but the header UID does not match path param UID', async () => { - - // Used to get back the user - userDatabaseFunctionsMock.updateUserByUid.mockResolvedValueOnce(fullNewUser); - - // The function being tested - const response = await request(app) - .put('/1') - .set(userIdHeader, "2") - .send(); - expect(response.status).toStrictEqual(400); - }) - }) - - describe('deleteUserByUid', () => { - it('[DELETE] /1', async () => { - - // Used to get back the user - userDatabaseFunctionsMock.deleteUserByUid.mockResolvedValueOnce(fullNewUser); - - // The function being tested - const response = await request(app) - .delete('/1') - .set(userIdHeader, "1") - .send(); - expect(response.status).toStrictEqual(204); - }) - - it('[DELETE] /1 but user does not exist', async () => { - // Used to get back no user - userDatabaseFunctionsMock.deleteUserByUid.mockRejectedValueOnce(new PrismaClientKnownRequestError('',{ - code: "P2025", - clientVersion: "Not important" - })); - - // The function being tested - const response = await request(app) - .delete('/1') - .set(userIdHeader, "1") - .send(); - expect(response.status).toStrictEqual(404); - }) - - it('[DELETE] /1 when database is unavailable', async () => { - // Simulate a database error - userDatabaseFunctionsMock.deleteUserByUid.mockRejectedValueOnce(new Error()); - - // The function being tested - const response = await request(app) - .delete('/1') - .set(userIdHeader, "1") - .send(); - expect(response.status).toStrictEqual(500); - }) - - it('[DELETE] /1 but the header UID does not match path param UID', async () => { - - // Used to get back the user - userDatabaseFunctionsMock.deleteUserByUid.mockResolvedValueOnce(fullNewUser); - - // The function being tested - const response = await request(app) - .delete('/1') - .set(userIdHeader, "2") - .send(); - expect(response.status).toStrictEqual(400); - }) - }) -}) diff --git a/services/user-service/test/vitest.config.unit.ts b/services/user-service/test/vitest.config.unit.ts deleted file mode 100644 index 340e63f1..00000000 --- a/services/user-service/test/vitest.config.unit.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - include: ['./test/**/*.test.ts'] - } -}) From d058691848dc33d04daf6cf82a55a8020b09e811 Mon Sep 17 00:00:00 2001 From: chunweii <47494777+chunweii@users.noreply.github.com> Date: Tue, 14 Nov 2023 12:13:43 +0800 Subject: [PATCH 07/19] Update readmes --- Assignment5-README.md | 11 +++++++++++ README.md | 7 ++----- 2 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 Assignment5-README.md diff --git a/Assignment5-README.md b/Assignment5-README.md new file mode 100644 index 00000000..734724eb --- /dev/null +++ b/Assignment5-README.md @@ -0,0 +1,11 @@ +Note: This assumes you are running Mac or Linux. If you are running Windows, please run all commands on a Git Bash terminal, or use Windows Subsystem for Linux. + +To run our app on Docker: + +1. Ensure depedencies are installed correctly (see [README.md](README.md)) + 1.1 Install the latest version of node.js v18 + 1.2 Install the latest version of yarn (`npm install -g yarn`) + 1.3 Install the latest version of docker +2. Create a `.env` file at the project root and copy the environment secrets from the uploaded file on CANVAS. +3. Start the app using `source start-app-with-docker.sh` +4. Open your browser and go to `localhost:3000` diff --git a/README.md b/README.md index 7ec3b76c..5ff790ea 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,6 @@ your services / frontend. MONGO_ATLAS_URL= FIREBASE_SERVICE_ACCOUNT= NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG={"apiKey": ,"authDomain": ,"projectId": ,"storageBucket": ,"messagingSenderId": ,"appId": } - TWILIO_ACCOUNT_SID= - TWILIO_API_KEY= - TWILIO_API_SECRET= ` Note: For `NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG`, the JSON should not have newlines since Next.js may not process it correctly. The difference between it and `FIREBASE_SERVICE_ACCOUNT` are shown below: @@ -122,7 +119,7 @@ it in case you forget later on when you have a lot more files to commit. 8. **Running everything at once:** To run everything at once and still maintain the ability to hot-reload your changes, use: ```bash - ./start-app-no-docker.sh # on mac /linus + ./start-app-no-docker.sh # on mac /linux # You can also use the above command on Windows with Git Bash @@ -163,7 +160,7 @@ This will stop and delete all the containers. **Run the start-app-with-docker.sh script:** From the root repo, run ```bash -./start-app-with-docker.sh # on mac / linus +./start-app-with-docker.sh # on mac / linux # You can also use the above command on Windows with Git Bash ``` From dfb9d508d191e8a1b51f20fb496175e6e9ac21e6 Mon Sep 17 00:00:00 2001 From: "YIHSUEN\\Yi Hsuen" Date: Wed, 15 Nov 2023 23:09:17 +0800 Subject: [PATCH 08/19] Remove non-essential code --- Dockerfile | 8 - .../prod-dockerfiles/Dockerfile.admin-service | 21 -- .../prod-dockerfiles/Dockerfile.frontend | 30 -- .../Dockerfile.matching-service | 21 -- .../Dockerfile.question-service | 21 -- .../prod-dockerfiles/Dockerfile.user-service | 21 -- docker-compose.yml | 57 --- .../src/backend-address/backend-address.ts | 6 + frontend/src/components/common/navbar.tsx | 13 +- .../firebase-client/useDeleteOwnAccount.ts | 2 +- frontend/src/firebase-client/useLogin.ts | 2 +- .../src/gateway-address/gateway-address.ts | 14 - frontend/src/hooks/useMatch.tsx | 29 -- frontend/src/hooks/useMatchmaking.tsx | 10 - frontend/src/pages/api/matchHandler.ts | 69 ---- frontend/src/pages/api/questionHandler.ts | 2 +- frontend/src/pages/api/userHandler.ts | 2 +- frontend/src/pages/interviews/find-match.tsx | 66 ---- frontend/src/pages/interviews/index.tsx | 170 --------- frontend/src/pages/interviews/match-found.tsx | 126 ------- .../src/pages/interviews/match-not-found.tsx | 21 -- .../src/providers/MatchmakingProvider.tsx | 180 --------- frontend/src/types/MatchTypes.ts | 9 - services/matching-service/.gitignore | 64 ---- services/matching-service/README.md | 57 --- services/matching-service/package.json | 24 -- services/matching-service/src/app.ts | 64 ---- .../src/controllers/matchingController.ts | 347 ------------------ .../matching-service/src/matchingQueue.ts | 15 - services/matching-service/src/prismaClient.ts | 5 - .../matching-service/src/questionAdapter.ts | 49 --- .../matching-service/src/routes/index.html | 107 ------ .../src/routes/matchingRoutes.ts | 9 - .../matching-service/src/swagger-output.json | 68 ---- services/matching-service/swagger-doc-gen.ts | 24 -- services/matching-service/tsconfig.json | 15 - 36 files changed, 11 insertions(+), 1737 deletions(-) delete mode 100644 Dockerfile delete mode 100644 deployment/prod-dockerfiles/Dockerfile.admin-service delete mode 100644 deployment/prod-dockerfiles/Dockerfile.frontend delete mode 100644 deployment/prod-dockerfiles/Dockerfile.matching-service delete mode 100644 deployment/prod-dockerfiles/Dockerfile.question-service delete mode 100644 deployment/prod-dockerfiles/Dockerfile.user-service delete mode 100644 docker-compose.yml create mode 100644 frontend/src/backend-address/backend-address.ts delete mode 100644 frontend/src/gateway-address/gateway-address.ts delete mode 100644 frontend/src/hooks/useMatch.tsx delete mode 100644 frontend/src/hooks/useMatchmaking.tsx delete mode 100644 frontend/src/pages/api/matchHandler.ts delete mode 100644 frontend/src/pages/interviews/find-match.tsx delete mode 100644 frontend/src/pages/interviews/index.tsx delete mode 100644 frontend/src/pages/interviews/match-found.tsx delete mode 100644 frontend/src/pages/interviews/match-not-found.tsx delete mode 100644 frontend/src/providers/MatchmakingProvider.tsx delete mode 100644 frontend/src/types/MatchTypes.ts delete mode 100644 services/matching-service/.gitignore delete mode 100644 services/matching-service/README.md delete mode 100644 services/matching-service/package.json delete mode 100644 services/matching-service/src/app.ts delete mode 100644 services/matching-service/src/controllers/matchingController.ts delete mode 100644 services/matching-service/src/matchingQueue.ts delete mode 100644 services/matching-service/src/prismaClient.ts delete mode 100644 services/matching-service/src/questionAdapter.ts delete mode 100644 services/matching-service/src/routes/index.html delete mode 100644 services/matching-service/src/routes/matchingRoutes.ts delete mode 100644 services/matching-service/src/swagger-output.json delete mode 100644 services/matching-service/swagger-doc-gen.ts delete mode 100644 services/matching-service/tsconfig.json diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index f282ccc8..00000000 --- a/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -# Base image -FROM node:18 - -# Set working directory -WORKDIR /app - -# Copy package.json and yarn.lock files -COPY package.json yarn.lock ./ diff --git a/deployment/prod-dockerfiles/Dockerfile.admin-service b/deployment/prod-dockerfiles/Dockerfile.admin-service deleted file mode 100644 index 7a67b32e..00000000 --- a/deployment/prod-dockerfiles/Dockerfile.admin-service +++ /dev/null @@ -1,21 +0,0 @@ -# Use the base image -FROM peerprep-base:latest - -# Set working directory -WORKDIR /app/services/admin-service - -# Copy the entire services directory and prisma -COPY services/admin-service /app/services/admin-service -COPY prisma ./prisma/ - -# Install all dependencies using Yarn Workspaces -RUN yarn install --frozen-lockfile --cwd /app - -# Generate the prisma client -RUN yarn prisma generate - -# Compile service from TypeScript to JavaScript -RUN yarn build - -# Run service -CMD [ "yarn", "workspace", "admin-service", "start" ] diff --git a/deployment/prod-dockerfiles/Dockerfile.frontend b/deployment/prod-dockerfiles/Dockerfile.frontend deleted file mode 100644 index c7d46d70..00000000 --- a/deployment/prod-dockerfiles/Dockerfile.frontend +++ /dev/null @@ -1,30 +0,0 @@ -# Use the base image you created above -FROM peerprep-base:latest - -# Copy utils -# COPY utils /app/utils/ - -# Set working directory for frontend -WORKDIR /app/frontend - -# Copy the entire frontend directory and prisma -COPY frontend /app/frontend -COPY prisma ./prisma/ - -# Install all dependencies using Yarn Workspaces -RUN yarn install --frozen-lockfile --cwd /app - -# Generate the prisma client -RUN yarn prisma generate - -# Compile service from TypeScript to JavaScript -# Note that NEXT_PUBLIC env variables are set at build time -ARG NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG_ARG -ENV NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG=$NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG_ARG - -RUN yarn build - -# Start command for the frontend -CMD [ "yarn", "workspace", "frontend", "start" ] - - diff --git a/deployment/prod-dockerfiles/Dockerfile.matching-service b/deployment/prod-dockerfiles/Dockerfile.matching-service deleted file mode 100644 index 07388d8a..00000000 --- a/deployment/prod-dockerfiles/Dockerfile.matching-service +++ /dev/null @@ -1,21 +0,0 @@ -# Use the base image -FROM peerprep-base:latest - -# Set working directory -WORKDIR /app/services/matching-service - -# Copy the entire services directory and prisma -COPY services/matching-service /app/services/matching-service -COPY prisma ./prisma/ - -# Install all dependencies using Yarn Workspaces -RUN yarn install --frozen-lockfile --cwd /app - -# Generate the prisma client -RUN yarn prisma generate - -# Compile service from TypeScript to JavaScript -RUN yarn build - -# Run service -CMD [ "yarn", "workspace", "matching-service", "start" ] diff --git a/deployment/prod-dockerfiles/Dockerfile.question-service b/deployment/prod-dockerfiles/Dockerfile.question-service deleted file mode 100644 index 049a7186..00000000 --- a/deployment/prod-dockerfiles/Dockerfile.question-service +++ /dev/null @@ -1,21 +0,0 @@ -# Use the base image -FROM peerprep-base:latest - -# Set working directory -WORKDIR /app/services/question-service - -# Copy the entire services directory and prisma -COPY services/question-service /app/services/question-service -COPY prisma ./prisma/ - -# Install all dependencies using Yarn Workspaces -RUN yarn install --frozen-lockfile --cwd /app - -# Generate the prisma client -RUN yarn prisma generate - -# Compile service from TypeScript to JavaScript -RUN yarn build - -# Run service -CMD [ "yarn", "workspace", "question-service", "start" ] diff --git a/deployment/prod-dockerfiles/Dockerfile.user-service b/deployment/prod-dockerfiles/Dockerfile.user-service deleted file mode 100644 index 544edc19..00000000 --- a/deployment/prod-dockerfiles/Dockerfile.user-service +++ /dev/null @@ -1,21 +0,0 @@ -# Use the base image -FROM peerprep-base:latest - -# Set working directory -WORKDIR /app/services/user-service - -# Copy the entire services directory and prisma -COPY services/user-service /app/services/user-service -COPY prisma ./prisma/ - -# Install all dependencies using Yarn Workspaces -RUN yarn install --frozen-lockfile --cwd /app - -# Generate the prisma client -RUN yarn prisma generate - -# Compile service from TypeScript to JavaScript -RUN yarn build - -# Run service -CMD [ "yarn", "workspace", "user-service", "start" ] diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index d95048a2..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,57 +0,0 @@ -version: "3" - -services: - user-service: - build: - context: . - dockerfile: deployment/prod-dockerfiles/Dockerfile.user-service - container_name: user-service - ports: - - "5001:5001" - environment: - PORT: 5001 - PRISMA_DATABASE_URL: ${PRISMA_DATABASE_URL} - - matching-service: - build: - context: . - dockerfile: deployment/prod-dockerfiles/Dockerfile.matching-service - container_name: matching-service - ports: - - "5002:5002" - environment: - PORT: 5002 - PRISMA_DATABASE_URL: ${PRISMA_DATABASE_URL} - QUESTION_SERVICE_HOSTNAME: "question-service" - - question-service: - build: - context: . - dockerfile: deployment/prod-dockerfiles/Dockerfile.question-service - container_name: question-service - ports: - - "5004:5004" - environment: - PORT: 5004 - MONGO_ATLAS_URL: ${MONGO_ATLAS_URL} - - admin-service: - build: - context: . - dockerfile: deployment/prod-dockerfiles/Dockerfile.admin-service - container_name: admin-service - ports: - - "5005:5005" - environment: - PORT: 5005 - FIREBASE_SERVICE_ACCOUNT: ${FIREBASE_SERVICE_ACCOUNT} - - frontend: - build: - context: . - dockerfile: deployment/prod-dockerfiles/Dockerfile.frontend - args: - NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG_ARG: ${NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG} - container_name: frontend - ports: - - "3000:3000" diff --git a/frontend/src/backend-address/backend-address.ts b/frontend/src/backend-address/backend-address.ts new file mode 100644 index 00000000..3adbfd03 --- /dev/null +++ b/frontend/src/backend-address/backend-address.ts @@ -0,0 +1,6 @@ +/** + * File for defining the address of the backend services. + */ +export const userApiPathAddress = "http://localhost:5001/api/user-service/"; +export const questionApiPathAddress = + "http://localhost:5004/api/question-service/"; diff --git a/frontend/src/components/common/navbar.tsx b/frontend/src/components/common/navbar.tsx index 5581216c..b76cb0e0 100644 --- a/frontend/src/components/common/navbar.tsx +++ b/frontend/src/components/common/navbar.tsx @@ -17,7 +17,6 @@ import { useLogout } from "@/firebase-client/useLogout"; import { useLogin } from "@/firebase-client/useLogin"; enum TabsOptions { - INTERVIEWS = "interviews", QUESTIONS = "questions", NULL = "", } @@ -33,9 +32,7 @@ export default function Navbar() { const currentPage = router.pathname; useEffect(() => { - if (currentPage === "/interviews") { - setActiveTab(TabsOptions.INTERVIEWS); - } else if (currentPage === "/questions") { + if (currentPage === "/questions") { setActiveTab(TabsOptions.QUESTIONS); } else { setActiveTab(TabsOptions.NULL); @@ -58,14 +55,6 @@ export default function Navbar() {
- - - Interviews - - { diff --git a/frontend/src/firebase-client/useLogin.ts b/frontend/src/firebase-client/useLogin.ts index 25cdd701..998872e6 100644 --- a/frontend/src/firebase-client/useLogin.ts +++ b/frontend/src/firebase-client/useLogin.ts @@ -2,7 +2,7 @@ import { GithubAuthProvider, signInWithPopup, User } from "firebase/auth"; import { auth } from "./firebase_config"; import { AuthContext } from "../contexts/AuthContext"; import { useContext, useState } from "react"; -import {userApiPathAddress} from "@/gateway-address/gateway-address"; +import {userApiPathAddress} from "@/backend-address/backend-address"; export const useLogin = () => { const [error, setError] = useState(null); diff --git a/frontend/src/gateway-address/gateway-address.ts b/frontend/src/gateway-address/gateway-address.ts deleted file mode 100644 index 9bff2917..00000000 --- a/frontend/src/gateway-address/gateway-address.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * File for defining the address of the gateway server. - * - * How to use: - * - Leave NEXT_PUBLIC_GATEWAY_ADDRESS empty for dev environments - * - For prod, pass in a separate address to NEXT_PUBLIC_GATEWAY_ADDRESS - */ -export const wsMatchProxyGatewayAddress = "http://localhost:5002"; - -export const userApiPathAddress = "http://localhost:5001/api/user-service/"; -export const questionApiPathAddress = - "http://localhost:5004/api/question-service/"; -export const matchApiPathAddress = - "http://localhost:5002/api/matching-service/"; diff --git a/frontend/src/hooks/useMatch.tsx b/frontend/src/hooks/useMatch.tsx deleted file mode 100644 index d62b88db..00000000 --- a/frontend/src/hooks/useMatch.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { AuthContext } from "@/contexts/AuthContext"; -import { - getMatchByRoomid as getMatchByRoomidApi, - patchMatchQuestionByRoomid as patchMatchQuestionByRoomidApi, -} from "@/pages/api/matchHandler"; -import { User } from "firebase/auth"; -import { useContext } from "react"; - -export const useMatch = () => { - const { user: currentUser, authIsReady } = useContext(AuthContext); - - const getMatch = async (roomId: string) => { - if (authIsReady && currentUser) { - const match = await getMatchByRoomidApi(currentUser, roomId); - return match; - } - }; - - const updateQuestionIdInMatch = async ( - roomId: string, - questionId: string - ) => { - if (authIsReady && currentUser) { - await patchMatchQuestionByRoomidApi(currentUser, roomId, questionId); - } - }; - - return { getMatch, updateQuestionIdInMatch }; -}; diff --git a/frontend/src/hooks/useMatchmaking.tsx b/frontend/src/hooks/useMatchmaking.tsx deleted file mode 100644 index e3f1582e..00000000 --- a/frontend/src/hooks/useMatchmaking.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { useContext } from "react"; -import { MatchmakingContext } from "../providers/MatchmakingProvider"; - -export function useMatchmaking() { - const context = useContext(MatchmakingContext); - if (!context) { - throw new Error("useMatchmaking must be used within a MatchmakingProvider"); - } - return context; -} diff --git a/frontend/src/pages/api/matchHandler.ts b/frontend/src/pages/api/matchHandler.ts deleted file mode 100644 index 5d124e9a..00000000 --- a/frontend/src/pages/api/matchHandler.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { matchApiPathAddress } from "@/gateway-address/gateway-address"; -import { Match } from "@/types/MatchTypes"; - -export const getMatchByRoomid = async (user: any, roomId: string) => { - try { - const url = `${matchApiPathAddress}match/${roomId}`; - const idToken = await user.getIdToken(true); - - const response = await fetch(url, { - method: "GET", - mode: "cors", - headers: { - "Content-Type": "application/json", - "User-Id-Token": idToken, - }, - }); - - if (!response.ok) { - throw new Error("Network response was not ok"); - } - - const data = await response.json(); - if (!data) { - throw new Error("There was an error fetching the match"); - } else if (data.error) { - throw new Error(data.error); - } else if (!data.info) { - throw new Error("There was an error fetching the match"); - } - return { - roomId: data.info.roomId, - userId1: data.info.userId1, - userId2: data.info.userId2, - chosenDifficulty: data.info.chosenDifficulty, - chosenProgrammingLanguage: data.info.chosenProgrammingLanguage, - questionId: data.info.questionId, - createdAt: data.info.createdAt, - }; - } catch (error) { - console.error("There was an error fetching the match", error); - } -}; - -export const patchMatchQuestionByRoomid = async ( - user: any, - roomId: string, - questionId: string -) => { - try { - const url = `${matchApiPathAddress}match/${roomId}`; - const idToken = await user.getIdToken(true); - - const response = await fetch(url, { - method: "PATCH", - mode: "cors", - headers: { - "Content-Type": "application/json", - "User-Id-Token": idToken, - }, - body: JSON.stringify({ questionId: questionId }), - }); - - if (!response.ok) { - throw new Error("Network response was not ok"); - } - } catch (error) { - console.error("There was an error fetching the match", error); - } -}; diff --git a/frontend/src/pages/api/questionHandler.ts b/frontend/src/pages/api/questionHandler.ts index 26ae62ab..f66e7054 100644 --- a/frontend/src/pages/api/questionHandler.ts +++ b/frontend/src/pages/api/questionHandler.ts @@ -1,4 +1,4 @@ -import { questionApiPathAddress } from "@/gateway-address/gateway-address"; +import { questionApiPathAddress } from "@/backend-address/backend-address"; import { Difficulty, Question } from "../../types/QuestionTypes"; import { formSchema } from "../questions/_form"; import { z } from "zod"; diff --git a/frontend/src/pages/api/userHandler.ts b/frontend/src/pages/api/userHandler.ts index c55765e5..adb33a82 100644 --- a/frontend/src/pages/api/userHandler.ts +++ b/frontend/src/pages/api/userHandler.ts @@ -1,4 +1,4 @@ -import { userApiPathAddress } from "@/gateway-address/gateway-address"; +import { userApiPathAddress } from "@/backend-address/backend-address"; import { EditableUser } from "@/types/UserTypes"; import { AppUser } from "@prisma/client"; diff --git a/frontend/src/pages/interviews/find-match.tsx b/frontend/src/pages/interviews/find-match.tsx deleted file mode 100644 index 7550cb6e..00000000 --- a/frontend/src/pages/interviews/find-match.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import Loader from "@/components/interviews/loader"; -import { Button } from "@/components/ui/button"; -import { TypographyBody, TypographyH2 } from "@/components/ui/typography"; -import { useRouter } from "next/router"; -import { use, useEffect, useState } from "react"; -import { useMatchmaking } from "@/hooks/useMatchmaking"; - -export default function FindMatch() { - const router = useRouter(); - const { match, cancelLooking } = useMatchmaking(); - const { query } = router; - const { retry } = query; - const [timeElapsed, setTimeElapsed] = useState(0); - - const onClickCancel = () => { - cancelLooking(); - router.push("/interviews"); - }; - - useEffect(() => { - const interval = setInterval(() => { - setTimeElapsed((prev) => prev + 1); - }, 1000); - return () => { - clearInterval(interval); - }; - }, []); - - useEffect(() => { - if (retry) { - router.push("/interviews"); - } - }, [retry, router]); - - useEffect(() => { - let timeout: ReturnType | null = null; - if (match) { - router.push("/interviews/match-found"); - } else { - timeout = setTimeout(() => { - cancelLooking(); - router.push("/interviews/match-not-found"); - }, 30000); - } - return () => { - if (timeout) clearTimeout(timeout); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [match, router]); - - return ( -
-
- Finding a match for your interview prep... - - Time elapsed: {timeElapsed} secs -
- - - - -
- ); -} diff --git a/frontend/src/pages/interviews/index.tsx b/frontend/src/pages/interviews/index.tsx deleted file mode 100644 index f294e038..00000000 --- a/frontend/src/pages/interviews/index.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import DifficultySelector from "@/components/common/difficulty-selector"; -import { languages } from "@/components/room/code-editor"; -import { Button } from "@/components/ui/button"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, -} from "@/components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { - TypographyBodyHeavy, - TypographyH1, - TypographyH2, - TypographySmall, -} from "@/components/ui/typography"; -import { AuthContext } from "@/contexts/AuthContext"; -import { useMatchmaking } from "@/hooks/useMatchmaking"; -import { useUser } from "@/hooks/useUser"; -import { cn } from "@/lib/utils"; -import { Check, ChevronsUpDown } from "lucide-react"; -import { useRouter } from "next/router"; -import { useContext, useEffect, useState } from "react"; - -type Difficulty = "easy" | "medium" | "hard" | "any"; - -export default function Interviews() { - const { user: currentUser } = useContext(AuthContext); - const [open, setOpen] = useState(false); - const [selectedLanguage, setSelectedLanguage] = useState( - languages.length > 0 ? languages[0].value : "c++" - ); - const [difficulty, setDifficulty] = useState("medium"); - - const router = useRouter(); - const { joinQueue } = useMatchmaking(); - const { getAppUser } = useUser(); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - if (currentUser) { - getAppUser().then((user) => { - if (user) { - setDifficulty((user.matchDifficulty as Difficulty) || difficulty); - setSelectedLanguage( - user.matchProgrammingLanguage || selectedLanguage - ); - } - setIsLoading(false); - }); - } else { - setTimeout(() => { - setIsLoading(false); - }, 2000); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentUser]); - - const onClickSearch = () => { - try { - joinQueue( - difficulty === "any" ? ["easy", "medium", "hard"] : [difficulty], - selectedLanguage - ); - console.log("Joined queue"); - router.push(`/interviews/find-match`); - } catch (error) { - console.error(error); - } - }; - - return ( -
- - Interviews - - - - Try out mock interviews with your peers! - - -
-
- Quick Match -
- Choose question difficulty - setDifficulty(value)} - showAny={true} - value={difficulty} - isLoading={isLoading} - /> -
- -
- Choose programming language -
- - - - - - - 0 - ? languages[0].label - : "Search Language..." - } - /> - No language found. - - {languages.map((language) => ( - { - setSelectedLanguage( - currentValue === selectedLanguage - ? "" - : currentValue - ); - setOpen(false); - }} - > - - {language.label} - - ))} - - - - -
-
- -
-
-
- ); -} diff --git a/frontend/src/pages/interviews/match-found.tsx b/frontend/src/pages/interviews/match-found.tsx deleted file mode 100644 index a21559e3..00000000 --- a/frontend/src/pages/interviews/match-found.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { languages } from "@/components/room/code-editor"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Button } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; -import { - TypographyCode, - TypographyH2, - TypographyH3, -} from "@/components/ui/typography"; -import { AuthContext } from "@/contexts/AuthContext"; -import { useMatchmaking } from "@/hooks/useMatchmaking"; -import { useUser } from "@/hooks/useUser"; -import { Difficulty } from "@/types/QuestionTypes"; -import { query } from "express"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { useContext, useEffect, useState } from "react"; -import { toast } from "react-toastify"; - -type UserInfo = { - displayName: string; - photoUrl: string; -}; - -const defaultUser: UserInfo = { - displayName: "John Doe", - photoUrl: "https://github.com/shadcn.png", -}; - -export default function MatchFound() { - const router = useRouter(); - const { match, leaveMatch, joinQueue, cancelLooking } = useMatchmaking(); - const { user, authIsReady } = useContext(AuthContext); - const [otherUser, setOtherUser] = useState(defaultUser); - - const { getAppUser } = useUser(); - const [isLoading, setIsLoading] = useState(true); - - const [difficulty, setDifficulty] = useState(["medium"]); - const [selectedLanguage, setSelectedLanguage] = useState( - languages.length > 0 ? languages[0].value : "c++" - ); - - useEffect(() => { - if (user) { - getAppUser().then((user) => { - if (user) { - setDifficulty([user.matchDifficulty as Difficulty] || difficulty); - setSelectedLanguage( - user.matchProgrammingLanguage || selectedLanguage - ); - } - setIsLoading(false); - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [user]); - - useEffect(() => { - if (!match) { - toast("Other user has left"); - router.push("/interviews"); - } else { - const fetchOtherUser = async () => { - const otherUserId = - match?.userId1 === user?.uid ? match?.userId2 : match?.userId1; - - const other = await getAppUser(otherUserId, false); - if (other) { - setOtherUser({ - displayName: other.displayName || "Anonymous", - photoUrl: other.photoUrl || defaultUser.photoUrl, - }); - } - - console.log(other); - }; - - if (user && authIsReady) { - fetchOtherUser(); - } - } - }, [user, authIsReady, match]); - - const onClickCancel = () => { - leaveMatch(); - router.push("/interviews"); - }; - - const onClickAccept = () => { - router.push(`/`); - }; - - return ( -
- Match Found! - - -
- - - - {defaultUser.displayName.charAt(0).toUpperCase()} - - -
- {otherUser?.displayName ?? "Annoymous"} - {/* @{otherUser?.displayName} */} -
-
-
- -
- - {/* */} - -
-
- ); -} diff --git a/frontend/src/pages/interviews/match-not-found.tsx b/frontend/src/pages/interviews/match-not-found.tsx deleted file mode 100644 index 33eb9a67..00000000 --- a/frontend/src/pages/interviews/match-not-found.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { TypographyBody, TypographyH2 } from "@/components/ui/typography"; -import Link from "next/link"; - -export default function MatchFound() { - return ( -
- - Sorry, we couldn’t find anyone :( - - - - Please come back later to find a peer to practice interviewing with! - - - - - -
- ) -} diff --git a/frontend/src/providers/MatchmakingProvider.tsx b/frontend/src/providers/MatchmakingProvider.tsx deleted file mode 100644 index 76195eaa..00000000 --- a/frontend/src/providers/MatchmakingProvider.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import React, { - createContext, - useContext, - useState, - useEffect, - useCallback, -} from "react"; -import { io, Socket } from "socket.io-client"; -import { Match } from "@prisma/client"; -import { AuthContext } from "@/contexts/AuthContext"; -import { wsMatchProxyGatewayAddress } from "@/gateway-address/gateway-address"; -import { useRouter } from "next/router"; -import { toast } from "react-toastify"; - -const SERVER_URL = wsMatchProxyGatewayAddress; - -interface MatchmakingContextValue { - socket: Socket | null; - match: Match | null; - message: string; - error: string; - joinQueue: (difficulties: string[], programmingLang: string) => void; - sendMessage: (message: string) => void; - leaveMatch: () => void; - cancelLooking: () => void; -} - -export const MatchmakingContext = createContext< - MatchmakingContextValue | undefined ->(undefined); - -interface MatchmakingProviderProps { - children: React.ReactNode; -} - -export const MatchmakingProvider: React.FC = ({ - children, -}) => { - const [socket, setSocket] = useState(null); - const [match, setMatch] = useState(null); - const [message, setMessage] = useState(""); - const [error, setError] = useState(""); - - const { user: currentUser, authIsReady } = useContext(AuthContext); - const router = useRouter(); - - const generateRandomNumber = () => { - // Return a random number either 0 or 1 as a string - return Math.floor(Math.random() * 2).toString(); - }; - - // Initialize socket connection - useEffect(() => { - if (currentUser) { - currentUser.getIdToken(true).then((token) => { - const newSocket = io(SERVER_URL, { - autoConnect: false, - query: { username: currentUser?.uid }, - //query: { username: generateRandomNumber() }, - extraHeaders: { - "User-Id-Token": token, - }, - }); - setSocket(newSocket); - newSocket.connect(); - - console.log("Socket connected"); - - return () => { - newSocket.close(); - }; - }); - } - }, [currentUser]); - - useEffect(() => { - if (!socket) return; - - // else we should join the room if they are in an exsiting match - // (i.e. they refreshed the page) - if ( - match && - router.route !== "/interviews/match-found" && - router.route !== "/interviews/find-match" - ) { - router.push(`/`); - leaveMatch(); // So that we won't always be redirected. - } - }, [match]); - - useEffect(() => { - if (!socket) return; - - socket.on("connect", () => { - console.log("Connected to server"); - }); - - socket.on("matchFound", (match: Match) => { - console.log("Match found:", match); - console.log("QuestionId:", match.questionId); - socket.emit("joinRoom", match.roomId); - setMatch(match); - }); - - socket.on("matchLeft", (match: Match) => { - console.log("Match left:", match); - setMatch(null); - }); - - socket.on("receiveMessage", (message: string) => { - console.log("Message received:", message); - setMessage(message); - }); - - socket.on("error", (error: string) => { - console.error("An error occurred:", error); - setError(error); - }); - - socket.on("disconnect", () => { - console.log("Disconnected from server"); - }); - - return () => { - socket.off("connect"); - socket.off("matchFound"); - socket.off("matchLeft"); - socket.off("receiveMessage"); - socket.off("error"); - socket.off("disconnect"); - }; - }, [socket]); - - const joinQueue = useCallback( - (difficulties: string[], programmingLang: string) => { - if (!socket) return; - - socket.emit("lookingForMatch", difficulties, programmingLang); - }, - [socket] - ); - - const sendMessage = useCallback( - (message: string) => { - if (!socket) return; - - socket.emit("sendMessage", message); - }, - [socket] - ); - - const leaveMatch = useCallback(() => { - if (!socket) return; - - socket.emit("leaveMatch"); - }, [socket]); - - const cancelLooking = useCallback(() => { - if (!socket) return; - - socket.emit("cancelLooking"); - }, [socket]); - - const value = { - socket, - match, - message, - error, - joinQueue, - sendMessage, - leaveMatch, - cancelLooking, - }; - - return ( - - {children} - - ); -}; diff --git a/frontend/src/types/MatchTypes.ts b/frontend/src/types/MatchTypes.ts deleted file mode 100644 index 7837a36d..00000000 --- a/frontend/src/types/MatchTypes.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type Match = { - roomId: string; - userId1: string; - userId2: string; - chosenDifficulty: string; - chosenProgrammingLanguage: string; - questionId?: string | null; - createdAt: Date; -}; diff --git a/services/matching-service/.gitignore b/services/matching-service/.gitignore deleted file mode 100644 index ce597ce5..00000000 --- a/services/matching-service/.gitignore +++ /dev/null @@ -1,64 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Ignore built ts files -dist/**/* - -# Dependency directories -node_modules/ -jspm_packages/ - -# Typescript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - -# next.js build output -.next diff --git a/services/matching-service/README.md b/services/matching-service/README.md deleted file mode 100644 index 3c3bd929..00000000 --- a/services/matching-service/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# User Service - -## Pre-requisites: -1. Install dependencies using `yarn install` -2. Run `yarn prisma generate` -3. `yarn workspace matching-service run dev:local` -4. Look at [index.html](./src/routes/index.html) to see how to use the API - -## API: -**Note:** All API endpoints are prefixed with `/api/matching-service` - -Matching service **DOES NOT** use REST API. It uses socket.io to communicate with the client. -The flow is: - -1. Client connects to /api/matching-service using socket.io and provides their username -2. Client can send messages to the server using socket.io as such: -```js -const username = "alice"; -const socket = io('http://localhost:5002/api/matching-service', {query: `username=${username}`}); -const difficulties = ["easy", "medium", "hard"]; -const programmingLanguage = "java"; -socket.emit('lookingForMatch', difficulties, programmingLanguage); // Looks for a new match -socket.emit('cancelLooking'); // Stop looking for a new match -socket.emit('leaveMatch'); // Leave the current match (only if you are in a match) -socket.emit('sendMatchMessage', "Hello World!"); // Send a message to the other user in the match -``` -3. Client should listen for messages from the server as such: -```js -socket.on('matchFound', (match) => { - console.log(match); // See Match type in prisma schema -}); -socket.on('matchNotFound', () => { - console.log("Probably timed out, can't find a match"); -}); -socket.on('matchLeft', () => { - console.log("Your match left the match"); // Other user emitted "leaveMatch" -}); -socket.on("receiveMessage", (message) => { - console.log(message); // Message from the other user or the server -}); -socket.on("error", (error) => { - console.log(error); // Error from the server -}); -``` -4. When a client wants to look for a match, the match request is placed in an in-memory queue. The server will try to match the user with another user in the queue. If a match is found, both users will be notified and will join a room. The server will then send a message to the client with the match information. If a match is not found after a specified time (60 seconds), the client will be notified and the request will be removed from the queue. - -Notes: - -We use socket.io instead of traditional HTTP REST API -Matched users will join a room -We use EventEmitters to notify the other waiting user of a new match - - -Extensions: - -Store the waiting users on a persistent queue in case the service dies (Use redis possibly) -Remove REST API code diff --git a/services/matching-service/package.json b/services/matching-service/package.json deleted file mode 100644 index aaa598d4..00000000 --- a/services/matching-service/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "matching-service", - "version": "0.0.0", - "private": true, - "scripts": { - "lint": "eslint src/**/*.{ts,js} swagger-doc-gen.ts", - "build": "yarnpkg run swagger-autogen && tsc", - "start": "node ./dist/src/app.js", - "dev:local": "dotenv -e ../../.env -c development -- yarnpkg dev", - "dev": "ts-node-dev src/app.ts", - "swagger-autogen": "ts-node swagger-doc-gen.ts" - }, - "dependencies": { - "cookie-parser": "~1.4.4", - "debug": "~2.6.9", - "morgan": "~1.9.1", - "socket.io": "^4.7.2" - }, - "devDependencies": { - "@types/cookie-parser": "^1.4.4", - "@types/cors": "^2.8.14", - "@types/morgan": "^1.9.5" - } -} diff --git a/services/matching-service/src/app.ts b/services/matching-service/src/app.ts deleted file mode 100644 index 8aac8869..00000000 --- a/services/matching-service/src/app.ts +++ /dev/null @@ -1,64 +0,0 @@ -import express from "express"; -import logger from "morgan"; -import { Server } from "socket.io"; -import matchingRoutes from "./routes/matchingRoutes"; -import { - handleConnection, - handleDisconnect, - handleJoinRoom, - handleLooking, -} from "./controllers/matchingController"; -import { handleCancelLooking } from "./controllers/matchingController"; -import { handleLeaveMatch } from "./controllers/matchingController"; -import { handleSendMessage } from "./controllers/matchingController"; -import cors from "cors"; -import swaggerUi from "swagger-ui-express"; -import swaggerFile from "./swagger-output.json"; - -const app = express(); -const port = process.env.PORT || 5002; - -app.use(express.json()); -app.use(cors()); -app.use(logger("dev")); -app.use("/api/matching-service", matchingRoutes); -app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerFile)); - -const socketIoOptions: any = { - cors: { - origin: process.env.FRONTEND_ADDRESS || "http://localhost:3000", - methods: ["GET", "POST", "PATCH"], - }, -}; - -const httpServer = require("http").createServer(app); -export const io = new Server(httpServer, socketIoOptions); - -app.set("io", io); - -io.on("connection", async (socket) => { - let userId = await handleConnection(socket); - - socket.on( - "disconnect", - handleDisconnect(socket, userId) - ); - - socket.on( - "lookingForMatch", - handleLooking(socket, userId) - ); - - socket.on("cancelLooking", handleCancelLooking(userId)); - - socket.on("leaveMatch", handleLeaveMatch(userId, socket)); - - socket.on("sendMessage", handleSendMessage(userId, socket)); - - socket.on("joinRoom", handleJoinRoom(userId, socket)); - -}); - -httpServer.listen(port, () => { - console.log(`matching-service is running on the port ${port}`); -}); diff --git a/services/matching-service/src/controllers/matchingController.ts b/services/matching-service/src/controllers/matchingController.ts deleted file mode 100644 index f0d0a345..00000000 --- a/services/matching-service/src/controllers/matchingController.ts +++ /dev/null @@ -1,347 +0,0 @@ -import { Request, Response } from "express"; -import { Socket } from "socket.io"; -import { io } from "../app"; -import prisma from "../prismaClient"; -import { getRandomQuestionOfDifficulty } from "../questionAdapter"; -import { EnumRoomStatus } from "@prisma/client"; - -export const MAX_WAITING_TIME = 60 * 1000; // 60 seconds - -export async function handleConnection(socket: Socket) { - let userId = (socket.handshake.query.username as string) || ""; - console.log(`User connected: ${socket.id} and username ${userId}`); - - const { count: earlierWaitingCount } = await prisma.waitingUser.deleteMany({ - where: { - userId: userId, - }, - }); - - if (earlierWaitingCount >= 1) { - console.log(`User ${userId} was waiting in the queue in another session`); - socket.emit( - "error", - "You are already waiting in the queue in another session. You will be removed from the queue." - ); - } - - // Join the room if the user is in a match - const existingMatch = await prisma.match.findFirst({ - where: { - OR: [{ userId1: userId }, { userId2: userId }], - }, - }); - - if (existingMatch) { - socket.join(existingMatch.roomId); - socket.emit("matchFound", existingMatch); - } - - return userId; -} - -export function handleDisconnect(socket: Socket, userId: string) { - return () => { - console.log(`User disconnected: ${socket.id}`); - // Remove user from queue if they disconnect - prisma.waitingUser - .deleteMany({ - where: { - userId: userId, - }, - }) - .catch((err) => { - console.log(err); - }); - - // Match should not be cancelled since the user might reconnect but we can notify the other user - prisma.match - .findFirst({ - where: { - OR: [{ userId1: userId }, { userId2: userId }], - }, - }) - .then((match) => { - if (match) { - const matchingUserId = - match?.userId1 === userId ? match?.userId2 : match?.userId1; - console.log( - `Notifying user ${matchingUserId} that user ${userId} has disconnected` - ); - io.to(match?.roomId || "").emit( - "receiveMessage", - "Server", - "Your partner has disconnected" - ); - } - }) - .catch((err) => { - console.log(err); - }); - }; -} - -export function handleLooking( - socket: Socket, - userId: string -): (difficulties: string[], programmingLang: string) => Promise { - return async (difficulties: string[], programmingLang: string) => { - if (!difficulties || !programmingLang) { - console.log(`Invalid request from user ${userId}`); - socket.emit("error", "Invalid request"); - return; - } - - let hasError = false; - const existingMatch = await prisma.match - .findFirst({ - where: { - OR: [{ userId1: userId }, { userId2: userId }], - }, - }) - .catch((err) => { - console.log(err); - socket.emit("error", "An error occurred in lookingForMatch."); - hasError = true; - }); - - if (hasError) { - return; - } - - if (existingMatch) { - console.log( - `User ${userId} is already matched with user ${ - existingMatch.userId1 === userId - ? existingMatch.userId2 - : existingMatch.userId1 - }` - ); - socket.emit("error", "You are already matched with someone."); - socket.join(existingMatch.roomId); - socket.emit("matchFound", existingMatch); - return; - } - - console.log(`User ${userId} is looking for a match with difficulties ${difficulties} and language ${programmingLang}. Initial state of queue: `, await prisma.waitingUser.findMany()); - - let { newMatch: foundMatch, matchingUser } = await prisma.$transaction( - async (tx) => { - const matchingUser = await tx.waitingUser.findFirst({ - where: { - progLang: programmingLang, - difficulty: { - hasSome: difficulties, - }, - createdAt: { - gte: new Date(Date.now() - MAX_WAITING_TIME), - }, - }, - }); - if (matchingUser) { - const commonDifficulty = matchingUser.difficulty.find((v) => - difficulties.includes(v) - ); - const newMatch = await tx.match.create({ - data: { - userId1: matchingUser.userId, - userId2: userId, - chosenDifficulty: commonDifficulty || "easy", - chosenProgrammingLanguage: programmingLang, - }, - }); - await tx.room.create({ - data: { - room_id: newMatch.roomId, - status: EnumRoomStatus.active, - text: "", - }, - }); - await tx.waitingUser.deleteMany({ - where: { - userId: { - in: [matchingUser.userId, userId], - }, - }, - }); - return { newMatch, matchingUser }; - } else { - await tx.waitingUser.create({ - data: { - userId: userId, - progLang: programmingLang, - difficulty: difficulties, - socketId: socket.id, - }, - }); - return { - newMatch: null, - matchingUser: null, - }; - } - } - ); - - if (!foundMatch) { - console.log(`No match found yet. Queued user ${userId}. Current queue: `, await prisma.waitingUser.findMany()); - return; - } - - const qnId = await getRandomQuestionOfDifficulty( - foundMatch.chosenDifficulty - ); - foundMatch = await prisma.match.update({ - where: { - roomId: foundMatch.roomId, - }, - data: { - questionId: qnId, - }, - }); - - console.log( - `Match found for user ${userId} with user ${ - foundMatch.userId1 === userId ? foundMatch.userId2 : foundMatch.userId1 - }, difficulty ${foundMatch.chosenDifficulty} and language ${foundMatch.chosenProgrammingLanguage}. - Current queue: `, - await prisma.waitingUser.findMany() - ); - - // Inform both users of the match - socket.emit("matchFound", foundMatch); - io.to(matchingUser?.socketId || "").emit("matchFound", foundMatch); - }; -} - -export function handleCancelLooking(userId: string): () => Promise { - return async () => { - await prisma.waitingUser.deleteMany({ - where: { - userId: userId, - }, - }); - console.log(`User ${userId} is no longer looking for a match. In queue now: `, await prisma.waitingUser.findMany()); - }; -} - -export function handleJoinRoom( - userId: string, - socket: Socket -): (roomId: string) => void { - return (roomId: string) => { - // TODO: Check if the user is in a match with relevant room id - console.log(`User ${socket.id} is joining room ${roomId}`); - socket.join(roomId); - }; -} - -export function handleLeaveMatch( - userId: string, - socket: Socket -): () => Promise { - return async () => { - console.log(`User ${userId} has left the match`); - // socket.emit("userLeft", userId); - - const deletedRoom = await prisma.$transaction(async (tx) => { - const match = await tx.match.findFirst({ - where: { - OR: [{ userId1: userId }, { userId2: userId }], - }, - }); - if (!match) { - console.log(`User ${userId} is not currently matched with anyone.`); - socket.emit("error", "You are not currently matched with anyone."); - return; - } - return await tx.match.delete({ - where: { - roomId: match?.roomId, - }, - }); - }); - - if (deletedRoom) { - console.log(`Room ${deletedRoom} has been deleted`); - io.to(deletedRoom.roomId).emit("matchLeft", deletedRoom); - } - }; -} - -export function handleSendMessage( - userId: string, - socket: Socket -): (message: string) => Promise { - return async (message: string) => { - if (!userId || !message) { - console.log(`Invalid request from user ${userId}`); - socket.emit("error", "Invalid request"); - return; - } - console.log(`User ${userId} sent a message: ${message}`); - - let hasError = false; - const match = await prisma.match - .findFirst({ - where: { - OR: [{ userId1: userId }, { userId2: userId }], - }, - }) - .catch((err) => { - console.log(err); - socket.emit("error", "An error occurred in sendMessage."); - hasError = true; - }); - - if (hasError) { - return; - } - - const matchedUser = - match?.userId1 === userId ? match?.userId2 : match?.userId1; - - if (matchedUser) { - // Forward the message to the matched user - socket.to(match?.roomId || "").emit("receiveMessage", userId, message); - } else { - // Error handling if the user tries to send a message without a match - console.log(`User ${userId} is not currently matched with anyone.`); - socket.emit("error", "You are not currently matched with anyone."); - } - }; -} - -export async function updateMatchQuestion(req: Request, res: Response) { - const room_id = req.params.room_id as string; - - const { questionId } = req.body; - - if (!questionId) { - return res - .status(400) - .json({ error: "Invalid or missing questionId in the request body" }); - } - - const match = await prisma.match.findUnique({ where: { roomId: room_id } }); - - if (!match) { - return res.status(404).json({ error: "Match not found" }); - } - - try { - const updatedMatch = await prisma.match.update({ - where: { roomId: room_id }, - data: { - questionId, - }, - }); - - return res.status(200).json({ - message: "Match updated successfully", - room_id: room_id, - info: updatedMatch, - }); - } catch (error) { - return res.status(500).json({ error: "Failed to update the match" }); - } -} diff --git a/services/matching-service/src/matchingQueue.ts b/services/matching-service/src/matchingQueue.ts deleted file mode 100644 index 60405200..00000000 --- a/services/matching-service/src/matchingQueue.ts +++ /dev/null @@ -1,15 +0,0 @@ -export class MatchingQueue { - private queue: number[] = []; - - enqueue(userId: number) { - this.queue.push(userId); - } - - dequeue(): number | undefined { - return this.queue.shift(); - } - - getQueue(): number[] { - return this.queue; - } -} diff --git a/services/matching-service/src/prismaClient.ts b/services/matching-service/src/prismaClient.ts deleted file mode 100644 index b5bf6ce8..00000000 --- a/services/matching-service/src/prismaClient.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); - -export default prisma; diff --git a/services/matching-service/src/questionAdapter.ts b/services/matching-service/src/questionAdapter.ts deleted file mode 100644 index 22a15746..00000000 --- a/services/matching-service/src/questionAdapter.ts +++ /dev/null @@ -1,49 +0,0 @@ -import http from "http"; - -export async function getRandomQuestionOfDifficulty( - difficulty: string -): Promise { - const requestBody = JSON.stringify({ difficulty }); - - const options = { - hostname: process.env.QUESTION_SERVICE_HOSTNAME || "localhost", - port: 5004, // Port of the question service - path: "/api/question-service/random-question", - method: "POST", - headers: { - "Content-Type": "application/json", - "Content-Length": Buffer.byteLength(requestBody), - }, - }; - - return new Promise((resolve, reject) => { - const req = http.request(options, (response) => { - let data = ""; - - response.on("data", (chunk) => { - data += chunk; - }); - - response.on("end", () => { - try { - const parsedData = JSON.parse(data); - const qnId = parsedData[0]._id; - if (qnId) { - resolve(qnId); - } else { - reject(new Error("Invalid response format")); - } - } catch (err) { - reject(err); - } - }); - }); - - req.on("error", (err) => { - reject(err); - }); - - req.write(requestBody); - req.end(); - }); -} diff --git a/services/matching-service/src/routes/index.html b/services/matching-service/src/routes/index.html deleted file mode 100644 index 5246debf..00000000 --- a/services/matching-service/src/routes/index.html +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - Matching Service - - -

- - - - - -
- - - - - - \ No newline at end of file diff --git a/services/matching-service/src/routes/matchingRoutes.ts b/services/matching-service/src/routes/matchingRoutes.ts deleted file mode 100644 index 9707c5f0..00000000 --- a/services/matching-service/src/routes/matchingRoutes.ts +++ /dev/null @@ -1,9 +0,0 @@ -import express from "express"; -import { updateMatchQuestion } from "../controllers/matchingController"; - -const router = express.Router(); - -router.patch("/match/:room_id", updateMatchQuestion); -router.get("/demo", (req, res) => res.sendFile(__dirname + "/index.html")); - -export default router; diff --git a/services/matching-service/src/swagger-output.json b/services/matching-service/src/swagger-output.json deleted file mode 100644 index f2a5e202..00000000 --- a/services/matching-service/src/swagger-output.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Matching Service", - "description": "", - "version": "1.0.0" - }, - "servers": [ - { - "url": "http://localhost:5002/" - } - ], - "paths": { - "/api/matching-service/match/{room_id}": { - "patch": { - "description": "", - "parameters": [ - { - "name": "room_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - }, - "404": { - "description": "Not Found" - }, - "500": { - "description": "Internal Server Error" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "questionId": { - "example": "any" - } - } - } - } - } - } - } - }, - "/api/matching-service/demo": { - "get": { - "description": "", - "responses": { - "200": { - "description": "OK" - } - } - } - } - } -} \ No newline at end of file diff --git a/services/matching-service/swagger-doc-gen.ts b/services/matching-service/swagger-doc-gen.ts deleted file mode 100644 index e9b0f20c..00000000 --- a/services/matching-service/swagger-doc-gen.ts +++ /dev/null @@ -1,24 +0,0 @@ -import swaggerAutogen from "swagger-autogen"; - -const doc = { - info: { - title: "Matching Service", - description: "", - }, - host: "localhost:5002", - schemes: ["http"], -}; - -const outputFile = "./src/swagger-output.json"; -const endpointsFiles = ["./src/app.ts"]; - -/* NOTE: if you use the express Router, you must pass in the - 'endpointsFiles' only the root file where the route starts, - such as index.js, app.js, routes.js, ... */ - -swaggerAutogen({ openapi: "3.0.0" })(outputFile, endpointsFiles, doc); -/*.then( - async () => { - await import("./src/app"); // Your project's root file - } - );*/ // to run it after swagger-autogen diff --git a/services/matching-service/tsconfig.json b/services/matching-service/tsconfig.json deleted file mode 100644 index 72438b4f..00000000 --- a/services/matching-service/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "es2016", - "module": "commonjs", - "resolveJsonModule": true, - "rootDir": ".", - "outDir": "./dist", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "src/**/__mocks__/*.ts"] -} From b293612b2b27633acc2d5945da8277ef2c981f66 Mon Sep 17 00:00:00 2001 From: "YIHSUEN\\Yi Hsuen" Date: Wed, 15 Nov 2023 23:36:35 +0800 Subject: [PATCH 09/19] Trim unnecessary code --- .../src/components/common/auth-checker.tsx | 33 ----- frontend/src/pages/_app.tsx | 36 ++--- frontend/src/pages/questions/[id]/edit.tsx | 4 +- frontend/src/pages/questions/index.tsx | 4 +- frontend/src/pages/questions/new.tsx | 2 +- frontend/src/pages/settings/_match.tsx | 137 ------------------ start-app-no-docker.sh | 2 - start-app-with-docker.sh | 9 -- 8 files changed, 20 insertions(+), 207 deletions(-) delete mode 100644 frontend/src/components/common/auth-checker.tsx delete mode 100644 frontend/src/pages/settings/_match.tsx delete mode 100644 start-app-with-docker.sh diff --git a/frontend/src/components/common/auth-checker.tsx b/frontend/src/components/common/auth-checker.tsx deleted file mode 100644 index 2c0cdf75..00000000 --- a/frontend/src/components/common/auth-checker.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React, { useEffect } from "react"; -import { useRouter } from "next/router"; -import { AuthContext } from "@/contexts/AuthContext"; -import { getAuth, onAuthStateChanged } from "firebase/auth"; -import { useContext } from "react"; - -interface AuthCheckerProps { - children: React.ReactNode; -} - -export default function AuthChecker({ children }: AuthCheckerProps) { - const auth = getAuth(); - const router = useRouter(); - const { user: currentUser, authIsReady } = useContext(AuthContext); - - const currentPage = router.pathname; - - useEffect(() => { - if (!currentUser && currentPage !== "/") { - onAuthStateChanged(auth, (user) => { - if (!user) { - router.push("/"); - } - }); - } - }); - - if (currentPage === "/") { - return children; - } - - return currentUser && children; -} diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index 46951e52..b2f93f63 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -3,8 +3,6 @@ import type { AppProps } from "next/app"; import Layout from "../components/common/layout"; import { Noto_Sans } from "next/font/google"; import AuthContextProvider from "@/contexts/AuthContext"; -import { MatchmakingProvider } from "../providers/MatchmakingProvider"; -import AuthChecker from "@/components/common/auth-checker"; import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; @@ -23,25 +21,21 @@ export default function App({ Component, pageProps }: AppProps) { `}
- - - - - - - - + + + +
diff --git a/frontend/src/pages/questions/[id]/edit.tsx b/frontend/src/pages/questions/[id]/edit.tsx index 26bdad2b..2be0721a 100644 --- a/frontend/src/pages/questions/[id]/edit.tsx +++ b/frontend/src/pages/questions/[id]/edit.tsx @@ -34,7 +34,7 @@ export default function EditQuestion() { if (!questionId || !authIsReady) { return; } - if (currentUser && isAdmin) { + if (currentUser) { fetchQuestion(currentUser, questionId as string).then(question => { if (question) { form.setValue("title", question.title); @@ -55,7 +55,7 @@ export default function EditQuestion() { setLoading(false); }); } else { - // if user is not logged in or is not admin, redirect to home + // if user is not logged in, redirect to home router.push("/"); } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/frontend/src/pages/questions/index.tsx b/frontend/src/pages/questions/index.tsx index 3be12805..5a529e78 100644 --- a/frontend/src/pages/questions/index.tsx +++ b/frontend/src/pages/questions/index.tsx @@ -56,7 +56,7 @@ export default function Questions() { Practice our questions to ace your coding interview!
- +
diff --git a/frontend/src/pages/questions/new.tsx b/frontend/src/pages/questions/new.tsx index af67c160..8a58781d 100644 --- a/frontend/src/pages/questions/new.tsx +++ b/frontend/src/pages/questions/new.tsx @@ -33,7 +33,7 @@ export default function NewQuestion() { if (!authIsReady) { return; } - if (!currentUser || !isAdmin) { + if (!currentUser) { router.push("/"); } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/frontend/src/pages/settings/_match.tsx b/frontend/src/pages/settings/_match.tsx deleted file mode 100644 index 3aae99bb..00000000 --- a/frontend/src/pages/settings/_match.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Label } from "@radix-ui/react-dropdown-menu"; -import { useUser } from "@/hooks/useUser"; -import { AuthContext } from "@/contexts/AuthContext"; -import { useContext, useEffect, useRef, useState } from "react"; -import { Button } from "@/components/ui/button"; -import { EditableUser } from "@/types/UserTypes"; -import DifficultySelector from "@/components/common/difficulty-selector"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Difficulty } from "@/types/QuestionTypes"; -import { DotWave } from "@uiball/loaders"; - -export default function MatchSettingsCard() { - const { user: currentUser } = useContext(AuthContext); - const { updateUser, getAppUser } = useUser(); - const [isLoading, setIsLoading] = useState(true); - const [showSuccess, setShowSuccess] = useState(false); - const submitButtonRef = useRef(null); - - const [updatedUser, setUpdatedUser] = useState({ - uid: currentUser?.uid ?? "", - } as EditableUser); - const [selectedDifficulty, setSelectedDifficulty] = - useState("medium"); - const [selectedLanguage, setSelectedLanguage] = useState("c++"); - - useEffect(() => { - if (currentUser) { - getAppUser().then((user) => { - if (user) { - setSelectedDifficulty(user.matchDifficulty as Difficulty || selectedDifficulty); - setSelectedLanguage(user.matchProgrammingLanguage || selectedLanguage); - } - setIsLoading(false); - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentUser]); - - useEffect(() => { - setUpdatedUser((prev) => ({ - ...prev, - matchDifficulty: selectedDifficulty, - })); - }, [selectedDifficulty]); - - useEffect(() => { - setUpdatedUser((prev) => ({ - ...prev, - matchProgrammingLanguage: selectedLanguage, - })); - }, [selectedLanguage]); - - return ( - - - Match Preferences - - - {isLoading ? ( -
- -
- ) : ( - -
- - -
-
- - { - setShowSuccess(false); - setSelectedDifficulty(value); - }} - /> -
-
- - {showSuccess && ( - - Successfully updated match preferences! - - )} -
-
- )} -
-
- ); -} diff --git a/start-app-no-docker.sh b/start-app-no-docker.sh index 8ca859d3..b8496ef3 100755 --- a/start-app-no-docker.sh +++ b/start-app-no-docker.sh @@ -10,7 +10,5 @@ prepend() { trap 'kill 0' INT TERM; \ (yarnpkg workspace frontend dev:local | prepend "frontend: ") & \ (yarnpkg workspace user-service dev:local | prepend "user-service: ") & \ - (yarnpkg workspace admin-service dev:local | prepend "admin-service: ") & \ - (yarnpkg workspace matching-service dev:local | prepend "matching-service: ") & \ (yarnpkg workspace question-service dev:local | prepend "question-service: ") & \ wait) diff --git a/start-app-with-docker.sh b/start-app-with-docker.sh deleted file mode 100644 index 4accbe24..00000000 --- a/start-app-with-docker.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -# These are the steps needed for docker to function - -# Step 1: Build the root-level Dockerfile and docker-compose services -yarnpkg docker:build - -# Step 2: Run the entire application -yarnpkg docker:devup From db2a6c33bc26b9431cc8054c3688270eb7efc1f8 Mon Sep 17 00:00:00 2001 From: "YIHSUEN\\Yi Hsuen" Date: Thu, 16 Nov 2023 01:08:13 +0800 Subject: [PATCH 10/19] Contextualise assignment 2 README --- Assignment5-README.md | 11 ---- README.md | 132 +++++++----------------------------------- 2 files changed, 20 insertions(+), 123 deletions(-) delete mode 100644 Assignment5-README.md diff --git a/Assignment5-README.md b/Assignment5-README.md deleted file mode 100644 index 734724eb..00000000 --- a/Assignment5-README.md +++ /dev/null @@ -1,11 +0,0 @@ -Note: This assumes you are running Mac or Linux. If you are running Windows, please run all commands on a Git Bash terminal, or use Windows Subsystem for Linux. - -To run our app on Docker: - -1. Ensure depedencies are installed correctly (see [README.md](README.md)) - 1.1 Install the latest version of node.js v18 - 1.2 Install the latest version of yarn (`npm install -g yarn`) - 1.3 Install the latest version of docker -2. Create a `.env` file at the project root and copy the environment secrets from the uploaded file on CANVAS. -3. Start the app using `source start-app-with-docker.sh` -4. Open your browser and go to `localhost:3000` diff --git a/README.md b/README.md index 5ff790ea..71cfc2ab 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ [![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-24ddc0f5d75046c5622901739e7c5dd533143b0c8e959d652212380cedb1ea36.svg)](https://classroom.github.com/a/6BOvYMwN) -## PeerPrep Monorepo User Guide +## Assignment 2 README -Prerequisites for PeerPrep Monorepo: +Prerequisites for PeerPrep Assignment 2: 1. **Yarn:** Ensure you have the latest version of Yarn installed. Yarn Workspaces is available in Yarn v1.0 and later. @@ -14,9 +14,8 @@ Prerequisites for PeerPrep Monorepo: 3. **Node.js:** Check each application's documentation for the recommended Node.js version. -4. **Git (Optional but Recommended):** -5. **Docker (If deploying with Docker):** -6. **Kubernetes Tools (If deploying with Kubernetes):** +4. **Git** +5. **Postman** or any other REST API testing tool (optional) --- @@ -30,12 +29,8 @@ your services / frontend. ├── /services │ ├── /admin-service (express application) │ ├── /user-service (express application) -│ ├── /matching-service (express application) │ ├── /question-service (express application) ├── /frontend -│ └── /pages for peerprep (NextJs application) -├── /deployment -│ └── /prod-dockerfiles (Images can be used with either dev or prod environments) ├── .env (not in git) ├── .env.firebase_emulators_test (not in git) └── README.md (and other root-level files & docs) @@ -45,11 +40,11 @@ your services / frontend. 1. Ensure that you have an `.env` file at the root directory with the following variables: `bash - PRISMA_DATABASE_URL= - MONGO_ATLAS_URL= - FIREBASE_SERVICE_ACCOUNT= - NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG={"apiKey": ,"authDomain": ,"projectId": ,"storageBucket": ,"messagingSenderId": ,"appId": } - ` + PRISMA_DATABASE_URL= + MONGO_ATLAS_URL= + FIREBASE_SERVICE_ACCOUNT= + NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG={"apiKey": ,"authDomain": ,"projectId": ,"storageBucket": ,"messagingSenderId": ,"appId": } + ` Note: For `NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG`, the JSON should not have newlines since Next.js may not process it correctly. The difference between it and `FIREBASE_SERVICE_ACCOUNT` are shown below: @@ -58,125 +53,38 @@ your services / frontend. | FIREBASE_SERVICE_ACCOUNT | For backend verification and administrative tasks | | NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG | For the frontend to connect to Firebase | -2. **Installing secret detection hooks:** From the root directory, run: - ```bash - pip install pre-commit - pre-commit install - ``` +Copy the environment secrets from the uploaded file on CANVAS. -**Disclaimer:** There is no guarantee that all secrets will be detected. -As a tip, if you think a file will eventually store secrets, immediately add it to .gitignore upon creating -it in case you forget later on when you have a lot more files to commit. -3. **Installing Dependencies:** From the root directory (`/peerprep`), run: +2. **Installing Dependencies:** From the root directory (`/peerprep`), run: ```bash - yarn install + yarn install --frozen-lockfile ``` or ```bash - yarnpkg install + yarnpkg install --frozen-lockfile ``` (if you have hadoop yarn installed) - This command will install dependencies for all services and the frontend in a - centralized `node_modules` directory at the root. - -4. **Adding Dependencies:** To add a dependency to a specific workspace (e.g., - `user-service`), use: - - ```bash - yarn workspace user-service add [dependency-name] - ``` - -5. **Initializing Prisma:** In the root file, run the following: +3. **Initializing Prisma:** In the root file, run the following: ```bash yarn prisma generate ## Do this whenever we change the models in schema.prisma ``` -6. **Running Backend Scripts:** To run a script specific to a workspace (e.g., - the `start` script for `user-service`), use: +4. **Running Scripts:** On separate tabs, run the following scripts: ```bash - yarn workspace user-service start + yarn workspace user-service dev:local + yarn workspace question-service dev:local + yarn workspace frontend dev:local ``` - -7. **Running Frontend Scripts:** To run the frontend cod, use: - - ```bash - yarn workspace frontend dev ## For development - - # or - - yarn workspace frontend build ## For first time setup run the build command - yarn workspace frontend start ## For subsequent runs - ``` - -8. **Running everything at once:** To run everything at once and still maintain the ability to hot-reload your changes, use: - - ```bash - ./start-app-no-docker.sh # on mac /linux - - # You can also use the above command on Windows with Git Bash - - ``` - -### Getting Started - Docker: - -Docker and Docker Compose are used to set up a simulated production build (meaning that the Docker images and -containers that will be spun up locally are almost identical to those in the production environment, with the exception -of some environment variables). - -1. **Run yarn docker:build:** From the root repo, run - -```bash -yarn docker:build -``` - -This will create new Docker images. - -2. **Run yarn docker:devup:** From the root repo, run - -```bash -yarn docker:devup -``` - -This will start all the containers. - -3. **Once done, run yarn docker:devdown:** From the root repo, run - -```bash -yarn docker:devdown -``` - -This will stop and delete all the containers. - -#### If you want to do all the above steps at once, see the below section - -**Run the start-app-with-docker.sh script:** From the root repo, run - -```bash -./start-app-with-docker.sh # on mac / linux - -# You can also use the above command on Windows with Git Bash -``` - -This will create new Docker images everytime it is run. Be careful of how much disk space you have left. - -Any edits you make to the source code will not be automatically reflected on the site. We recommend using Docker -Compose to check if your changes are likely to work on the production environment once they have been proven to work -in your local development environment. - -### Notes: - -- After setting up Yarn Workspaces, any `node_modules` directories in individual - services or applications can be safely removed. -- Always ensure thorough testing after adding or updating dependencies to ensure - all parts of the system function as expected. +You may also run `yarn workspace admin-service dev:local` if you want to set/remove admin permissions on a user but +otherwise, this is not necessary since admin verification is done within the respective services. ### Prisma Notes From 7ccfee7e80fbba6b457fa507371c9a9b17ce74b2 Mon Sep 17 00:00:00 2001 From: Ong Jun Xiong Date: Fri, 17 Nov 2023 22:55:10 +0800 Subject: [PATCH 11/19] update assignemnt 2 --- README.md | 19 +++---- frontend/src/pages/_app.tsx | 14 ----- frontend/src/pages/profile/_profile.tsx | 65 ++---------------------- frontend/src/pages/settings/_account.tsx | 4 -- frontend/src/pages/settings/index.tsx | 40 +++------------ frontend/tsconfig.json | 23 +++++++-- yarn.lock | 59 ++------------------- 7 files changed, 43 insertions(+), 181 deletions(-) diff --git a/README.md b/README.md index 71cfc2ab..4abc5014 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ Prerequisites for PeerPrep Assignment 2: 3. **Node.js:** Check each application's documentation for the recommended Node.js version. 4. **Git** -5. **Postman** or any other REST API testing tool (optional) +5. **Dotenv** - `yarn global add dotenv` - This is used to open the services +6. **Postman** or any other REST API testing tool (optional) --- @@ -40,11 +41,11 @@ your services / frontend. 1. Ensure that you have an `.env` file at the root directory with the following variables: `bash - PRISMA_DATABASE_URL= - MONGO_ATLAS_URL= - FIREBASE_SERVICE_ACCOUNT= - NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG={"apiKey": ,"authDomain": ,"projectId": ,"storageBucket": ,"messagingSenderId": ,"appId": } - ` +PRISMA_DATABASE_URL= +MONGO_ATLAS_URL= +FIREBASE_SERVICE_ACCOUNT= +NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG={"apiKey": ,"authDomain": ,"projectId": ,"storageBucket": ,"messagingSenderId": ,"appId": } +` Note: For `NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG`, the JSON should not have newlines since Next.js may not process it correctly. The difference between it and `FIREBASE_SERVICE_ACCOUNT` are shown below: @@ -55,7 +56,6 @@ your services / frontend. Copy the environment secrets from the uploaded file on CANVAS. - 2. **Installing Dependencies:** From the root directory (`/peerprep`), run: ```bash @@ -83,8 +83,9 @@ Copy the environment secrets from the uploaded file on CANVAS. yarn workspace question-service dev:local yarn workspace frontend dev:local ``` -You may also run `yarn workspace admin-service dev:local` if you want to set/remove admin permissions on a user but -otherwise, this is not necessary since admin verification is done within the respective services. + + You may also run `yarn workspace admin-service dev:local` if you want to set/remove admin permissions on a user but + otherwise, this is not necessary since admin verification is done within the respective services. ### Prisma Notes diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index b2f93f63..ff59d213 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -3,8 +3,6 @@ import type { AppProps } from "next/app"; import Layout from "../components/common/layout"; import { Noto_Sans } from "next/font/google"; import AuthContextProvider from "@/contexts/AuthContext"; -import { ToastContainer } from "react-toastify"; -import "react-toastify/dist/ReactToastify.css"; const notoSans = Noto_Sans({ weight: ["400", "500", "600", "700", "800", "900"], @@ -21,18 +19,6 @@ export default function App({ Component, pageProps }: AppProps) { `}
- diff --git a/frontend/src/pages/profile/_profile.tsx b/frontend/src/pages/profile/_profile.tsx index 16605c84..4beeb78c 100644 --- a/frontend/src/pages/profile/_profile.tsx +++ b/frontend/src/pages/profile/_profile.tsx @@ -1,14 +1,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; -import { - TypographyBody, - TypographyH3, - TypographyH2, -} from "@/components/ui/typography"; +import { TypographyBody, TypographyH3 } from "@/components/ui/typography"; import Link from "next/link"; -import ActivityCalendar, { Activity } from "react-activity-calendar"; -import { Tooltip as MuiTooltip } from "@mui/material"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; export type UserProfile = { uid: string; @@ -23,38 +16,13 @@ type ProfileProps = { isCurrentUser: boolean; }; -export default function Profile({ - selectedUser, - isCurrentUser, - loadingState, -}: ProfileProps) { +export default function Profile({ selectedUser, isCurrentUser }: ProfileProps) { console.log(selectedUser); const getInitials = (name: string) => { if (!name) return "Annonymous"; - const names = name.split(" "); - let initials = ""; - names.forEach((n) => { - initials += n[0].toUpperCase(); - }); - return initials; + return name; }; - const date = new Date(); - const dateTodayString = date.toISOString().slice(0, 10); - date.setFullYear(date.getFullYear() - 1); - const dateLastYearString = date.toISOString().slice(0, 10); - - // We need a date from last year to make sure the calendar is styled properly - const countsByDate: Record = { - [dateTodayString]: { date: dateTodayString, count: 0, level: 0 }, - [dateLastYearString]: { date: dateLastYearString, count: 0, level: 0 }, - }; - - // Extract the values from the dictionary to get the final activities array - const activities = Object.values(countsByDate).sort((a, b) => - a.date.localeCompare(b.date) - ); - return (
@@ -78,33 +46,6 @@ export default function Profile({ )}
-
- - - - Activity - - - - ( - - {block} - - )} - labels={{ - totalCount: "{{count}} activities in 2023", - }} - /> - - -
); } diff --git a/frontend/src/pages/settings/_account.tsx b/frontend/src/pages/settings/_account.tsx index a994337a..e24c7523 100644 --- a/frontend/src/pages/settings/_account.tsx +++ b/frontend/src/pages/settings/_account.tsx @@ -38,10 +38,6 @@ export default function AccountSettingsCard() { uid: currentUser?.uid ?? "", } as EditableUser); - useEffect(() => { - console.log(updatedUser); - }, [updatedUser]); - return ( diff --git a/frontend/src/pages/settings/index.tsx b/frontend/src/pages/settings/index.tsx index 599ad6ad..55582b5c 100644 --- a/frontend/src/pages/settings/index.tsx +++ b/frontend/src/pages/settings/index.tsx @@ -2,23 +2,8 @@ import { Button } from "@/components/ui/button"; import { TypographyH1 } from "@/components/ui/typography"; -import { UserIcon, Settings2Icon } from "lucide-react"; import { useRouter } from "next/router"; import AccountSettingsCard from "./_account"; -import MatchSettingsCard from "./_match"; - -const settingsOptions = [ - { - title: "Account", - href: "/settings/#account", - icon: UserIcon, - }, - { - title: "Match Preferences", - href: "/settings/#match", - icon: Settings2Icon, - }, -] export default function Settings() { const router = useRouter(); @@ -26,26 +11,17 @@ export default function Settings() { return (
-
-
- Settings -
- {settingsOptions.map((option) => ( -
- -
- ))} -
-
-
+ Settings - +
- ) + ); } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 28c08247..c898f2b5 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -15,9 +19,18 @@ "jsx": "preserve", "incremental": true, "paths": { - "@/*": ["./src/*"] - } + "@/*": [ + "./src/*" + ] + }, + "forceConsistentCasingInFileNames": true }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] } diff --git a/yarn.lock b/yarn.lock index 970c9122..11344305 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2846,17 +2846,12 @@ dependencies: "@types/express" "*" -"@types/cookie@^0.4.1": - version "0.4.1" - resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" - integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== - "@types/cookiejar@*": version "2.1.3" resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.3.tgz#c54976fb8f3a32ea8da844f59f0374dd39656e13" integrity sha512-LZ8SD3LpNmLMDLkG2oCBjZg+ETnx6XdCjydUE0HwojDmnDfDUnhMKKbtth1TZh+hzcqb03azrYWoXLS8sMXdqg== -"@types/cors@^2.8.12", "@types/cors@^2.8.14": +"@types/cors@^2.8.14": version "2.8.15" resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.15.tgz#eb143aa2f8807ddd78e83cbff141bbedd91b60ee" integrity sha512-n91JxbNLD8eQIuXDIChAN1tCKNWCEgpceU9b7ZMbFA+P+Q4yIeh80jizFLEvolRPc1ES0VdwFlGv+kJTSirogw== @@ -3008,7 +3003,7 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== -"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^20.8.7": +"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^20.8.7": version "20.8.8" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.8.tgz#adee050b422061ad5255fc38ff71b2bb96ea2a0e" integrity sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ== @@ -3332,7 +3327,7 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" -accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: +accepts@~1.3.5, 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== @@ -3839,11 +3834,6 @@ base64-js@^1.3.0, base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -base64id@2.0.0, base64id@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" - integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== - basic-auth-connect@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz#fdb0b43962ca7b40456a7c2bb48fe173da2d2122" @@ -4502,11 +4492,6 @@ cookie@0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -cookie@~0.4.1: - version "0.4.2" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" - integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== - cookiejar@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" @@ -4529,7 +4514,7 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== -cors@^2.8.5, cors@~2.8.5: +cors@^2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== @@ -4991,22 +4976,6 @@ engine.io-parser@~5.2.1: resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.1.tgz#9f213c77512ff1a6cc0c7a86108a7ffceb16fcfb" integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ== -engine.io@~6.5.2: - version "6.5.3" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.3.tgz#80b0692912cef3a417e1b7433301d6397bf0374b" - integrity sha512-IML/R4eG/pUS5w7OfcDE0jKrljWS9nwnEfsxWCIJF5eO6AHo6+Hlv+lQbdlAYsiJPHzUthLm1RUjnBzWOs45cw== - dependencies: - "@types/cookie" "^0.4.1" - "@types/cors" "^2.8.12" - "@types/node" ">=10.0.0" - accepts "~1.3.4" - base64id "2.0.0" - cookie "~0.4.1" - cors "~2.8.5" - debug "~4.3.1" - engine.io-parser "~5.2.1" - ws "~8.11.0" - enhanced-resolve@^5.12.0: version "5.15.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" @@ -9804,13 +9773,6 @@ smart-buffer@^4.2.0: resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== -socket.io-adapter@~2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12" - integrity sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA== - dependencies: - ws "~8.11.0" - socket.io-client@*, socket.io-client@^4.7.2: version "4.7.2" resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.2.tgz#f2f13f68058bd4e40f94f2a1541f275157ff2c08" @@ -9829,19 +9791,6 @@ socket.io-parser@~4.2.4: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" -socket.io@^4.7.2: - version "4.7.2" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.2.tgz#22557d76c3f3ca48f82e73d68b7add36a22df002" - integrity sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw== - dependencies: - accepts "~1.3.4" - base64id "~2.0.0" - cors "~2.8.5" - debug "~4.3.2" - engine.io "~6.5.2" - socket.io-adapter "~2.5.2" - socket.io-parser "~4.2.4" - socks-proxy-agent@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz#dc069ecf34436621acb41e3efa66ca1b5fed15b6" From 8fa6f18ac2ad857950c440bafb29440161b9b142 Mon Sep 17 00:00:00 2001 From: Ong Jun Xiong Date: Fri, 17 Nov 2023 23:09:46 +0800 Subject: [PATCH 12/19] fix hydration issues --- frontend/src/components/common/navbar.tsx | 7 +- frontend/src/components/ui/button.tsx | 4 +- frontend/src/components/ui/card.tsx | 41 +++-- frontend/src/components/ui/form.tsx | 97 ++++++------ frontend/src/components/ui/typography.tsx | 24 ++- frontend/src/pages/questions/[id]/index.tsx | 4 +- frontend/src/pages/questions/_form.tsx | 165 +++++++++++++------- 7 files changed, 205 insertions(+), 137 deletions(-) diff --git a/frontend/src/components/common/navbar.tsx b/frontend/src/components/common/navbar.tsx index b76cb0e0..4959c713 100644 --- a/frontend/src/components/common/navbar.tsx +++ b/frontend/src/components/common/navbar.tsx @@ -68,7 +68,7 @@ export default function Navbar() { )} - {isAdmin &&

Admin Page

} + {isAdmin &&
Admin Page
} {!currentUser && (
); } @@ -40,16 +40,16 @@ export function TypographyH3({ className?: string; }) { return ( -

{children} -

+ ); } export function TypographyBody({ children }: { children: React.ReactNode }) { - return

{children}

; + return
{children}
; } export function TypographyBodyHeavy({ @@ -57,7 +57,7 @@ export function TypographyBodyHeavy({ }: { children: React.ReactNode; }) { - return

{children}

; + return
{children}
; } export function TypographySmall({ @@ -68,9 +68,9 @@ export function TypographySmall({ className?: string; }) { return ( - +
{children} - +
); } @@ -79,9 +79,7 @@ export function TypographySmallHeavy({ }: { children: React.ReactNode; }) { - return ( - {children} - ); + return
{children}
; } export function TypographyBlockquote({ diff --git a/frontend/src/pages/questions/[id]/index.tsx b/frontend/src/pages/questions/[id]/index.tsx index 71203a4c..e5a2a71b 100644 --- a/frontend/src/pages/questions/[id]/index.tsx +++ b/frontend/src/pages/questions/[id]/index.tsx @@ -22,7 +22,7 @@ export default function Questions() { if (!authIsReady || !questionId) { console.log("auth not ready or questionId not found"); return; - }; + } if (currentUser) { fetchQuestion(currentUser, questionId) .then((question) => { @@ -44,7 +44,7 @@ export default function Questions() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [questionId, authIsReady, currentUser]); - if (question === null && !loading) return

Question not found

; + if (question === null && !loading) return
Question not found
; return (
diff --git a/frontend/src/pages/questions/_form.tsx b/frontend/src/pages/questions/_form.tsx index 59978b92..4221db8a 100644 --- a/frontend/src/pages/questions/_form.tsx +++ b/frontend/src/pages/questions/_form.tsx @@ -18,21 +18,21 @@ import { UseFormReturn } from "react-hook-form"; export const formSchema = z.object({ title: z.string().min(2).max(100), - difficulty: z.enum(['easy', 'medium', 'hard']), + difficulty: z.enum(["easy", "medium", "hard"]), topics: z.array(z.string().min(2).max(100)), description: z.string().min(2).max(10000), testCasesInputs: z.array(z.string().min(2).max(10000)), testCasesOutputs: z.array(z.string().min(2).max(10000)), - defaultCode: z.object({ - "python": z.string().min(0).max(10000), - "java": z.string().min(0).max(10000), - "c++": z.string().min(0).max(10000) - }) || undefined, -}) - + defaultCode: + z.object({ + python: z.string().min(0).max(10000), + java: z.string().min(0).max(10000), + "c++": z.string().min(0).max(10000), + }) || undefined, +}); const defaultCodes = { - 'python': `def twoSum(self, nums: list[int], target: int) -> list[int]: + python: `def twoSum(self, nums: list[int], target: int) -> list[int]: pass if __name__ == "__main__": @@ -41,7 +41,7 @@ if __name__ == "__main__": target = int(input()) print(" ".join(twoSum(nums, target)))`, -'java': `import java.util.*; + java: `import java.util.*; class Solution { public int[] twoSum(int[] nums, int target) { @@ -62,7 +62,7 @@ class Solution { } }`, -'c++': `#include + "c++": `#include #include using namespace std; @@ -86,10 +86,9 @@ int main() { vector result = solution.twoSum(nums, target); cout << result[0] << " " << result[1] << endl; return 0; -}` +}`, }; - interface QuestionsFormProps { form: UseFormReturn>; onSubmit: any; @@ -105,15 +104,28 @@ export default function QuestionsForm({ type = "add", loading = false, }: QuestionsFormProps) { - const {testCasesInputs, testCasesOutputs} = form.getValues(); + const { testCasesInputs, testCasesOutputs } = form.getValues(); const [, forceUpdate] = useReducer((x) => x + 1, 0); - const createTopic = (label: string) => ({ value: label.toLowerCase(), label }); + const createTopic = (label: string) => ({ + value: label.toLowerCase(), + label, + }); - const topics = ["Algorithms", "Arrays", "Bit Manipulation", "Brainteaser", "Data Structures", "Databases", "Graph", "Recursion", "Strings"].map(createTopic); + const topics = [ + "Algorithms", + "Arrays", + "Bit Manipulation", + "Brainteaser", + "Data Structures", + "Databases", + "Graph", + "Recursion", + "Strings", + ].map(createTopic); useEffect(() => { - form.setValue('defaultCode', defaultCodes); + form.setValue("defaultCode", defaultCodes); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -185,46 +197,87 @@ export default function QuestionsForm({ )} /> -

Test cases

+
Test cases
{testCasesInputs.map((testCase, index) => { - return
-