From 50f12112d86c2ac6fcfd9a4c0c3af7c3bdee274e Mon Sep 17 00:00:00 2001 From: Alan Tai Date: Wed, 21 Sep 2022 09:41:37 +0800 Subject: [PATCH] Replace the no-code backend with a serverless implementation --- .github/workflows/cd.yml | 131 ++++++++++++++---- .github/workflows/ci.yml | 106 ++++++++++++-- .gitignore | 14 -- README.md | 38 +++-- backend/.funcignore | 6 + backend/.gitignore | 17 +++ backend/host.json | 15 ++ backend/libs/database.ts | 42 ++++++ backend/package.json | 23 +++ backend/sessions/function.json | 21 +++ backend/sessions/index.ts | 73 ++++++++++ backend/sessions/sample.dat | 3 + backend/tsconfig.json | 10 ++ .eslintrc.js => frontend/.eslintrc.js | 0 frontend/.gitignore | 13 ++ CNAME => frontend/CNAME | 0 babel.config.js => frontend/babel.config.js | 0 .../config-overrides.js | 0 package.json => frontend/package.json | 10 +- .../public}/data/disclaimer.md | 0 .../public}/data/install.sh.ejs | 10 ++ {public => frontend/public}/data/privacy.md | 0 {public => frontend/public}/data/terms.md | 0 {public => frontend/public}/favicon-16x16.png | Bin {public => frontend/public}/favicon-32x32.png | Bin {public => frontend/public}/favicon.ico | Bin {public => frontend/public}/index.html | 0 .../public}/safari-pinned-tab.svg | 0 {src => frontend/src}/App.tsx | 0 {src => frontend/src}/LogoLarge.png | Bin {src => frontend/src}/LogoSmall.png | Bin {src => frontend/src}/apis/appStore.ts | 0 {src => frontend/src}/apis/homebrew.ts | 0 {src => frontend/src}/apis/index.ts | 1 - {src => frontend/src}/apis/tweak.ts | 0 .../src}/components/ActionBar.test.tsx | 0 .../src}/components/ActionBar.tsx | 0 .../components/CopyableTextField.test.tsx | 0 .../src}/components/CopyableTextField.tsx | 0 .../src}/components/ExternalLink.tsx | 0 .../src}/components/FilteredItemGrid.test.tsx | 0 .../src}/components/FilteredItemGrid.tsx | 0 {src => frontend/src}/components/Footer.tsx | 0 .../src}/components/ItemGrid.test.tsx | 0 {src => frontend/src}/components/ItemGrid.tsx | 0 .../src}/components/ItemList.test.tsx | 0 {src => frontend/src}/components/ItemList.tsx | 0 .../src}/components/Loading.test.tsx | 0 {src => frontend/src}/components/Loading.tsx | 0 .../src}/components/MarkdownDialog.tsx | 0 .../src}/components/PopUpDialog.test.tsx | 0 .../src}/components/PopUpDialog.tsx | 0 .../src}/components/PopUpFooter.tsx | 0 .../src}/components/SearchBox.test.tsx | 0 .../src}/components/SearchBox.tsx | 0 .../src}/components/SelectableItem.test.tsx | 0 .../src}/components/SelectableItem.tsx | 0 .../__snapshots__/ActionBar.test.tsx.snap | 0 .../__snapshots__/ItemGrid.test.tsx.snap | 0 .../__snapshots__/ItemList.test.tsx.snap | 0 .../__snapshots__/Loading.test.tsx.snap | 0 .../__snapshots__/PopUpDialog.test.tsx.snap | 0 {src => frontend/src}/components/index.ts | 0 {src => frontend/src}/constants.ts | 2 +- {src => frontend/src}/hooks/index.ts | 0 {src => frontend/src}/hooks/redux.ts | 0 {src => frontend/src}/i18n/en.json | 0 {src => frontend/src}/i18n/index.ts | 0 {src => frontend/src}/index.css | 0 {src => frontend/src}/index.tsx | 0 .../src}/models/AppStoreSearchResults.ts | 0 .../src}/models/HomebrewFormulae.ts | 0 {src => frontend/src}/models/Item.ts | 0 {src => frontend/src}/models/Tweak.ts | 0 {src => frontend/src}/models/index.ts | 1 - {src => frontend/src}/redux/index.ts | 6 +- {src => frontend/src}/redux/sessionSlice.ts | 0 {src => frontend/src}/screens/About.test.tsx | 0 {src => frontend/src}/screens/About.tsx | 0 .../src}/screens/Disclaimer.test.tsx | 0 {src => frontend/src}/screens/Disclaimer.tsx | 0 {src => frontend/src}/screens/Home.test.tsx | 0 {src => frontend/src}/screens/Home.tsx | 26 ++-- .../src}/screens/ItemDetail.test.tsx | 0 {src => frontend/src}/screens/ItemDetail.tsx | 0 .../src}/screens/Privacy.test.tsx | 0 {src => frontend/src}/screens/Privacy.tsx | 0 .../src}/screens/SessionDetail.test.tsx | 9 +- .../src}/screens/SessionDetail.tsx | 13 +- {src => frontend/src}/screens/Terms.test.tsx | 0 {src => frontend/src}/screens/Terms.tsx | 0 .../screens/__snapshots__/About.test.tsx.snap | 0 .../__snapshots__/Disclaimer.test.tsx.snap | 0 .../screens/__snapshots__/Home.test.tsx.snap | 0 .../__snapshots__/Privacy.test.tsx.snap | 0 .../screens/__snapshots__/Terms.test.tsx.snap | 0 {src => frontend/src}/screens/index.ts | 0 {src => frontend/src}/setupTests.ts | 0 {src => frontend/src}/styles/index.ts | 0 {src => frontend/src}/utils/index.ts | 0 {src => frontend/src}/utils/test.tsx | 0 tsconfig.json => frontend/tsconfig.json | 0 src/apis/session.ts | 17 --- src/models/Session.ts | 6 - terraform/.gitignore | 6 + terraform/backends.tf | 10 ++ terraform/data.tf | 5 + terraform/databases.tf | 64 +++++++++ terraform/functions.tf | 47 +++++++ terraform/locals.tf | 4 + terraform/main.tf | 8 ++ terraform/outputs.tf | 3 + terraform/providers.tf | 24 ++++ terraform/provisioners.tf | 11 ++ terraform/storages.tf | 42 ++++++ terraform/variables.tf | 34 +++++ 116 files changed, 738 insertions(+), 133 deletions(-) create mode 100644 backend/.funcignore create mode 100644 backend/.gitignore create mode 100644 backend/host.json create mode 100644 backend/libs/database.ts create mode 100644 backend/package.json create mode 100644 backend/sessions/function.json create mode 100644 backend/sessions/index.ts create mode 100644 backend/sessions/sample.dat create mode 100644 backend/tsconfig.json rename .eslintrc.js => frontend/.eslintrc.js (100%) create mode 100644 frontend/.gitignore rename CNAME => frontend/CNAME (100%) rename babel.config.js => frontend/babel.config.js (100%) rename config-overrides.js => frontend/config-overrides.js (100%) rename package.json => frontend/package.json (94%) rename {public => frontend/public}/data/disclaimer.md (100%) rename {public => frontend/public}/data/install.sh.ejs (87%) rename {public => frontend/public}/data/privacy.md (100%) rename {public => frontend/public}/data/terms.md (100%) rename {public => frontend/public}/favicon-16x16.png (100%) rename {public => frontend/public}/favicon-32x32.png (100%) rename {public => frontend/public}/favicon.ico (100%) rename {public => frontend/public}/index.html (100%) rename {public => frontend/public}/safari-pinned-tab.svg (100%) rename {src => frontend/src}/App.tsx (100%) rename {src => frontend/src}/LogoLarge.png (100%) rename {src => frontend/src}/LogoSmall.png (100%) rename {src => frontend/src}/apis/appStore.ts (100%) rename {src => frontend/src}/apis/homebrew.ts (100%) rename {src => frontend/src}/apis/index.ts (77%) rename {src => frontend/src}/apis/tweak.ts (100%) rename {src => frontend/src}/components/ActionBar.test.tsx (100%) rename {src => frontend/src}/components/ActionBar.tsx (100%) rename {src => frontend/src}/components/CopyableTextField.test.tsx (100%) rename {src => frontend/src}/components/CopyableTextField.tsx (100%) rename {src => frontend/src}/components/ExternalLink.tsx (100%) rename {src => frontend/src}/components/FilteredItemGrid.test.tsx (100%) rename {src => frontend/src}/components/FilteredItemGrid.tsx (100%) rename {src => frontend/src}/components/Footer.tsx (100%) rename {src => frontend/src}/components/ItemGrid.test.tsx (100%) rename {src => frontend/src}/components/ItemGrid.tsx (100%) rename {src => frontend/src}/components/ItemList.test.tsx (100%) rename {src => frontend/src}/components/ItemList.tsx (100%) rename {src => frontend/src}/components/Loading.test.tsx (100%) rename {src => frontend/src}/components/Loading.tsx (100%) rename {src => frontend/src}/components/MarkdownDialog.tsx (100%) rename {src => frontend/src}/components/PopUpDialog.test.tsx (100%) rename {src => frontend/src}/components/PopUpDialog.tsx (100%) rename {src => frontend/src}/components/PopUpFooter.tsx (100%) rename {src => frontend/src}/components/SearchBox.test.tsx (100%) rename {src => frontend/src}/components/SearchBox.tsx (100%) rename {src => frontend/src}/components/SelectableItem.test.tsx (100%) rename {src => frontend/src}/components/SelectableItem.tsx (100%) rename {src => frontend/src}/components/__snapshots__/ActionBar.test.tsx.snap (100%) rename {src => frontend/src}/components/__snapshots__/ItemGrid.test.tsx.snap (100%) rename {src => frontend/src}/components/__snapshots__/ItemList.test.tsx.snap (100%) rename {src => frontend/src}/components/__snapshots__/Loading.test.tsx.snap (100%) rename {src => frontend/src}/components/__snapshots__/PopUpDialog.test.tsx.snap (100%) rename {src => frontend/src}/components/index.ts (100%) rename {src => frontend/src}/constants.ts (67%) rename {src => frontend/src}/hooks/index.ts (100%) rename {src => frontend/src}/hooks/redux.ts (100%) rename {src => frontend/src}/i18n/en.json (100%) rename {src => frontend/src}/i18n/index.ts (100%) rename {src => frontend/src}/index.css (100%) rename {src => frontend/src}/index.tsx (100%) rename {src => frontend/src}/models/AppStoreSearchResults.ts (100%) rename {src => frontend/src}/models/HomebrewFormulae.ts (100%) rename {src => frontend/src}/models/Item.ts (100%) rename {src => frontend/src}/models/Tweak.ts (100%) rename {src => frontend/src}/models/index.ts (84%) rename {src => frontend/src}/redux/index.ts (83%) rename {src => frontend/src}/redux/sessionSlice.ts (100%) rename {src => frontend/src}/screens/About.test.tsx (100%) rename {src => frontend/src}/screens/About.tsx (100%) rename {src => frontend/src}/screens/Disclaimer.test.tsx (100%) rename {src => frontend/src}/screens/Disclaimer.tsx (100%) rename {src => frontend/src}/screens/Home.test.tsx (100%) rename {src => frontend/src}/screens/Home.tsx (86%) rename {src => frontend/src}/screens/ItemDetail.test.tsx (100%) rename {src => frontend/src}/screens/ItemDetail.tsx (100%) rename {src => frontend/src}/screens/Privacy.test.tsx (100%) rename {src => frontend/src}/screens/Privacy.tsx (100%) rename {src => frontend/src}/screens/SessionDetail.test.tsx (76%) rename {src => frontend/src}/screens/SessionDetail.tsx (82%) rename {src => frontend/src}/screens/Terms.test.tsx (100%) rename {src => frontend/src}/screens/Terms.tsx (100%) rename {src => frontend/src}/screens/__snapshots__/About.test.tsx.snap (100%) rename {src => frontend/src}/screens/__snapshots__/Disclaimer.test.tsx.snap (100%) rename {src => frontend/src}/screens/__snapshots__/Home.test.tsx.snap (100%) rename {src => frontend/src}/screens/__snapshots__/Privacy.test.tsx.snap (100%) rename {src => frontend/src}/screens/__snapshots__/Terms.test.tsx.snap (100%) rename {src => frontend/src}/screens/index.ts (100%) rename {src => frontend/src}/setupTests.ts (100%) rename {src => frontend/src}/styles/index.ts (100%) rename {src => frontend/src}/utils/index.ts (100%) rename {src => frontend/src}/utils/test.tsx (100%) rename tsconfig.json => frontend/tsconfig.json (100%) delete mode 100644 src/apis/session.ts delete mode 100644 src/models/Session.ts create mode 100644 terraform/.gitignore create mode 100644 terraform/backends.tf create mode 100644 terraform/data.tf create mode 100644 terraform/databases.tf create mode 100644 terraform/functions.tf create mode 100644 terraform/locals.tf create mode 100644 terraform/main.tf create mode 100644 terraform/outputs.tf create mode 100644 terraform/providers.tf create mode 100644 terraform/provisioners.tf create mode 100644 terraform/storages.tf create mode 100644 terraform/variables.tf diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 7137826..b0b0402 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -4,7 +4,7 @@ on: types: - created jobs: - build: + build-frontend: runs-on: ubuntu-latest steps: - name: Checkout repository @@ -14,47 +14,119 @@ jobs: with: node-version: 18 - name: Cache dependencies - id: cache-dependencies + id: cache-dependencies-frontend uses: actions/cache@v3 with: - key: ${{ runner.os }}-${{ hashFiles('**/package.json') }} + key: ${{ runner.os }}-${{ hashFiles('frontend/package.json') }} path: | - node_modules + frontend/node_modules ~/.npm - name: Install dependencies - if: ${{ steps.cache-dependencies.outputs.cache-hit == false }} + if: ${{ steps.cache-dependencies-frontend.outputs.cache-hit == false }} run: npm i --legacy-peer-deps + working-directory: frontend - name: Build the app run: npm run build + working-directory: frontend env: REACT_APP_MIXPANEL_TOKEN: ${{ secrets.REACT_APP_MIXPANEL_TOKEN }} - REACT_APP_BUGSNAG_KEY: ${{ secrets.REACT_APP_BUGSNAG_KEY }} REACT_APP_SENTRY_DSN: ${{ secrets.REACT_APP_SENTRY_DSN }} - name: Save the build uses: actions/upload-artifact@v3 with: - name: build - path: build/ - deploy: + name: frontend-build + path: frontend/build/ + build-backend: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Cache dependencies + id: cache-dependencies-backend + uses: actions/cache@v3 + with: + key: ${{ runner.os }}-${{ hashFiles('backend/package.json') }} + path: | + backend/node_modules + ~/.npm + - name: Install dependencies + if: ${{ steps.cache-dependencies-backend.outputs.cache-hit == false }} + run: npm i --legacy-peer-deps + working-directory: backend + - name: Build the app + run: npm run build + working-directory: backend + - name: Save the build + uses: actions/upload-artifact@v3 + with: + name: backend-build + path: backend/build/ + deploy-frontend: runs-on: ubuntu-latest needs: - - build + - build-frontend steps: - name: Restore the build uses: actions/download-artifact@v3 with: - name: build - path: build/ + name: frontend-build + path: frontend/build/ - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./build + publish_dir: ./frontend/build cname: brewmymac.sh + deploy-backend: + runs-on: ubuntu-latest + needs: + - build-backend + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }} + - name: Terraform Init + run: terraform init -lock-timeout=900s + working-directory: terraform + env: + TF_IN_AUTOMATION: true + TF_WORKSPACE: main + TF_VAR_app_id: ${{ secrets.TF_VAR_APP_ID }} + TF_VAR_client_secret: ${{ secrets.ARM_CLIENT_SECRET }} + TF_VAR_tenant_id: ${{ secrets.ARM_TENANT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }} + ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }} + ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }} + ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }} + - name: Restore the build + uses: actions/download-artifact@v3 + with: + name: backend-build + path: backend/build/ + - name: Terraform Apply + run: terraform apply -auto-approve -input=false -lock-timeout=900s + working-directory: terraform + env: + TF_IN_AUTOMATION: true + TF_WORKSPACE: main + TF_VAR_app_id: ${{ secrets.TF_VAR_APP_ID }} + TF_VAR_client_secret: ${{ secrets.ARM_CLIENT_SECRET }} + TF_VAR_tenant_id: ${{ secrets.ARM_TENANT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }} + ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }} + ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }} + ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }} check: runs-on: ubuntu-latest needs: - - deploy + - deploy-frontend steps: - name: Wait for 1 minute run: sleep 60 @@ -62,12 +134,12 @@ jobs: - name: Check the deployment uses: lakuapik/gh-actions-http-status@v1 with: - url: https://brewmymac.sh - expected_status: '[200]' + sites: '["https://brewmymac.sh"]' + expected: '[200]' sentry: runs-on: ubuntu-latest needs: - - build + - build-frontend steps: - name: Checkout repository uses: actions/checkout@v3 @@ -76,32 +148,35 @@ jobs: with: node-version: 18 - name: Cache dependencies - id: cache-dependencies + id: cache-dependencies-frontend uses: actions/cache@v3 with: - key: ${{ runner.os }}-${{ hashFiles('**/package.json') }} + key: ${{ runner.os }}-${{ hashFiles('frontend/package.json') }} path: | - node_modules + frontend/node_modules ~/.npm - name: Install dependencies - if: ${{ steps.cache-dependencies.outputs.cache-hit == false }} + if: ${{ steps.cache-dependencies-frontend.outputs.cache-hit == false }} run: npm i --legacy-peer-deps + working-directory: frontend - name: Restore the build uses: actions/download-artifact@v3 with: - name: build - path: build/ - - name: Retrieve release version information - uses: martinbeentjes/release-version-action@main - id: release-version + name: frontend-build + path: frontend/build/ + - name: Retrieve package version information + id: package-version + uses: martinbeentjes/npm-get-version-action@main + with: + path: frontend - name: Create a new Sentry release - run: ./node_modules/.bin/sentry-cli releases new ${{ steps.release-version.outputs.current-version }} + run: ./frontend/node_modules/.bin/sentry-cli releases new ${{ steps.package-version.outputs.current-version }} env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} - name: Upload source maps to Sentry - run: ./node_modules/.bin/sentry-cli releases files ${{ steps.release-version.outputs.current-version }} upload-sourcemaps build --rewrite + run: ./frontend/node_modules/.bin/sentry-cli releases files ${{ steps.package-version.outputs.current-version }} upload-sourcemaps frontend/build --rewrite env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3c913c..8922578 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,57 @@ name: CI on: - push jobs: + check: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }} + - name: Terraform Format + run: terraform fmt -check + working-directory: terraform + - name: Terraform Init + run: terraform init -lock-timeout=900s + working-directory: terraform + env: + TF_IN_AUTOMATION: true + TF_WORKSPACE: main + TF_VAR_app_id: ${{ secrets.TF_VAR_APP_ID }} + TF_VAR_client_secret: ${{ secrets.ARM_CLIENT_SECRET }} + TF_VAR_tenant_id: ${{ secrets.ARM_TENANT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }} + ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }} + ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }} + ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }} + - name: Terraform Validate + run: terraform validate -no-color + working-directory: terraform + env: + TF_IN_AUTOMATION: true + TF_WORKSPACE: main + TF_VAR_app_id: ${{ secrets.TF_VAR_APP_ID }} + TF_VAR_client_secret: ${{ secrets.ARM_CLIENT_SECRET }} + TF_VAR_tenant_id: ${{ secrets.ARM_TENANT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }} + ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }} + ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }} + ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }} + - name: Terraform Plan + run: terraform plan -no-color -input=false -lock-timeout=900s + working-directory: terraform + env: + TF_IN_AUTOMATION: true + TF_WORKSPACE: main + TF_VAR_app_id: ${{ secrets.TF_VAR_APP_ID }} + TF_VAR_client_secret: ${{ secrets.ARM_CLIENT_SECRET }} + TF_VAR_tenant_id: ${{ secrets.ARM_TENANT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }} + ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }} + ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }} + ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }} test: runs-on: ubuntu-latest steps: @@ -12,23 +63,25 @@ jobs: with: node-version: 18 - name: Cache dependencies - id: cache-dependencies + id: cache-dependencies-frontend uses: actions/cache@v3 with: - key: ${{ runner.os }}-${{ hashFiles('**/package.json') }} + key: ${{ runner.os }}-${{ hashFiles('frontend/package.json') }} path: | - node_modules + frontend/node_modules ~/.npm - name: Install dependencies - if: ${{ steps.cache-dependencies.outputs.cache-hit == false }} + if: ${{ steps.cache-dependencies-frontend.outputs.cache-hit == false }} run: npm i --legacy-peer-deps + working-directory: frontend - name: Run tests run: npm test + working-directory: frontend - name: Save coverage report uses: actions/upload-artifact@v3 with: - name: coverage - path: coverage/ + name: frontend-coverage + path: frontend/coverage/ scan: runs-on: ubuntu-latest needs: @@ -39,14 +92,14 @@ jobs: - name: Restore coverage report uses: actions/download-artifact@v3 with: - name: coverage - path: coverage/ + name: frontend-coverage + path: frontend/coverage/ - name: Run SonarCloud scan uses: SonarSource/sonarcloud-github-action@master env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - build: + build-frontend: runs-on: ubuntu-latest steps: - name: Checkout repository @@ -56,19 +109,44 @@ jobs: with: node-version: 18 - name: Cache dependencies - id: cache-dependencies + id: cache-dependencies-frontend uses: actions/cache@v3 with: - key: ${{ runner.os }}-${{ hashFiles('**/package.json') }} + key: ${{ runner.os }}-${{ hashFiles('frontend/package.json') }} path: | - node_modules + frontend/node_modules ~/.npm - name: Install dependencies - if: ${{ steps.cache-dependencies.outputs.cache-hit == false }} + if: ${{ steps.cache-dependencies-frontend.outputs.cache-hit == false }} run: npm i --legacy-peer-deps + working-directory: frontend - name: Build the app run: npm run build + working-directory: frontend env: REACT_APP_MIXPANEL_TOKEN: ${{ secrets.REACT_APP_MIXPANEL_TOKEN }} - REACT_APP_BUGSNAG_KEY: ${{ secrets.REACT_APP_BUGSNAG_KEY }} REACT_APP_SENTRY_DSN: ${{ secrets.REACT_APP_SENTRY_DSN }} + build-backend: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Cache dependencies + id: cache-dependencies-backend + uses: actions/cache@v3 + with: + key: ${{ runner.os }}-${{ hashFiles('backend/package.json') }} + path: | + backend/node_modules + ~/.npm + - name: Install dependencies + if: ${{ steps.cache-dependencies-backend.outputs.cache-hit == false }} + run: npm i --legacy-peer-deps + working-directory: backend + - name: Build the app + run: npm run build + working-directory: backend diff --git a/.gitignore b/.gitignore index fcca382..e446f02 100644 --- a/.gitignore +++ b/.gitignore @@ -4,17 +4,3 @@ # JetBrains .idea/ *.iml - -# Dependencies -node_modules/ - -# Testing -coverage/ - -# Secrets -.env - -# Generated -build/ -dist/ -*.log diff --git a/README.md b/README.md index a70384f..1ac650a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![GitHub workflow status](https://img.shields.io/github/workflow/status/ayltai/BrewMyMac/CI?style=flat)](https://github.com/ayltai/BrewMyMac/actions) [![Coverage](https://img.shields.io/sonar/coverage/ayltai_BrewMyMac?server=https%3A%2F%2Fsonarcloud.io)](https://sonarcloud.io/dashboard?id=ayltai_BrewMyMac) [![Quality gate](https://img.shields.io/sonar/quality_gate/ayltai_BrewMyMac?server=https%3A%2F%2Fsonarcloud.io)](https://sonarcloud.io/dashboard?id=ayltai_BrewMyMac) -![Vulnerabilities](https://img.shields.io/snyk/vulnerabilities/github/ayltai/BrewMyMac?style=flat) +[![Vulnerabilities](https://snyk.io/test/github/ayltai/BrewMyMac/badge.svg?targetFile=frontend/package.json)](https://snyk.io/test/github/ayltai/BrewMyMac?targetFile=frontend/package.json) Possibly the coolest way to install apps and customize your Mac! @@ -25,7 +25,11 @@ Try it: [https://brewmymac.sh](https://brewmymac.sh) * [macOS tweaks](https://github.com/ayltai/ansible-macOS-tweaks): A collection of nearly 50 macOS customizations -## Running locally +## Frontend + +The frontend is a React single-page application created using [Create React App](https://create-react-app.dev). The code is located under `frontend` directory. + +### Running locally You need [Node.js](https://nodejs.org) and [npm](https://www.npmjs.com) installed on your machine. @@ -41,7 +45,7 @@ You need [Node.js](https://nodejs.org) and [npm](https://www.npmjs.com) installe ``` 5. Open [http://localhost:3000](http://localhost:3000) with your browser -## Building from source +### Building from source You need [Node.js](https://nodejs.org) and [npm](https://www.npmjs.com) installed on your machine. @@ -55,20 +59,28 @@ You need [Node.js](https://nodejs.org) and [npm](https://www.npmjs.com) installe ``` 3. The built app will be in the `build` folder -## Deploying your own instance +## Backend -Apart from changing the values in steps 2 and 3 in the [Running locally](#running-locally) section, you will need to create your own API for saving and retrieving sessions. +The backend is a Node.js application targeted to run as [Azure Functions](https://azure.microsoft.com/en-us/services/functions/). The code is located under `backend` directory. -A session is a generated Shell script that contains the list of apps to be installed and tweaks to be applied and it is uniquely identified by its ID. The minimal data structure of a session is as follows: +### Deploying your own instance -```json -{ - "sessionId": "string", - "script": "string" -} -``` +[Terraform](https://www.terraform.io) is used to deploy the backend to [Azure](https://azure.microsoft.com). The code is located under `terraform` directory. -This project uses [Xano](https://www.xano.com) as the backend service which provides persistent data storage and RESTful APIs. You can use any other service that provides similar functionality. If you do that, you need to change the URLs in `src/api/session.ts` and `src/screens/SessionDetail.tsx`. +1. Install [Terraform](https://www.terraform.io/downloads.html) +2. Install [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) +3. Build the backend + ```bash + cd backend + npm run build + cd ../terraform + ``` +4. Authenticate with Azure: There are different ways to authenticate with Azure. See [here](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/service_principal_client_secret) for more details. Make sure the role running Terraform has the `Contributor` role on the subscription. +5. Run `terraform init` to initialize the Terraform working directory. You will probably need to change the `backend` configuration in `backends.tf` to use a different way to manage your Terraform state. +6. Run `terraform plan` to see what changes will be made to your infrastructure. You will probably need to change the `variables.tf` file to use a different resource group name, location, etc. +7. Run `terraform apply` to apply the changes. You will probably need to change the `variables.tf` file as explained above. +8. The backend will be deployed to Azure. You can now run the frontend locally or deploy it to Azure or GitHub Pages as well. +9. You can run `terraform destroy` to destroy the backend when you no longer need it. ## Architecture diff --git a/backend/.funcignore b/backend/.funcignore new file mode 100644 index 0000000..d077c9b --- /dev/null +++ b/backend/.funcignore @@ -0,0 +1,6 @@ +*.js.map +*.ts +*.test.ts +.git* +local.settings.json +tsconfig.json diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..b3ce943 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,17 @@ +# Dependencies +node_modules/ +package-lock.json + +# Testing +coverage/ + +# Secrets +.env + +# Generated +build/ +*.log + +# Azure Functions +appsettings.json +local.settings.json diff --git a/backend/host.json b/backend/host.json new file mode 100644 index 0000000..6fec838 --- /dev/null +++ b/backend/host.json @@ -0,0 +1,15 @@ +{ + "version" : "2.0", + "logging" : { + "applicationInsights" : { + "samplingSettings" : { + "isEnabled" : true, + "excludedTypes" : "Request" + } + } + }, + "extensionBundle" : { + "id" : "Microsoft.Azure.Functions.ExtensionBundle", + "version" : "[3.*, 4.0.0)" + } +} diff --git a/backend/libs/database.ts b/backend/libs/database.ts new file mode 100644 index 0000000..eef8fbc --- /dev/null +++ b/backend/libs/database.ts @@ -0,0 +1,42 @@ +import { connect, model, Schema, } from 'mongoose'; +import { v4, } from 'uuid'; + +const SessionSchema = new Schema({ + sessionId : { + type : String, + default : v4, + index : true, + }, + creationDate : { + type : Date, + default : Date.now, + index : true, + }, + script : String, +}); + +const SessionModel = model('Session', SessionSchema, 'sessions'); + +let db : typeof import('mongoose') | null = null; + +export const init = async () => { + if (!db) db = await connect(process.env.CONNECTION_STRING!); +}; + +export const createSession = async (script : string) => { + const session = await SessionModel.create({ + script, + }); + + return session.sessionId; +}; + +export const deleteSessions = async (days : number) => await SessionModel.find({}).where('creationDate').lt(Date.now() - days * 24 * 60 * 60 * 1000).deleteMany().exec(); + +export const getSessionScript = async (sessionId : string) => { + const session = await SessionModel.findOne({ + sessionId, + }).lean(); + + return session?.script; +}; diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..a5f08a9 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,23 @@ +{ + "name" : "brewmymac-backend", + "description" : "BrewMyMac backend", + "version" : "1.0.0", + "scripts" : { + "build" : "tsc", + "postbuild" : "cp package.json build && cd build && npm i --legacy-peer-deps --production", + "watch" : "tsc -w", + "prestart" : "npm run build", + "start" : "func start", + "update" : "npx npm-check-updates" + }, + "dependencies" : { + "mongoose" : "^6.6.1", + "uuid" : "^9.0.0" + }, + "devDependencies" : { + "@azure/functions" : "^3.2.0", + "@types/node" : "^18.7.18", + "@types/uuid" : "^8.3.4", + "typescript" : "^4.8.3" + } +} diff --git a/backend/sessions/function.json b/backend/sessions/function.json new file mode 100644 index 0000000..0a7046a --- /dev/null +++ b/backend/sessions/function.json @@ -0,0 +1,21 @@ +{ + "bindings" : [ + { + "authLevel" : "anonymous", + "type" : "httpTrigger", + "direction" : "in", + "name" : "req", + "methods" : [ + "get", + "post", + "delete" + ] + }, + { + "type" : "http", + "direction" : "out", + "name" : "res" + } + ], + "scriptFile" : "../build/sessions/index.js" +} diff --git a/backend/sessions/index.ts b/backend/sessions/index.ts new file mode 100644 index 0000000..c770d2f --- /dev/null +++ b/backend/sessions/index.ts @@ -0,0 +1,73 @@ +import { AzureFunction, Context, HttpRequest, } from '@azure/functions'; + +import { init, createSession, getSessionScript, deleteSessions, } from '../libs/database'; + +const httpTrigger : AzureFunction = async (context : Context, req : HttpRequest) : Promise => { + try { + switch (req.method) { + case 'GET': + if (req.query.sessionId) { + await init(); + + const script = await getSessionScript(req.query.sessionId); + + context.res = script ? { + status : 200, + body : script, + } : { + status : 404, + }; + } else { + context.res = { + status : 400, + }; + } + + break; + + case 'POST': + if (req.body) { + await init(); + + context.res = { + status : 200, + body : await createSession(req.body), + }; + } else { + context.res = { + status : 400, + }; + } + + break; + + case 'DELETE': + if (req.query.days) { + await init(); + + await deleteSessions(Number(req.query.days)); + + context.res = { + status : 200, + }; + } else { + context.res = { + status : 400, + }; + } + + break; + + default: + throw new Error(`The HTTP ${req.method} method is not supported.`); + } + } catch (error) { + context.res = { + status : 500, + body : error ? JSON.stringify(error) : null, + }; + } + +}; + +export default httpTrigger; diff --git a/backend/sessions/sample.dat b/backend/sessions/sample.dat new file mode 100644 index 0000000..fe728d4 --- /dev/null +++ b/backend/sessions/sample.dat @@ -0,0 +1,3 @@ +{ + "name" : "Azure" +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..f4bd135 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions" : { + "module" : "commonjs", + "target" : "es6", + "outDir" : "build", + "rootDir" : ".", + "sourceMap" : true, + "strict" : true + } +} diff --git a/.eslintrc.js b/frontend/.eslintrc.js similarity index 100% rename from .eslintrc.js rename to frontend/.eslintrc.js diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..8045eb8 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,13 @@ +# Dependencies +node_modules/ +package-lock.json + +# Testing +coverage/ + +# Secrets +.env + +# Generated +build/ +*.log diff --git a/CNAME b/frontend/CNAME similarity index 100% rename from CNAME rename to frontend/CNAME diff --git a/babel.config.js b/frontend/babel.config.js similarity index 100% rename from babel.config.js rename to frontend/babel.config.js diff --git a/config-overrides.js b/frontend/config-overrides.js similarity index 100% rename from config-overrides.js rename to frontend/config-overrides.js diff --git a/package.json b/frontend/package.json similarity index 94% rename from package.json rename to frontend/package.json index 21186c1..41b428f 100644 --- a/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name" : "brew-my-mac", + "name" : "brewmymac-frontend", "description" : "Possibly the coolest way to install apps and customize your Mac!", "version" : "1.1.0", "author" : { @@ -36,13 +36,12 @@ "@emotion/styled" : "^11.10.4", "@fontsource/inconsolata" : "^4.5.7", "@fontsource/roboto" : "^4.5.8", - "@mui/icons-material" : "^5.10.3", - "@mui/material" : "^5.10.5", + "@mui/icons-material" : "^5.10.6", + "@mui/material" : "^5.10.6", "@reduxjs/toolkit" : "^1.8.5", "@sentry/react" : "^7.13.0", "@sentry/tracing" : "^7.13.0", "ejs" : "^3.1.8", - "flexsearch" : "^0.7.21", "i18next" : "^21.9.2", "markdown-to-jsx" : "^7.1.7", "minisearch" : "^5.0.0", @@ -53,8 +52,7 @@ "react-i18next" : "^11.18.6", "react-redux" : "^8.0.2", "redux-persist" : "^6.0.0", - "usehooks-ts" : "^2.6.0", - "uuid" : "^9.0.0" + "usehooks-ts" : "^2.7.0" }, "devDependencies" : { "@babel/core" : "^7.19.1", diff --git a/public/data/disclaimer.md b/frontend/public/data/disclaimer.md similarity index 100% rename from public/data/disclaimer.md rename to frontend/public/data/disclaimer.md diff --git a/public/data/install.sh.ejs b/frontend/public/data/install.sh.ejs similarity index 87% rename from public/data/install.sh.ejs rename to frontend/public/data/install.sh.ejs index b776b66..8e968e1 100644 --- a/public/data/install.sh.ejs +++ b/frontend/public/data/install.sh.ejs @@ -2,6 +2,16 @@ set -u printf '\e[8;50;120t' +echo '⏳ Checking if Homebrew is installed ...' +which -s brew +if [[ $? != 0 ]] ; then + echo 'Installing Homebrew ...' + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" && eval "$(/opt/homebrew/bin/brew shellenv)" + echo '✅ Successfully installed Homebrew' +else + echo '✅ Homebrew is already installed.' +fi + echo '⏳ Installing prerequisites ...' brew install figlet echo '✅ Prerequisites are installed' diff --git a/public/data/privacy.md b/frontend/public/data/privacy.md similarity index 100% rename from public/data/privacy.md rename to frontend/public/data/privacy.md diff --git a/public/data/terms.md b/frontend/public/data/terms.md similarity index 100% rename from public/data/terms.md rename to frontend/public/data/terms.md diff --git a/public/favicon-16x16.png b/frontend/public/favicon-16x16.png similarity index 100% rename from public/favicon-16x16.png rename to frontend/public/favicon-16x16.png diff --git a/public/favicon-32x32.png b/frontend/public/favicon-32x32.png similarity index 100% rename from public/favicon-32x32.png rename to frontend/public/favicon-32x32.png diff --git a/public/favicon.ico b/frontend/public/favicon.ico similarity index 100% rename from public/favicon.ico rename to frontend/public/favicon.ico diff --git a/public/index.html b/frontend/public/index.html similarity index 100% rename from public/index.html rename to frontend/public/index.html diff --git a/public/safari-pinned-tab.svg b/frontend/public/safari-pinned-tab.svg similarity index 100% rename from public/safari-pinned-tab.svg rename to frontend/public/safari-pinned-tab.svg diff --git a/src/App.tsx b/frontend/src/App.tsx similarity index 100% rename from src/App.tsx rename to frontend/src/App.tsx diff --git a/src/LogoLarge.png b/frontend/src/LogoLarge.png similarity index 100% rename from src/LogoLarge.png rename to frontend/src/LogoLarge.png diff --git a/src/LogoSmall.png b/frontend/src/LogoSmall.png similarity index 100% rename from src/LogoSmall.png rename to frontend/src/LogoSmall.png diff --git a/src/apis/appStore.ts b/frontend/src/apis/appStore.ts similarity index 100% rename from src/apis/appStore.ts rename to frontend/src/apis/appStore.ts diff --git a/src/apis/homebrew.ts b/frontend/src/apis/homebrew.ts similarity index 100% rename from src/apis/homebrew.ts rename to frontend/src/apis/homebrew.ts diff --git a/src/apis/index.ts b/frontend/src/apis/index.ts similarity index 77% rename from src/apis/index.ts rename to frontend/src/apis/index.ts index 7a18b8f..0ae2f98 100644 --- a/src/apis/index.ts +++ b/frontend/src/apis/index.ts @@ -1,4 +1,3 @@ export { appStoreApi, useSearchQuery, } from './appStore'; export { homebrewApi, useCaskQuery, useFormulaQuery, } from './homebrew'; -export { sessionApi, useGetSessionQuery, } from './session'; export { tweakApi, useGetPlaybookQuery, useGetTweaksQuery, } from './tweak'; diff --git a/src/apis/tweak.ts b/frontend/src/apis/tweak.ts similarity index 100% rename from src/apis/tweak.ts rename to frontend/src/apis/tweak.ts diff --git a/src/components/ActionBar.test.tsx b/frontend/src/components/ActionBar.test.tsx similarity index 100% rename from src/components/ActionBar.test.tsx rename to frontend/src/components/ActionBar.test.tsx diff --git a/src/components/ActionBar.tsx b/frontend/src/components/ActionBar.tsx similarity index 100% rename from src/components/ActionBar.tsx rename to frontend/src/components/ActionBar.tsx diff --git a/src/components/CopyableTextField.test.tsx b/frontend/src/components/CopyableTextField.test.tsx similarity index 100% rename from src/components/CopyableTextField.test.tsx rename to frontend/src/components/CopyableTextField.test.tsx diff --git a/src/components/CopyableTextField.tsx b/frontend/src/components/CopyableTextField.tsx similarity index 100% rename from src/components/CopyableTextField.tsx rename to frontend/src/components/CopyableTextField.tsx diff --git a/src/components/ExternalLink.tsx b/frontend/src/components/ExternalLink.tsx similarity index 100% rename from src/components/ExternalLink.tsx rename to frontend/src/components/ExternalLink.tsx diff --git a/src/components/FilteredItemGrid.test.tsx b/frontend/src/components/FilteredItemGrid.test.tsx similarity index 100% rename from src/components/FilteredItemGrid.test.tsx rename to frontend/src/components/FilteredItemGrid.test.tsx diff --git a/src/components/FilteredItemGrid.tsx b/frontend/src/components/FilteredItemGrid.tsx similarity index 100% rename from src/components/FilteredItemGrid.tsx rename to frontend/src/components/FilteredItemGrid.tsx diff --git a/src/components/Footer.tsx b/frontend/src/components/Footer.tsx similarity index 100% rename from src/components/Footer.tsx rename to frontend/src/components/Footer.tsx diff --git a/src/components/ItemGrid.test.tsx b/frontend/src/components/ItemGrid.test.tsx similarity index 100% rename from src/components/ItemGrid.test.tsx rename to frontend/src/components/ItemGrid.test.tsx diff --git a/src/components/ItemGrid.tsx b/frontend/src/components/ItemGrid.tsx similarity index 100% rename from src/components/ItemGrid.tsx rename to frontend/src/components/ItemGrid.tsx diff --git a/src/components/ItemList.test.tsx b/frontend/src/components/ItemList.test.tsx similarity index 100% rename from src/components/ItemList.test.tsx rename to frontend/src/components/ItemList.test.tsx diff --git a/src/components/ItemList.tsx b/frontend/src/components/ItemList.tsx similarity index 100% rename from src/components/ItemList.tsx rename to frontend/src/components/ItemList.tsx diff --git a/src/components/Loading.test.tsx b/frontend/src/components/Loading.test.tsx similarity index 100% rename from src/components/Loading.test.tsx rename to frontend/src/components/Loading.test.tsx diff --git a/src/components/Loading.tsx b/frontend/src/components/Loading.tsx similarity index 100% rename from src/components/Loading.tsx rename to frontend/src/components/Loading.tsx diff --git a/src/components/MarkdownDialog.tsx b/frontend/src/components/MarkdownDialog.tsx similarity index 100% rename from src/components/MarkdownDialog.tsx rename to frontend/src/components/MarkdownDialog.tsx diff --git a/src/components/PopUpDialog.test.tsx b/frontend/src/components/PopUpDialog.test.tsx similarity index 100% rename from src/components/PopUpDialog.test.tsx rename to frontend/src/components/PopUpDialog.test.tsx diff --git a/src/components/PopUpDialog.tsx b/frontend/src/components/PopUpDialog.tsx similarity index 100% rename from src/components/PopUpDialog.tsx rename to frontend/src/components/PopUpDialog.tsx diff --git a/src/components/PopUpFooter.tsx b/frontend/src/components/PopUpFooter.tsx similarity index 100% rename from src/components/PopUpFooter.tsx rename to frontend/src/components/PopUpFooter.tsx diff --git a/src/components/SearchBox.test.tsx b/frontend/src/components/SearchBox.test.tsx similarity index 100% rename from src/components/SearchBox.test.tsx rename to frontend/src/components/SearchBox.test.tsx diff --git a/src/components/SearchBox.tsx b/frontend/src/components/SearchBox.tsx similarity index 100% rename from src/components/SearchBox.tsx rename to frontend/src/components/SearchBox.tsx diff --git a/src/components/SelectableItem.test.tsx b/frontend/src/components/SelectableItem.test.tsx similarity index 100% rename from src/components/SelectableItem.test.tsx rename to frontend/src/components/SelectableItem.test.tsx diff --git a/src/components/SelectableItem.tsx b/frontend/src/components/SelectableItem.tsx similarity index 100% rename from src/components/SelectableItem.tsx rename to frontend/src/components/SelectableItem.tsx diff --git a/src/components/__snapshots__/ActionBar.test.tsx.snap b/frontend/src/components/__snapshots__/ActionBar.test.tsx.snap similarity index 100% rename from src/components/__snapshots__/ActionBar.test.tsx.snap rename to frontend/src/components/__snapshots__/ActionBar.test.tsx.snap diff --git a/src/components/__snapshots__/ItemGrid.test.tsx.snap b/frontend/src/components/__snapshots__/ItemGrid.test.tsx.snap similarity index 100% rename from src/components/__snapshots__/ItemGrid.test.tsx.snap rename to frontend/src/components/__snapshots__/ItemGrid.test.tsx.snap diff --git a/src/components/__snapshots__/ItemList.test.tsx.snap b/frontend/src/components/__snapshots__/ItemList.test.tsx.snap similarity index 100% rename from src/components/__snapshots__/ItemList.test.tsx.snap rename to frontend/src/components/__snapshots__/ItemList.test.tsx.snap diff --git a/src/components/__snapshots__/Loading.test.tsx.snap b/frontend/src/components/__snapshots__/Loading.test.tsx.snap similarity index 100% rename from src/components/__snapshots__/Loading.test.tsx.snap rename to frontend/src/components/__snapshots__/Loading.test.tsx.snap diff --git a/src/components/__snapshots__/PopUpDialog.test.tsx.snap b/frontend/src/components/__snapshots__/PopUpDialog.test.tsx.snap similarity index 100% rename from src/components/__snapshots__/PopUpDialog.test.tsx.snap rename to frontend/src/components/__snapshots__/PopUpDialog.test.tsx.snap diff --git a/src/components/index.ts b/frontend/src/components/index.ts similarity index 100% rename from src/components/index.ts rename to frontend/src/components/index.ts diff --git a/src/constants.ts b/frontend/src/constants.ts similarity index 67% rename from src/constants.ts rename to frontend/src/constants.ts index c524b5b..94bd18e 100644 --- a/src/constants.ts +++ b/frontend/src/constants.ts @@ -1,4 +1,4 @@ export const HOMEBREW_REFRESH_INTERVAL = 1000 * 60 * 60; export const TWEAKS_REFRESH_INTERVAL = 1000 * 60 * 60 * 24; -export const INITIAL_FILTER = 'firefox'; export const MIN_SEARCH_LENGTH = 3; +export const BREWMYMAC_API_ENDPOINT = 'https://brewmymac.azurewebsites.net'; diff --git a/src/hooks/index.ts b/frontend/src/hooks/index.ts similarity index 100% rename from src/hooks/index.ts rename to frontend/src/hooks/index.ts diff --git a/src/hooks/redux.ts b/frontend/src/hooks/redux.ts similarity index 100% rename from src/hooks/redux.ts rename to frontend/src/hooks/redux.ts diff --git a/src/i18n/en.json b/frontend/src/i18n/en.json similarity index 100% rename from src/i18n/en.json rename to frontend/src/i18n/en.json diff --git a/src/i18n/index.ts b/frontend/src/i18n/index.ts similarity index 100% rename from src/i18n/index.ts rename to frontend/src/i18n/index.ts diff --git a/src/index.css b/frontend/src/index.css similarity index 100% rename from src/index.css rename to frontend/src/index.css diff --git a/src/index.tsx b/frontend/src/index.tsx similarity index 100% rename from src/index.tsx rename to frontend/src/index.tsx diff --git a/src/models/AppStoreSearchResults.ts b/frontend/src/models/AppStoreSearchResults.ts similarity index 100% rename from src/models/AppStoreSearchResults.ts rename to frontend/src/models/AppStoreSearchResults.ts diff --git a/src/models/HomebrewFormulae.ts b/frontend/src/models/HomebrewFormulae.ts similarity index 100% rename from src/models/HomebrewFormulae.ts rename to frontend/src/models/HomebrewFormulae.ts diff --git a/src/models/Item.ts b/frontend/src/models/Item.ts similarity index 100% rename from src/models/Item.ts rename to frontend/src/models/Item.ts diff --git a/src/models/Tweak.ts b/frontend/src/models/Tweak.ts similarity index 100% rename from src/models/Tweak.ts rename to frontend/src/models/Tweak.ts diff --git a/src/models/index.ts b/frontend/src/models/index.ts similarity index 84% rename from src/models/index.ts rename to frontend/src/models/index.ts index 18c2e7f..30aea3d 100644 --- a/src/models/index.ts +++ b/frontend/src/models/index.ts @@ -1,5 +1,4 @@ export type { AppStoreSearchResult, AppStoreSearchResults, } from './AppStoreSearchResults'; export type { HomebrewFormula, } from './HomebrewFormulae'; export type { Item, } from './Item'; -export type { Session, } from './Session'; export type { Tweak, } from './Tweak'; diff --git a/src/redux/index.ts b/frontend/src/redux/index.ts similarity index 83% rename from src/redux/index.ts rename to frontend/src/redux/index.ts index a5f3abb..5aca014 100644 --- a/src/redux/index.ts +++ b/frontend/src/redux/index.ts @@ -2,7 +2,7 @@ import { combineReducers, configureStore, } from '@reduxjs/toolkit'; import { FLUSH, PAUSE, PERSIST, persistReducer, persistStore, PURGE, REGISTER, REHYDRATE, } from 'redux-persist'; import sessionStorage from 'redux-persist/lib/storage/session'; -import { appStoreApi, homebrewApi, sessionApi, tweakApi, } from '../apis'; +import { appStoreApi, homebrewApi, tweakApi, } from '../apis'; import { sessionReducer, } from './sessionSlice'; @@ -12,7 +12,6 @@ export const store = configureStore({ blacklist : [ appStoreApi.reducerPath, homebrewApi.reducerPath, - sessionApi.reducerPath, tweakApi.reducerPath, ], storage : sessionStorage, @@ -20,7 +19,6 @@ export const store = configureStore({ session : sessionReducer, [ appStoreApi.reducerPath ] : appStoreApi.reducer, [ homebrewApi.reducerPath ] : homebrewApi.reducer, - [ sessionApi.reducerPath ] : sessionApi.reducer, [ tweakApi.reducerPath ] : tweakApi.reducer, })), middleware : getDefaultMiddleware => getDefaultMiddleware({ @@ -34,7 +32,7 @@ export const store = configureStore({ REHYDRATE, ], }, - }).concat(appStoreApi.middleware, homebrewApi.middleware, sessionApi.middleware, tweakApi.middleware), + }).concat(appStoreApi.middleware, homebrewApi.middleware, tweakApi.middleware), }); export const persistor = persistStore(store); diff --git a/src/redux/sessionSlice.ts b/frontend/src/redux/sessionSlice.ts similarity index 100% rename from src/redux/sessionSlice.ts rename to frontend/src/redux/sessionSlice.ts diff --git a/src/screens/About.test.tsx b/frontend/src/screens/About.test.tsx similarity index 100% rename from src/screens/About.test.tsx rename to frontend/src/screens/About.test.tsx diff --git a/src/screens/About.tsx b/frontend/src/screens/About.tsx similarity index 100% rename from src/screens/About.tsx rename to frontend/src/screens/About.tsx diff --git a/src/screens/Disclaimer.test.tsx b/frontend/src/screens/Disclaimer.test.tsx similarity index 100% rename from src/screens/Disclaimer.test.tsx rename to frontend/src/screens/Disclaimer.test.tsx diff --git a/src/screens/Disclaimer.tsx b/frontend/src/screens/Disclaimer.tsx similarity index 100% rename from src/screens/Disclaimer.tsx rename to frontend/src/screens/Disclaimer.tsx diff --git a/src/screens/Home.test.tsx b/frontend/src/screens/Home.test.tsx similarity index 100% rename from src/screens/Home.test.tsx rename to frontend/src/screens/Home.test.tsx diff --git a/src/screens/Home.tsx b/frontend/src/screens/Home.tsx similarity index 86% rename from src/screens/Home.tsx rename to frontend/src/screens/Home.tsx index 1617bc2..4c3a09c 100644 --- a/src/screens/Home.tsx +++ b/frontend/src/screens/Home.tsx @@ -4,11 +4,10 @@ import { Badge, Box, Grid, IconButton, styled, ToggleButton, Tooltip, } from '@m import { render, } from 'ejs'; import React, { useEffect, useState, } from 'react'; import { useTranslation, } from 'react-i18next'; -import { v4, } from 'uuid'; import { ActionBar, FilteredItemGrid, Loading, PopUpFooter, SearchBox, } from '../components'; +import { BREWMYMAC_API_ENDPOINT, } from '../constants'; import { useAppSelector, } from '../hooks'; -import type { Session, } from '../models'; import { SessionDetail, } from './SessionDetail'; @@ -34,7 +33,7 @@ export const Home = () => { const [ filter, setFilter, ] = useState(''); const [ inProgress, setInProgress, ] = useState(false); const [ showSelected, setShowSelected, ] = useState(false); - const [ savedSession, setSavedSession, ] = useState(); + const [ savedSessionId, setSavedSessionId, ] = useState(); const [ showSessionDetail, setShowSessionDetail, ] = useState(false); const { t, } = useTranslation(); @@ -46,7 +45,7 @@ export const Home = () => { const handleToggleShowSelected = () => setShowSelected(!showSelected); const handleInstall = async () => { - setSavedSession(undefined); + setSavedSessionId(undefined); setShowSessionDetail(true); const file = await fetch('/data/install.sh.ejs'); @@ -56,23 +55,20 @@ export const Home = () => { items : session.items, }); - const response = await fetch('https://x8ki-letl-twmt.n7.xano.io/api:bt-93slL/sessions', { + const response = await fetch(`${BREWMYMAC_API_ENDPOINT}/api/sessions`, { method : 'POST', headers : { - 'Content-Type' : 'application/json', + 'Content-Type' : 'text/plain', }, - body : JSON.stringify({ - sessionId : v4(), - script, - }), + body : script, }); - setSavedSession(await response.json()); + setSavedSessionId(await response.text()); }; const handleCloseSession = () => { setShowSessionDetail(false); - setSavedSession(undefined); + setSavedSessionId(undefined); }; useEffect(() => { @@ -131,13 +127,13 @@ export const Home = () => { filter={filter} onStatusChange={handleStatusChange} /> - {showSessionDetail && savedSession && ( + {showSessionDetail && savedSessionId && ( )} - {showSessionDetail && !savedSession && } + {showSessionDetail && !savedSessionId && } ); diff --git a/src/screens/ItemDetail.test.tsx b/frontend/src/screens/ItemDetail.test.tsx similarity index 100% rename from src/screens/ItemDetail.test.tsx rename to frontend/src/screens/ItemDetail.test.tsx diff --git a/src/screens/ItemDetail.tsx b/frontend/src/screens/ItemDetail.tsx similarity index 100% rename from src/screens/ItemDetail.tsx rename to frontend/src/screens/ItemDetail.tsx diff --git a/src/screens/Privacy.test.tsx b/frontend/src/screens/Privacy.test.tsx similarity index 100% rename from src/screens/Privacy.test.tsx rename to frontend/src/screens/Privacy.test.tsx diff --git a/src/screens/Privacy.tsx b/frontend/src/screens/Privacy.tsx similarity index 100% rename from src/screens/Privacy.tsx rename to frontend/src/screens/Privacy.tsx diff --git a/src/screens/SessionDetail.test.tsx b/frontend/src/screens/SessionDetail.test.tsx similarity index 76% rename from src/screens/SessionDetail.test.tsx rename to frontend/src/screens/SessionDetail.test.tsx index 37eda6b..c19c6c5 100644 --- a/src/screens/SessionDetail.test.tsx +++ b/frontend/src/screens/SessionDetail.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { Item, Session, } from '../models'; +import { Item, } from '../models'; import { render, } from '../utils/test'; import { SessionDetail, } from './SessionDetail'; @@ -12,16 +12,11 @@ const ITEM : Item = { parameter : 'dummy_parameter', }; -const SESSION : Session = { - sessionId : 'dummy session id', - script : 'dummy script', -}; - describe('', () => { it('renders all child items', () => { const { getByText, } = render( diff --git a/src/screens/SessionDetail.tsx b/frontend/src/screens/SessionDetail.tsx similarity index 82% rename from src/screens/SessionDetail.tsx rename to frontend/src/screens/SessionDetail.tsx index 6648e98..f9af1d6 100644 --- a/src/screens/SessionDetail.tsx +++ b/frontend/src/screens/SessionDetail.tsx @@ -5,16 +5,17 @@ import React, { useEffect, } from 'react'; import { useTranslation, } from 'react-i18next'; import { CopyableTextField, ItemList, PopUpDialog, } from '../components'; -import type { Item, Session, } from '../models'; +import { BREWMYMAC_API_ENDPOINT, } from '../constants'; +import type { Item, } from '../models'; export const SessionDetail = ({ - session, + sessionId, items, onClose, } : { - session : Session, - items : Item[], - onClose? : () => void, + sessionId : string, + items : Item[], + onClose? : () => void, }) => { const { t, } = useTranslation(); @@ -68,7 +69,7 @@ export const SessionDetail = ({ + value={`/bin/bash -c "$(curl -fsSL ${BREWMYMAC_API_ENDPOINT}/api/sessions?sessionId=${sessionId}"`} /> ); }; diff --git a/src/screens/Terms.test.tsx b/frontend/src/screens/Terms.test.tsx similarity index 100% rename from src/screens/Terms.test.tsx rename to frontend/src/screens/Terms.test.tsx diff --git a/src/screens/Terms.tsx b/frontend/src/screens/Terms.tsx similarity index 100% rename from src/screens/Terms.tsx rename to frontend/src/screens/Terms.tsx diff --git a/src/screens/__snapshots__/About.test.tsx.snap b/frontend/src/screens/__snapshots__/About.test.tsx.snap similarity index 100% rename from src/screens/__snapshots__/About.test.tsx.snap rename to frontend/src/screens/__snapshots__/About.test.tsx.snap diff --git a/src/screens/__snapshots__/Disclaimer.test.tsx.snap b/frontend/src/screens/__snapshots__/Disclaimer.test.tsx.snap similarity index 100% rename from src/screens/__snapshots__/Disclaimer.test.tsx.snap rename to frontend/src/screens/__snapshots__/Disclaimer.test.tsx.snap diff --git a/src/screens/__snapshots__/Home.test.tsx.snap b/frontend/src/screens/__snapshots__/Home.test.tsx.snap similarity index 100% rename from src/screens/__snapshots__/Home.test.tsx.snap rename to frontend/src/screens/__snapshots__/Home.test.tsx.snap diff --git a/src/screens/__snapshots__/Privacy.test.tsx.snap b/frontend/src/screens/__snapshots__/Privacy.test.tsx.snap similarity index 100% rename from src/screens/__snapshots__/Privacy.test.tsx.snap rename to frontend/src/screens/__snapshots__/Privacy.test.tsx.snap diff --git a/src/screens/__snapshots__/Terms.test.tsx.snap b/frontend/src/screens/__snapshots__/Terms.test.tsx.snap similarity index 100% rename from src/screens/__snapshots__/Terms.test.tsx.snap rename to frontend/src/screens/__snapshots__/Terms.test.tsx.snap diff --git a/src/screens/index.ts b/frontend/src/screens/index.ts similarity index 100% rename from src/screens/index.ts rename to frontend/src/screens/index.ts diff --git a/src/setupTests.ts b/frontend/src/setupTests.ts similarity index 100% rename from src/setupTests.ts rename to frontend/src/setupTests.ts diff --git a/src/styles/index.ts b/frontend/src/styles/index.ts similarity index 100% rename from src/styles/index.ts rename to frontend/src/styles/index.ts diff --git a/src/utils/index.ts b/frontend/src/utils/index.ts similarity index 100% rename from src/utils/index.ts rename to frontend/src/utils/index.ts diff --git a/src/utils/test.tsx b/frontend/src/utils/test.tsx similarity index 100% rename from src/utils/test.tsx rename to frontend/src/utils/test.tsx diff --git a/tsconfig.json b/frontend/tsconfig.json similarity index 100% rename from tsconfig.json rename to frontend/tsconfig.json diff --git a/src/apis/session.ts b/src/apis/session.ts deleted file mode 100644 index aaa1797..0000000 --- a/src/apis/session.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createApi, fetchBaseQuery, } from '@reduxjs/toolkit/query/react'; - -import type { Session, } from '../models'; - -export const sessionApi = createApi({ - reducerPath : 'sessionApi', - baseQuery : fetchBaseQuery({ - baseUrl : 'https://x8ki-letl-twmt.n7.xano.io', - }), - endpoints : build => ({ - getSession : build.query({ - query : sessionId => `/api:bt-93slL/sessions/${sessionId}`, - }), - }), -}); - -export const { useGetSessionQuery, } = sessionApi; diff --git a/src/models/Session.ts b/src/models/Session.ts deleted file mode 100644 index 5e06d8f..0000000 --- a/src/models/Session.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type Session = { - id? : string, - sessionId : string, - creationDate? : Date, - script : string, -}; diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 0000000..cb10ec2 --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,6 @@ +# Terraform +.terraform/ +.terraform.lock.hcl + +# Generated +*.zip diff --git a/terraform/backends.tf b/terraform/backends.tf new file mode 100644 index 0000000..7b3f5d8 --- /dev/null +++ b/terraform/backends.tf @@ -0,0 +1,10 @@ +terraform { + backend "remote" { + hostname = "app.terraform.io" + organization = "brewmymac" + + workspaces { + prefix = "azure-" + } + } +} diff --git a/terraform/data.tf b/terraform/data.tf new file mode 100644 index 0000000..7933e10 --- /dev/null +++ b/terraform/data.tf @@ -0,0 +1,5 @@ +data "archive_file" "this" { + type = "zip" + source_dir = "${path.module}/../backend" + output_path = "${path.module}/${var.tag}.zip" +} diff --git a/terraform/databases.tf b/terraform/databases.tf new file mode 100644 index 0000000..6ca9b42 --- /dev/null +++ b/terraform/databases.tf @@ -0,0 +1,64 @@ +resource "azurerm_cosmosdb_account" "this" { + resource_group_name = azurerm_resource_group.this.name + location = azurerm_resource_group.this.location + name = var.cosmosdb_account_name + offer_type = "Standard" + kind = "MongoDB" + enable_free_tier = true + + capabilities { + name = "EnableMongo" + } + + consistency_policy { + consistency_level = "Session" + } + + geo_location { + failover_priority = 0 + location = azurerm_resource_group.this.location + } + + tags = { + Name = var.tag + } +} + +resource "azurerm_cosmosdb_mongo_database" "this" { + resource_group_name = azurerm_cosmosdb_account.this.resource_group_name + account_name = azurerm_cosmosdb_account.this.name + name = var.tag + + autoscale_settings { + max_throughput = var.cosmosdb_max_throughput + } +} + +resource "azurerm_cosmosdb_mongo_collection" "this" { + resource_group_name = azurerm_cosmosdb_mongo_database.this.resource_group_name + account_name = azurerm_cosmosdb_mongo_database.this.account_name + database_name = azurerm_cosmosdb_mongo_database.this.name + name = "sessions" + + index { + unique = true + + keys = [ + "_id", + ] + } + + index { + unique = true + + keys = [ + "sessionId", + ] + } + + index { + keys = [ + "creationDate", + ] + } +} diff --git a/terraform/functions.tf b/terraform/functions.tf new file mode 100644 index 0000000..cb1cf08 --- /dev/null +++ b/terraform/functions.tf @@ -0,0 +1,47 @@ +resource "azurerm_service_plan" "this" { + resource_group_name = azurerm_resource_group.this.name + location = azurerm_resource_group.this.location + name = var.tag + os_type = "Linux" + sku_name = "Y1" + + tags = { + Name = var.tag + } +} + +resource "azurerm_linux_function_app" "this" { + resource_group_name = azurerm_resource_group.this.name + location = azurerm_resource_group.this.location + name = var.tag + service_plan_id = azurerm_service_plan.this.id + storage_account_name = azurerm_storage_account.this.name + storage_account_access_key = azurerm_storage_account.this.primary_access_key + https_only = true + + site_config { + application_stack { + node_version = "18" + } + + cors { + allowed_origins = [ + "https://brewmymac.sh", + ] + } + + http2_enabled = true + } + + app_settings = { + AzureWebJobsDisableHomepage = "true" + FUNCTIONS_WORKER_RUNTIME = "node" + WEBSITE_NODE_DEFAULT_VERSION = "~18" + WEBSITE_RUN_FROM_PACKAGE = "https://${azurerm_storage_account.this.name}.blob.core.windows.net/${azurerm_storage_container.this.name}/${azurerm_storage_blob.this.name}${data.azurerm_storage_account_blob_container_sas.this.sas}" + CONNECTION_STRING = azurerm_cosmosdb_account.this.connection_strings[0] + } + + tags = { + Name = var.tag + } +} diff --git a/terraform/locals.tf b/terraform/locals.tf new file mode 100644 index 0000000..faf0db8 --- /dev/null +++ b/terraform/locals.tf @@ -0,0 +1,4 @@ +locals { + login_command = "az login --service-principal -u ${var.app_id} -p ${var.client_secret} --tenant ${var.tenant_id}" + deploy_command = "az functionapp deployment source config-zip -g ${azurerm_resource_group.this.name} -n ${azurerm_linux_function_app.this.name} --src ${data.archive_file.this.output_path}" +} diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..072c142 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,8 @@ +resource "azurerm_resource_group" "this" { + name = var.resource_group_name + location = var.resource_group_location + + tags = { + Name = var.tag + } +} diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000..500de3c --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,3 @@ +output "hostname" { + value = azurerm_linux_function_app.this.default_hostname +} diff --git a/terraform/providers.tf b/terraform/providers.tf new file mode 100644 index 0000000..6ac1ed8 --- /dev/null +++ b/terraform/providers.tf @@ -0,0 +1,24 @@ +terraform { + required_version = ">= 1.2.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 3.0.0" + } + + archive = { + source = "hashicorp/archive" + version = ">= 2.2.0" + } + + null = { + source = "hashicorp/null" + version = ">= 3.0.0" + } + } +} + +provider "azurerm" { + features {} +} diff --git a/terraform/provisioners.tf b/terraform/provisioners.tf new file mode 100644 index 0000000..9e66d17 --- /dev/null +++ b/terraform/provisioners.tf @@ -0,0 +1,11 @@ +resource "null_resource" "this" { + provisioner "local-exec" { + command = "${local.login_command} && ${local.deploy_command}" + } + + triggers = { + archive = filemd5(data.archive_file.this.output_path) + login_command = local.login_command + deploy_command = local.deploy_command + } +} diff --git a/terraform/storages.tf b/terraform/storages.tf new file mode 100644 index 0000000..f5f47b0 --- /dev/null +++ b/terraform/storages.tf @@ -0,0 +1,42 @@ +resource "azurerm_storage_account" "this" { + resource_group_name = azurerm_resource_group.this.name + location = azurerm_resource_group.this.location + name = var.tag + account_tier = "Standard" + account_replication_type = "LRS" + + tags = { + Name = var.tag + } +} + +resource "azurerm_storage_container" "this" { + storage_account_name = azurerm_storage_account.this.name + name = var.tag + container_access_type = "private" +} + +resource "azurerm_storage_blob" "this" { + storage_account_name = azurerm_storage_container.this.storage_account_name + storage_container_name = azurerm_storage_container.this.name + name = "${filesha256(data.archive_file.this.output_path)}.zip" + source = data.archive_file.this.output_path + type = "Block" +} + +data "azurerm_storage_account_blob_container_sas" "this" { + connection_string = azurerm_storage_account.this.primary_connection_string + container_name = azurerm_storage_blob.this.storage_container_name + https_only = true + start = "2022-01-01T00:00:00Z" + expiry = "2023-12-31T23:59:59Z" + + permissions { + read = true + write = false + add = false + create = false + delete = false + list = false + } +} diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..8e35bae --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,34 @@ +variable "resource_group_name" { + default = "BrewMyMac" +} + +variable "resource_group_location" { + default = "ukwest" +} + +variable "cosmosdb_account_name" { + default = "brewmymac" +} + +variable "cosmosdb_max_throughput" { + default = 1000 +} + +variable "client_secret" { + description = "The Client Secret for the Service Principal" + sensitive = true +} + +variable "app_id" { + description = "The Application ID of the Service Principal to use for this deployment." + sensitive = true +} + +variable "tenant_id" { + description = "The Tenant ID of the Subscription to use for this deployment." + sensitive = true +} + +variable "tag" { + default = "brewmymac" +}