diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..1320b9a3 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@babel/preset-env"] +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..031d766a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +**/.env +**/.env.* +**/secrets +.git diff --git a/.env.development.local b/.env.development.local new file mode 100644 index 00000000..e56c18bd --- /dev/null +++ b/.env.development.local @@ -0,0 +1,2 @@ +ENVIRONMENT_TYPE="local-dev" +NEXT_PUBLIC_ENVIRONMENT_TYPE="local-dev" diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..ca143645 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,19 @@ +{ + "env": { + "browser": true, + "node": true, + "es2021": true + }, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" + ], + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": { + "no-unused-vars": "off" + } +} diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 00000000..0ccd1348 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "peerprep-group11-dev" + } +} diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 00000000..a17da917 --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,72 @@ +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 new file mode 100644 index 00000000..b16d2016 --- /dev/null +++ b/.github/workflows/production.yml @@ -0,0 +1,133 @@ +# 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/.gitignore b/.gitignore new file mode 100644 index 00000000..409ec9bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules +.env +.idea/ +.env.firebase_emulators_test +secrets/ +yarn-error.log +bash.exe.stackdump +sh.exe.stackdump diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..587737a3 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: + - repo: https://github.com/gitleaks/gitleaks + rev: v8.16.1 + hooks: + - id: gitleaks + stages: [commit] diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..222861c3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 2, + "useTabs": false +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..6c2ff60b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "githubPullRequests.ignoredPullRequestBranches": [ + "master" + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..f282ccc8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +# 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/G11_Report.pdf b/G11_Report.pdf new file mode 100644 index 00000000..67f2d61b Binary files /dev/null and b/G11_Report.pdf differ diff --git a/README.md b/README.md index 726ba36b..99042a6a 100644 --- a/README.md +++ b/README.md @@ -1 +1,240 @@ -# AssignmentTemplate \ No newline at end of file +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-24ddc0f5d75046c5622901739e7c5dd533143b0c8e959d652212380cedb1ea36.svg)](https://classroom.github.com/a/6BOvYMwN) + +## PeerPrep Monorepo User Guide + +Prerequisites for PeerPrep Monorepo: + +1. **Yarn:** Ensure you have the latest version of Yarn installed. Yarn + Workspaces is available in Yarn v1.0 and later. +2. Installation (if not already installed): + + ```bash + npm install -g yarn + ``` + +3. **Node.js:** Check each application's documentation for the recommended + Node.js version. +4. **Git** +5. **Docker (If deploying with Docker):** +6. **Kubernetes Tools (If deploying with Kubernetes):** + +--- + +Adjust these prerequisites based on the specific requirements of +your services / frontend. + +### Structure: + +``` +/peerprep +├── /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 +│ ├── /gke-prod-manifests +│ ├── /prod-dockerfiles +│ └── build-export-prod-images.sh +├── /prisma +├── /utils +├── .env (not in git) +├── .env.firebase_emulators_test (not in git) +└── README.md (and other root-level files & docs) +``` + +### 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 + ``` +This step requires Git to be installed. + +**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 + yarn install + ``` + + or + + ```bash + yarnpkg install + ``` + + (if you have hadoop yarn installed) + + If you want to use the exact `yarn.lock` versions without any modification, add a `--frozen-lockfile` flag to the + command. + + This command will install dependencies for all services and the frontend in a + centralized `node_modules` directory at the root as well as within the respective directories. + +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: + + ```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: + + ```bash + yarn workspace user-service start + ``` + +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). + +NOTE: Do not run both Docker and No Docker at the same time. This will cause port conflicts. + +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. + + + +#### 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. + +3. **Once done, run yarn docker:devdown:** From the root repo, run +```bash +yarn docker:devdown +``` +This will stop and delete all the containers. You must run this to delete containers regardless of whether you used +`./start-app-with-docker.sh` or `yarn docker:devup`. + +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. + +### Playing around with the app locally + +- To test all features of the application, you will need 2 GitHub accounts. +- You must login on both accounts on the same computer, since the setup assumes that services are centralised +services just like in the production environment. +- You can do the login for your second account by opening a new browser tab in +incognito mode. + +#### Note on NUS WiFi: + +Access to remote databases may be blocked when using NUS WiFi. You will need a VPN to overcome this limitation if you +are running the local app on campus. + +### Dependency Notes: + +- Always ensure thorough testing after adding or updating dependencies to ensure + all parts of the system function as expected. + +### Prisma Notes + +Next steps: + +1. Set the PRISMA_DATABASE_URL in the .env file to point to your existing database. If + your database has no tables yet, read https://pris.ly/d/getting-started +2. Set the provider of the datasource block in schema.prisma to match your + database: postgresql, mysql, sqlite, sqlserver, mongodb or cockroachdb. +3. Run prisma db pull to turn your database schema into a Prisma schema. +4. Run prisma generate to generate the Prisma Client. You can then start + querying your database. + +``` + +``` + +### 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 + +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} +``` + +In the CI environment, the environment variables have to be defined separately. +For example, FIREBASE_SERVICE_ACCOUNT will be passed in as a secret on GitHub Actions. diff --git a/css/styles.css b/css/styles.css deleted file mode 100644 index e115fbb8..00000000 --- a/css/styles.css +++ /dev/null @@ -1 +0,0 @@ -/* blank style sheet */ diff --git a/deployment/build-export-prod-images.sh b/deployment/build-export-prod-images.sh new file mode 100644 index 00000000..f859d851 --- /dev/null +++ b/deployment/build-export-prod-images.sh @@ -0,0 +1,26 @@ +#!/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/Prod_Node_Specifications.md b/deployment/gke-prod-manifests/Prod_Node_Specifications.md new file mode 100644 index 00000000..08876180 --- /dev/null +++ b/deployment/gke-prod-manifests/Prod_Node_Specifications.md @@ -0,0 +1,12 @@ +# Specifications for GKE Nodes +The nodes used in GKE are of the machine type e2-standard-4 + +| Key | Value | +|-----|-------| +| vCPUs per node | 4 | +| Memory size | 16 GB | +| Boot disk size | 50 GB | +| Node image type | cos_containerd | + +The GKE cluster is created in asia-southeast1 region across all 3 availability zones. +1 node is created per zone. diff --git a/deployment/gke-prod-manifests/admin-service-autoscaling.yaml b/deployment/gke-prod-manifests/admin-service-autoscaling.yaml new file mode 100644 index 00000000..9b007ece --- /dev/null +++ b/deployment/gke-prod-manifests/admin-service-autoscaling.yaml @@ -0,0 +1,18 @@ +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 new file mode 100644 index 00000000..e82736c0 --- /dev/null +++ b/deployment/gke-prod-manifests/admin-service-deployment.yaml @@ -0,0 +1,42 @@ +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 + imagePullPolicy: Always + 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: "250m" + restartPolicy: Always +status: {} diff --git a/deployment/gke-prod-manifests/admin-service-service.yaml b/deployment/gke-prod-manifests/admin-service-service.yaml new file mode 100644 index 00000000..fc14bd05 --- /dev/null +++ b/deployment/gke-prod-manifests/admin-service-service.yaml @@ -0,0 +1,16 @@ +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 new file mode 100644 index 00000000..524b4f91 --- /dev/null +++ b/deployment/gke-prod-manifests/collaboration-service-autoscaling.yaml @@ -0,0 +1,18 @@ +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 new file mode 100644 index 00000000..6118b9f4 --- /dev/null +++ b/deployment/gke-prod-manifests/collaboration-service-deployment.yaml @@ -0,0 +1,59 @@ +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 + imagePullPolicy: Always + 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: "250m" + restartPolicy: Always +status: {} diff --git a/deployment/gke-prod-manifests/collaboration-service-service.yaml b/deployment/gke-prod-manifests/collaboration-service-service.yaml new file mode 100644 index 00000000..028a83dc --- /dev/null +++ b/deployment/gke-prod-manifests/collaboration-service-service.yaml @@ -0,0 +1,16 @@ +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 new file mode 100644 index 00000000..5a178863 --- /dev/null +++ b/deployment/gke-prod-manifests/frontend-autoscaling.yaml @@ -0,0 +1,18 @@ +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 new file mode 100644 index 00000000..9db64d39 --- /dev/null +++ b/deployment/gke-prod-manifests/frontend-deployment.yaml @@ -0,0 +1,34 @@ +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 + imagePullPolicy: Always + name: frontend + ports: + - containerPort: 3000 + hostPort: 3000 + protocol: TCP + resources: + # You must specify requests for CPU to autoscale + # based on CPU utilization + requests: + cpu: "250m" + restartPolicy: Always +status: {} diff --git a/deployment/gke-prod-manifests/frontend-ingress.yaml b/deployment/gke-prod-manifests/frontend-ingress.yaml new file mode 100644 index 00000000..c99b9e91 --- /dev/null +++ b/deployment/gke-prod-manifests/frontend-ingress.yaml @@ -0,0 +1,13 @@ +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 new file mode 100644 index 00000000..a72a5a12 --- /dev/null +++ b/deployment/gke-prod-manifests/frontend-service.yaml @@ -0,0 +1,16 @@ +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 new file mode 100644 index 00000000..8386b65b --- /dev/null +++ b/deployment/gke-prod-manifests/gateway-autoscaling.yaml @@ -0,0 +1,18 @@ +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 new file mode 100644 index 00000000..d390dc11 --- /dev/null +++ b/deployment/gke-prod-manifests/gateway-backend-config.yaml @@ -0,0 +1,14 @@ +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 new file mode 100644 index 00000000..3e3cde39 --- /dev/null +++ b/deployment/gke-prod-manifests/gateway-deployment.yaml @@ -0,0 +1,58 @@ +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 + imagePullPolicy: Always + 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: "250m" + restartPolicy: Always +status: {} diff --git a/deployment/gke-prod-manifests/gateway-http-ingress.yaml b/deployment/gke-prod-manifests/gateway-http-ingress.yaml new file mode 100644 index 00000000..cb6434ae --- /dev/null +++ b/deployment/gke-prod-manifests/gateway-http-ingress.yaml @@ -0,0 +1,13 @@ +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 new file mode 100644 index 00000000..0d9ffc66 --- /dev/null +++ b/deployment/gke-prod-manifests/gateway-service.yaml @@ -0,0 +1,28 @@ +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 new file mode 100644 index 00000000..b3900773 --- /dev/null +++ b/deployment/gke-prod-manifests/gateway-wscollaboration-ingress.yaml @@ -0,0 +1,13 @@ +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 new file mode 100644 index 00000000..5aea8575 --- /dev/null +++ b/deployment/gke-prod-manifests/gateway-wsmatch-ingress.yaml @@ -0,0 +1,13 @@ +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 new file mode 100644 index 00000000..4feaab2e --- /dev/null +++ b/deployment/gke-prod-manifests/gke-managed-cert.yaml @@ -0,0 +1,10 @@ +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 new file mode 100644 index 00000000..0e4c037d --- /dev/null +++ b/deployment/gke-prod-manifests/matching-service-autoscaling.yaml @@ -0,0 +1,18 @@ +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 new file mode 100644 index 00000000..0829c048 --- /dev/null +++ b/deployment/gke-prod-manifests/matching-service-deployment.yaml @@ -0,0 +1,46 @@ +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 + imagePullPolicy: Always + 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: "250m" + restartPolicy: Always +status: {} diff --git a/deployment/gke-prod-manifests/matching-service-service.yaml b/deployment/gke-prod-manifests/matching-service-service.yaml new file mode 100644 index 00000000..09c84100 --- /dev/null +++ b/deployment/gke-prod-manifests/matching-service-service.yaml @@ -0,0 +1,16 @@ +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 new file mode 100644 index 00000000..e9f91e48 --- /dev/null +++ b/deployment/gke-prod-manifests/question-service-autoscaling.yaml @@ -0,0 +1,18 @@ +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 new file mode 100644 index 00000000..c55b717f --- /dev/null +++ b/deployment/gke-prod-manifests/question-service-deployment.yaml @@ -0,0 +1,42 @@ +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 + imagePullPolicy: Always + 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: "250m" + restartPolicy: Always +status: {} diff --git a/deployment/gke-prod-manifests/question-service-service.yaml b/deployment/gke-prod-manifests/question-service-service.yaml new file mode 100644 index 00000000..3850127e --- /dev/null +++ b/deployment/gke-prod-manifests/question-service-service.yaml @@ -0,0 +1,16 @@ +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 new file mode 100644 index 00000000..05061a0b --- /dev/null +++ b/deployment/gke-prod-manifests/user-service-autoscaling.yaml @@ -0,0 +1,18 @@ +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 new file mode 100644 index 00000000..10b726d8 --- /dev/null +++ b/deployment/gke-prod-manifests/user-service-deployment.yaml @@ -0,0 +1,42 @@ +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 + imagePullPolicy: Always + 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: "250m" + restartPolicy: Always +status: {} diff --git a/deployment/gke-prod-manifests/user-service-service.yaml b/deployment/gke-prod-manifests/user-service-service.yaml new file mode 100644 index 00000000..37bae945 --- /dev/null +++ b/deployment/gke-prod-manifests/user-service-service.yaml @@ -0,0 +1,16 @@ +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 new file mode 100644 index 00000000..84ae7943 --- /dev/null +++ b/deployment/prod-dockerfiles/Dockerfile.admin-service @@ -0,0 +1,24 @@ +# 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 new file mode 100644 index 00000000..85457b6e --- /dev/null +++ b/deployment/prod-dockerfiles/Dockerfile.collaboration-service @@ -0,0 +1,28 @@ +# 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 new file mode 100644 index 00000000..974aca87 --- /dev/null +++ b/deployment/prod-dockerfiles/Dockerfile.frontend @@ -0,0 +1,43 @@ +# 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 new file mode 100644 index 00000000..a243c5c8 --- /dev/null +++ b/deployment/prod-dockerfiles/Dockerfile.gateway @@ -0,0 +1,24 @@ +# 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 new file mode 100644 index 00000000..91a5ffc4 --- /dev/null +++ b/deployment/prod-dockerfiles/Dockerfile.matching-service @@ -0,0 +1,24 @@ +# 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 new file mode 100644 index 00000000..375efea8 --- /dev/null +++ b/deployment/prod-dockerfiles/Dockerfile.question-service @@ -0,0 +1,24 @@ +# 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 new file mode 100644 index 00000000..49d84fb2 --- /dev/null +++ b/deployment/prod-dockerfiles/Dockerfile.user-service @@ -0,0 +1,24 @@ +# 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 new file mode 100644 index 00000000..0a9c5259 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,90 @@ +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 new file mode 100644 index 00000000..19a66cdf --- /dev/null +++ b/firebase.json @@ -0,0 +1,11 @@ +{ + "emulators": { + "singleProjectMode": true, + "auth": { + "port": 9099 + }, + "ui": { + "enabled": false + } + } +} diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 00000000..bffb357a --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 00000000..8f322f0d --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/Firebase.md b/frontend/Firebase.md new file mode 100644 index 00000000..cf3d2f1f --- /dev/null +++ b/frontend/Firebase.md @@ -0,0 +1,42 @@ +This project uses Firebase with GitHub authentication. + +## Acknowledgments +The code for Firebase was adapted from a tutorial on freeCodeCamp: https://www.freecodecamp.org/news/github-user-authentication-using-firebase-and-reactjs-with-hooks/ + +## How it works +There are a few files involved in the client-side authentication: + +| File | Purpose | +|------------------------------------|----------------------------------------------------------------| +| firebase-client/firebase_config.ts | Sets up the Firebase Auth interface | +| firebase-client/use*.ts | Function hooks for doing login/logout and deleting own account | +| contexts/AuthContext.tsx | Saves the logged in User within a global state | +| reducers/authReducer.ts | Changes the state based on login/logout actions | + +## Current supported actions + +| Action | Remarks | +| ---- |---------| +| Login | | +| Logout | | +| DeleteOwnAccount | Will also log the user out, so in authReducer.ts, it is treated as a LOGOUT action | + +## How to use the authentication code in the front end + +For saving the authentication context, wrap the app with `` as shown below: +``` +export default function App({ Component, pageProps }: AppProps) { + return + + +} +``` + +A logout button might have the following code: +``` + +``` + +`onClick` is used to run the `logout()` function exported from `useLogout.ts`. `isPending` is used for displaying a placeholder if the button is not yet ready. diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..791774e8 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,61 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with +[`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +Requirements + +1. yarn >= 1.22.19 +2. Node >= 18.13.0 + +Install Dependencies + +``` +yarn +``` + +First, run the development server: + +```bash +yarn dev:local +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the +result. + +You can start editing the page by modifying `pages/index.tsx`. The page +auto-updates as you edit the file. + +[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on +[http://localhost:3000/api/hello](http://localhost:3000/api/hello). This +endpoint can be edited in `pages/api/hello.ts`. + +The `pages/api` directory is mapped to `/api/*`. Files in this directory are +treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead +of React pages. + +This project uses +[`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to +automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js + features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out +[the Next.js GitHub repository](https://github.com/vercel/next.js/) - your +feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the +[Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) +from the creators of Next.js. + +Check out our +[Next.js deployment documentation](https://nextjs.org/docs/deployment) for more +details. diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 00000000..a08ee285 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/styles/globals.scss", + "baseColor": "slate", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/frontend/next.config.js b/frontend/next.config.js new file mode 100644 index 00000000..5dd865f2 --- /dev/null +++ b/frontend/next.config.js @@ -0,0 +1,9 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + experimental: { + externalDir: true, + }, +}; + +module.exports = nextConfig; diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..8e3d0f22 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,73 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "lint": "next lint", + "dev:local": "dotenv -e ../.env -- yarnpkg dev", + "dev": "next dev", + "build": "next build", + "start": "next start -H 0.0.0.0", + "build:local": "dotenv -e ../.env -- next build", + "start:local": "dotenv -e ../.env -- next start -H 0.0.0.0" + }, + "dependencies": { + "@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", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-tabs": "^1.0.4", + "@tanstack/react-query": "^5.0.0", + "@tanstack/react-table": "^8.10.4", + "@uiball/loaders": "^1.3.0", + "autoprefixer": "10.4.15", + "class-variance-authority": "^0.7.0", + "clsx": "^2.0.0", + "cmdk": "^0.2.0", + "diff-match-patch": "^1.0.5", + "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", + "react": "18.2.0", + "react-activity-calendar": "^2.0.2", + "react-dom": "18.2.0", + "react-hook-form": "^7.47.0", + "react-icons": "^4.11.0", + "react-markdown": "^9.0.0", + "react-syntax-highlighter": "^15.5.0", + "rehype-raw": "^7.0.0", + "sanitize-html": "^2.11.0", + "sass": "^1.69.0", + "socket.io-client": "^4.7.2", + "tailwind-merge": "^1.14.0", + "tailwindcss": "3.3.3", + "tailwindcss-animate": "^1.0.7", + "twilio-video": "^2.28.1", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/diff-match-patch": "^1.0.34", + "@types/lodash": "^4.14.199", + "@types/react": "^18.2.31", + "@types/react-dom": "^18.2.14", + "@types/react-syntax-highlighter": "^15.5.10", + "@types/sanitize-html": "^2.9.4", + "@types/socket.io-client": "^3.0.0", + "eslint-config-next": "^13.5.6" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 00000000..33ad091d --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/public/CodeParty.svg b/frontend/public/CodeParty.svg new file mode 100644 index 00000000..5c48c992 --- /dev/null +++ b/frontend/public/CodeParty.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 00000000..718d6fea Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/hero-image.jpeg b/frontend/public/hero-image.jpeg new file mode 100644 index 00000000..b3b17145 Binary files /dev/null and b/frontend/public/hero-image.jpeg differ diff --git a/frontend/public/next.svg b/frontend/public/next.svg new file mode 100644 index 00000000..5174b28c --- /dev/null +++ b/frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/vercel.svg b/frontend/public/vercel.svg new file mode 100644 index 00000000..d2f84222 --- /dev/null +++ b/frontend/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/common/auth-checker.tsx b/frontend/src/components/common/auth-checker.tsx new file mode 100644 index 00000000..2c0cdf75 --- /dev/null +++ b/frontend/src/components/common/auth-checker.tsx @@ -0,0 +1,33 @@ +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 new file mode 100644 index 00000000..009e8cfd --- /dev/null +++ b/frontend/src/components/common/difficulty-selector.tsx @@ -0,0 +1,62 @@ +import Loader from "../interviews/loader"; +import { Button } from "../ui/button"; + +type Difficulty = "easy" | "medium" | "hard" | "any"; + +interface DifficultySelectorProps { + onChange: (value: Difficulty) => void; + showAny: boolean; + value: Difficulty; + isLoading?: boolean; +} + +export default function DifficultySelector({ + onChange, + showAny, + value, + isLoading = false, +}: DifficultySelectorProps) { + const difficulties = [ + { label: "Easy", value: "easy" }, + { label: "Medium", value: "medium" }, + { label: "Hard", value: "hard" }, + ]; + + if (showAny) { + difficulties.push({ label: "Any", value: "any" }); + } + + return ( +
+ {difficulties.map((difficulty) => ( + + ))} +
+ ); +} + +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/footer.tsx b/frontend/src/components/common/footer.tsx new file mode 100644 index 00000000..f4c93a80 --- /dev/null +++ b/frontend/src/components/common/footer.tsx @@ -0,0 +1,9 @@ +import { TypographyLink } from "../ui/typography"; + +export default function Footer() { + return ( +
+ Source Code +
+ ) +} diff --git a/frontend/src/components/common/layout.tsx b/frontend/src/components/common/layout.tsx new file mode 100644 index 00000000..59d2c57c --- /dev/null +++ b/frontend/src/components/common/layout.tsx @@ -0,0 +1,13 @@ +import Navbar from "./navbar"; +import Footer from "./footer"; +import React from "react"; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + <> + +
{children}
+