diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml deleted file mode 100644 index a17da917..00000000 --- a/.github/workflows/integration.yml +++ /dev/null @@ -1,72 +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 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 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 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 diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml deleted file mode 100644 index b16d2016..00000000 --- a/.github/workflows/production.yml +++ /dev/null @@ -1,133 +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 }} - 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 }} - -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/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/README.md b/README.md index 5f79a4ca..4abc5014 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,22 @@ [![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. + 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. -4. **Git (Optional but Recommended):** -5. **Docker (If deploying with Docker):** -6. **Kubernetes Tools (If deploying with Kubernetes):** + Node.js version. +4. **Git** +5. **Dotenv** - `yarn global add dotenv` - This is used to open the services +6. **Postman** or any other REST API testing tool (optional) --- @@ -30,15 +30,8 @@ your services / frontend. ├── /services │ ├── /admin-service (express application) │ ├── /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 -│ ├── /docker -│ └── /kubernetes ├── .env (not in git) ├── .env.firebase_emulators_test (not in git) └── README.md (and other root-level files & docs) @@ -47,136 +40,52 @@ 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 | - -2. **Installing secret detection hooks:** From the root directory, run: - ```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 +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: + +| Variable | Purpose | +| ------------------------------------ | ------------------------------------------------- | +| FIREBASE_SERVICE_ACCOUNT | For backend verification and administrative tasks | +| NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG | For the frontend to connect to Firebase | + +Copy the environment secrets from the uploaded file on CANVAS. + +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 /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 -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 / linus - -# 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 @@ -195,19 +104,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 deleted file mode 100644 index 84ae7943..00000000 --- a/deployment/prod-dockerfiles/Dockerfile.admin-service +++ /dev/null @@ -1,24 +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 - -# 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 deleted file mode 100644 index 974aca87..00000000 --- a/deployment/prod-dockerfiles/Dockerfile.frontend +++ /dev/null @@ -1,43 +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/ -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 -# 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 - -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 deleted file mode 100644 index 91a5ffc4..00000000 --- a/deployment/prod-dockerfiles/Dockerfile.matching-service +++ /dev/null @@ -1,24 +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 - -# 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 deleted file mode 100644 index 375efea8..00000000 --- a/deployment/prod-dockerfiles/Dockerfile.question-service +++ /dev/null @@ -1,24 +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 - -# 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 deleted file mode 100644 index 49d84fb2..00000000 --- a/deployment/prod-dockerfiles/Dockerfile.user-service +++ /dev/null @@ -1,24 +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 - -# 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 deleted file mode 100644 index 0a9c5259..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,90 +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" - - 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: . - 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} - - 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/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/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/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/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/components/common/difficulty-selector.tsx b/frontend/src/components/common/difficulty-selector.tsx index 5f1ea278..4a6f79fa 100644 --- a/frontend/src/components/common/difficulty-selector.tsx +++ b/frontend/src/components/common/difficulty-selector.tsx @@ -1,4 +1,3 @@ -import Loader from "../interviews/loader"; import { Button } from "../ui/button"; type Difficulty = "easy" | "medium" | "hard" | "any"; @@ -47,3 +46,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/common/navbar.tsx b/frontend/src/components/common/navbar.tsx index 5581216c..4959c713 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 - - )}
- {isAdmin &&

Admin Page

} + {isAdmin &&
Admin Page
} {!currentUser && (
- ); - }, - }, - { - 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/interviews/loader.tsx b/frontend/src/components/interviews/loader.tsx deleted file mode 100644 index eeeb6c70..00000000 --- a/frontend/src/components/interviews/loader.tsx +++ /dev/null @@ -1,33 +0,0 @@ -export default function Loader() { - return ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ) -} diff --git a/frontend/src/components/profile/columns.tsx b/frontend/src/components/profile/columns.tsx deleted file mode 100644 index 73d9cf15..00000000 --- a/frontend/src/components/profile/columns.tsx +++ /dev/null @@ -1,51 +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(); - }, - enableSorting: true, - sortDescFirst: true, - }, - { - id: "actions", - header: "Actions", - cell: ({ row }) => { - const attemptId = row.original.id; - return ( - - ); - }, - }, -]; 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/code-editor.tsx b/frontend/src/components/room/code-editor.tsx index d9421681..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, solved: boolean) => void; - onLeaveRoomClick?: () => void; -}; - export const languages = [ { value: "python", @@ -55,170 +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 (solved: boolean) => { - if (isSubmitting) { - return; // Do nothing if a submission is already in progress. - } - setIsSubmitting(true); - try { - onSubmitClick(monacoInstance?.getValue() ?? value, solved); - } 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/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/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/components/ui/alt-button.tsx b/frontend/src/components/ui/alt-button.tsx new file mode 100644 index 00000000..2fc21ac0 --- /dev/null +++ b/frontend/src/components/ui/alt-button.tsx @@ -0,0 +1,55 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center rounded-md text-base font-semibold ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-30 cursor-pointer", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/80", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border-2 border-primary bg-transparent hover:bg-accent text-primary", + secondary: "bg-accent text-accent-foreground hover:bg-accent/50", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-12 px-4 py-3", + sm: "h-11 rounded-md px-3", + lg: "h-13 rounded-md px-8", + icon: "h-12 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button2 = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + } +); +Button2.displayName = "Button2"; + +export { Button2, buttonVariants }; diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index 63f91ef5..de3d3d63 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const buttonVariants = cva( - "inline-flex items-center justify-center rounded-md text-base font-semibold ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-30", + "inline-flex items-center justify-center rounded-md text-base font-semibold ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-30 cursor-pointer", { variants: { variant: { diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx index 1a9fb6b6..671c2e10 100644 --- a/frontend/src/components/ui/card.tsx +++ b/frontend/src/components/ui/card.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Card = React.forwardRef< HTMLDivElement, @@ -14,8 +14,8 @@ const Card = React.forwardRef< )} {...props} /> -)) -Card.displayName = "Card" +)); +Card.displayName = "Card"; const CardHeader = React.forwardRef< HTMLDivElement, @@ -26,14 +26,14 @@ const CardHeader = React.forwardRef< className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} /> -)) -CardHeader.displayName = "CardHeader" +)); +CardHeader.displayName = "CardHeader"; const CardTitle = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -

-)) -CardTitle.displayName = "CardTitle" +)); +CardTitle.displayName = "CardTitle"; const CardDescription = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -

-)) -CardDescription.displayName = "CardDescription" +)); +CardDescription.displayName = "CardDescription"; const CardContent = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (

-)) -CardContent.displayName = "CardContent" +)); +CardContent.displayName = "CardContent"; const CardFooter = React.forwardRef< HTMLDivElement, @@ -73,7 +73,14 @@ const CardFooter = React.forwardRef< className={cn("flex items-center p-6 pt-0", className)} {...props} /> -)) -CardFooter.displayName = "CardFooter" +)); +CardFooter.displayName = "CardFooter"; -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/frontend/src/components/ui/form.tsx b/frontend/src/components/ui/form.tsx index 4603f8b3..70720927 100644 --- a/frontend/src/components/ui/form.tsx +++ b/frontend/src/components/ui/form.tsx @@ -1,6 +1,6 @@ -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" -import { Slot } from "@radix-ui/react-slot" +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { Slot } from "@radix-ui/react-slot"; import { Controller, ControllerProps, @@ -8,23 +8,23 @@ import { FieldValues, FormProvider, useFormContext, -} from "react-hook-form" +} from "react-hook-form"; -import { cn } from "@/lib/utils" -import { Label } from "@/components/ui/label" +import { cn } from "@/lib/utils"; +import { Label } from "@/components/ui/label"; -const Form = FormProvider +const Form = FormProvider; type FormFieldContextValue< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath > = { - name: TName -} + name: TName; +}; const FormFieldContext = React.createContext( {} as FormFieldContextValue -) +); const FormField = < TFieldValues extends FieldValues = FieldValues, @@ -36,21 +36,21 @@ const FormField = < - ) -} + ); +}; const useFormField = () => { - const fieldContext = React.useContext(FormFieldContext) - const itemContext = React.useContext(FormItemContext) - const { getFieldState, formState } = useFormContext() + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); - const fieldState = getFieldState(fieldContext.name, formState) + const fieldState = getFieldState(fieldContext.name, formState); if (!fieldContext) { - throw new Error("useFormField should be used within ") + throw new Error("useFormField should be used within "); } - const { id } = itemContext + const { id } = itemContext; return { id, @@ -59,36 +59,36 @@ const useFormField = () => { formDescriptionId: `${id}-form-item-description`, formMessageId: `${id}-form-item-message`, ...fieldState, - } -} + }; +}; type FormItemContextValue = { - id: string -} + id: string; +}; const FormItemContext = React.createContext( {} as FormItemContextValue -) +); const FormItem = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => { - const id = React.useId() + const id = React.useId(); return (
- ) -}) -FormItem.displayName = "FormItem" + ); +}); +FormItem.displayName = "FormItem"; const FormLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => { - const { error, formItemId } = useFormField() + const { error, formItemId } = useFormField(); return (
+ ); +}); +FormMessage.displayName = "FormMessage"; export { useFormField, @@ -173,4 +174,4 @@ export { FormDescription, FormMessage, FormField, -} +}; diff --git a/frontend/src/components/ui/typography.tsx b/frontend/src/components/ui/typography.tsx index 961ba438..2d76a20a 100644 --- a/frontend/src/components/ui/typography.tsx +++ b/frontend/src/components/ui/typography.tsx @@ -8,11 +8,11 @@ export function TypographyH1({ className?: string; }) { return ( -

{children} -

+
); } @@ -24,11 +24,11 @@ export function TypographyH2({ className?: string; }) { return ( -

{children} -

+

); } @@ -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/firebase-client/useDeleteOwnAccount.ts b/frontend/src/firebase-client/useDeleteOwnAccount.ts index 355a46fb..9a05e10c 100644 --- a/frontend/src/firebase-client/useDeleteOwnAccount.ts +++ b/frontend/src/firebase-client/useDeleteOwnAccount.ts @@ -1,10 +1,16 @@ import { auth } from "./firebase_config"; import { AuthContext } from "../contexts/AuthContext"; import { useContext } from "react"; -import { userApiPathAddress } from "@/gateway-address/gateway-address"; -import { GithubAuthProvider, reauthenticateWithCredential, signInWithPopup } from "firebase/auth"; +import { userApiPathAddress } from "@/backend-address/backend-address"; +import { + GithubAuthProvider, + reauthenticateWithCredential, + signInWithPopup, +} from "firebase/auth"; +import { useRouter } from "next/router"; export const useDeleteOwnAccount = () => { + const router = useRouter(); const { dispatch } = useContext(AuthContext); const deleteOwnAccount = async () => { try { @@ -18,18 +24,19 @@ export const useDeleteOwnAccount = () => { throw new Error("Could not get credential"); } await reauthenticateWithCredential(currentUser, credential); - + await fetch(userApiPathAddress + currentUser.uid, { method: "DELETE", headers: { "User-Id-Token": idToken, - "User-Id": currentUser.uid + "User-Id": currentUser.uid, }, }); // This will delete the user from the Firebase Authentication database await currentUser.delete(); dispatch({ type: "LOGOUT" }); console.log("user logged out and deleted"); + router.push("/"); } else { console.log("You are not logged in."); } 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 22a736d6..00000000 --- a/frontend/src/gateway-address/gateway-address.ts +++ /dev/null @@ -1,24 +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 - */ -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 userApiPathAddress = httpProxyGatewayAddress + "api/user-service/"; -export const questionApiPathAddress = - httpProxyGatewayAddress + "api/question-service/"; -export const matchApiPathAddress = - httpProxyGatewayAddress + "api/matching-service/"; -export const collaborationApiPathAddress = - httpProxyGatewayAddress + "api/collaboration-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/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/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/_app.tsx b/frontend/src/pages/_app.tsx index 46951e52..ff59d213 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -3,10 +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"; const notoSans = Noto_Sans({ weight: ["400", "500", "600", "700", "800", "900"], @@ -23,25 +19,9 @@ export default function App({ Component, pageProps }: AppProps) { `}
- - - - - - - - + + +
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/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/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 206ee7ef..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"; @@ -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/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/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/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 3f730c22..00000000 --- a/frontend/src/pages/interviews/index.tsx +++ /dev/null @@ -1,222 +0,0 @@ -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 { - 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"; - -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); - 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]); - - // sync leaderboard data - - 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} - - ))} - - - - -
-
- -
-
- Leaderboard - -
-
-
-
- ); -} diff --git a/frontend/src/pages/interviews/match-found.tsx b/frontend/src/pages/interviews/match-found.tsx deleted file mode 100644 index bc2bdda9..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(`/room/${match?.roomId}`); - }; - - 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/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..4beeb78c 100644 --- a/frontend/src/pages/profile/_profile.tsx +++ b/frontend/src/pages/profile/_profile.tsx @@ -1,19 +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 { Attempt } from "@/types/UserTypes"; -import { User } from "firebase/auth"; +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"; -import { DataTable } from "@/components/profile/data-table"; -import { columns } from "@/components/profile/columns"; -import { DotWave } from "@uiball/loaders"; export type UserProfile = { uid: string; @@ -25,70 +13,16 @@ export type UserProfile = { type ProfileProps = { selectedUser: UserProfile; loadingState: "loading" | "error" | "success"; - attempts?: Attempt[]; isCurrentUser: boolean; }; -export default function Profile({ - selectedUser, - attempts, - 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 }, - }; - - 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) - ); - return (
@@ -112,53 +46,6 @@ export default function Profile({ )}
-
- - - - Activity - - - - ( - - {block} - - )} - labels={{ - totalCount: "{{count}} activities in 2023", - }} - /> - - - - - 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]/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/[id]/index.tsx b/frontend/src/pages/questions/[id]/index.tsx index f6716dfe..e5a2a71b 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"; @@ -8,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 @@ -22,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) => { @@ -43,22 +44,7 @@ export default function Questions() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [questionId, authIsReady, currentUser]); - function onSubmitClick(value: string, solved: boolean) { - postAttempt({ - uid: currentUser ? currentUser.uid : "user", - question_id: questionId, - answer: value || answer, - solved: solved, // assume true - }) - .catch((err: any) => { - console.log(err); - }) - .finally(() => { - router.push("/profile"); - }); - } - - if (question === null && !loading) return

Question not found

; + if (question === null && !loading) return
Question not found
; return (
@@ -92,15 +78,6 @@ export default function Questions() { /> -
- -
)} 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
- - - - - - - - - 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/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 3ea1a409..00000000 --- a/services/matching-service/src/controllers/matchingController.ts +++ /dev/null @@ -1,343 +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; - } - - 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(`Queued user ${userId}.`); - 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 - } and difficulty ${foundMatch.chosenDifficulty}` - ); - - // 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 () => { - console.log(`User ${userId} is no longer looking for a match`); - await prisma.waitingUser.deleteMany({ - where: { - userId: userId, - }, - }); - }; -} - -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"] -} 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/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 +} 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'] - } -}) diff --git a/start-app-no-docker.sh b/start-app-no-docker.sh index 85342f8f..b8496ef3 100755 --- a/start-app-no-docker.sh +++ b/start-app-no-docker.sh @@ -10,9 +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 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/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 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; -} diff --git a/yarn.lock b/yarn.lock index 7db30759..11344305 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" @@ -2860,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== @@ -2896,13 +2877,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 +2924,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 +2991,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== @@ -3036,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== @@ -3149,13 +3116,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 +3164,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" @@ -3372,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== @@ -3429,7 +3384,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 +3401,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 +3779,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" @@ -3886,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" @@ -3957,7 +3900,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== @@ -4549,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" @@ -4576,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== @@ -4684,11 +4622,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 +4664,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 +4788,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" @@ -5062,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" @@ -5515,11 +5413,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 +5458,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 +5794,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 +5904,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 +6085,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 +6109,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 +6542,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 +6831,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 +6922,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 +6932,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 +7119,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 +7231,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 +7523,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 +7984,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== @@ -8316,11 +8143,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" @@ -8538,14 +8360,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 +8467,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 +8518,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 +9019,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 +9031,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 +9310,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 +9418,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 +9604,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" @@ -10064,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" @@ -10089,19 +9791,6 @@ socket.io-parser@~4.2.4: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" -socket.io@*, 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" @@ -10205,11 +9894,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" @@ -10732,11 +10416,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 +10498,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 +10791,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 +10837,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 +11190,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"