diff --git a/.github/images/star-infisical.gif b/.github/images/star-infisical.gif index bb0752cb7..6d0789969 100644 Binary files a/.github/images/star-infisical.gif and b/.github/images/star-infisical.gif differ diff --git a/.github/workflows/check-be-pull-request.yml b/.github/workflows/check-be-pull-request.yml new file mode 100644 index 000000000..f17d8c5c8 --- /dev/null +++ b/.github/workflows/check-be-pull-request.yml @@ -0,0 +1,41 @@ +name: Check Backend Pull Request + +on: + pull_request: + types: [ opened, synchronize ] + paths: + - 'backend/**' + - '!backend/README.md' + - '!backend/.*' + - 'backend/.eslintrc.js' + + +jobs: + + check-be-pr: + name: Check + runs-on: ubuntu-latest + + steps: + - + name: โ˜๏ธ Checkout source + uses: actions/checkout@v3 + - + name: ๐Ÿ”ง Setup Node 16 + uses: actions/setup-node@v3 + with: + node-version: '16' + cache: 'npm' + cache-dependency-path: backend/package-lock.json + - + name: ๐Ÿ“ฆ Install dependencies + run: npm ci --only-production --ignore-scripts + working-directory: backend + # - + # name: ๐Ÿงช Run tests + # run: npm run test:ci + # working-directory: backend + - + name: ๐Ÿ—๏ธ Run build + run: npm run build + working-directory: backend diff --git a/.github/workflows/check-fe-pull-request.yml b/.github/workflows/check-fe-pull-request.yml new file mode 100644 index 000000000..b91e6f060 --- /dev/null +++ b/.github/workflows/check-fe-pull-request.yml @@ -0,0 +1,41 @@ +name: Check Frontend Pull Request + +on: + pull_request: + types: [ opened, synchronize ] + paths: + - 'frontend/**' + - '!frontend/README.md' + - '!frontend/.*' + - 'frontend/.eslintrc.js' + + +jobs: + + check-fe-pr: + name: Check + runs-on: ubuntu-latest + + steps: + - + name: โ˜๏ธ Checkout source + uses: actions/checkout@v3 + - + name: ๐Ÿ”ง Setup Node 16 + uses: actions/setup-node@v3 + with: + node-version: '16' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + - + name: ๐Ÿ“ฆ Install dependencies + run: npm ci --only-production --ignore-scripts + working-directory: frontend + # - + # name: ๐Ÿงช Run tests + # run: npm run test:ci + # working-directory: frontend + - + name: ๐Ÿ—๏ธ Run build + run: npm run build + working-directory: frontend diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 770fa7e83..e85ddcfd5 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -3,35 +3,88 @@ name: Push to Docker Hub on: [workflow_dispatch] jobs: - docker: + + backend-image: + name: Build backend image runs-on: ubuntu-latest + steps: - - name: Checkout - uses: actions/checkout@v2 + - + name: โ˜๏ธ Checkout source + uses: actions/checkout@v3 - - name: Set up QEMU + name: ๐Ÿ”ง Set up QEMU uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx + name: ๐Ÿ”ง Set up Docker Buildx uses: docker/setup-buildx-action@v2 - - name: Login to Docker Hub + name: ๐Ÿ‹ Login to Docker Hub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + # - + # name: ๐Ÿ“ฆ Build backend and export to Docker + # uses: docker/build-push-action@v3 + # with: + # load: true + # context: backend + # tags: infisical/backend:test + # - + # name: ๐Ÿงช Test backend image + # run: | + # docker run --rm infisical/backend:test - - name: Build and push backend + name: ๐Ÿ—๏ธ Build backend and push uses: docker/build-push-action@v3 with: push: true context: backend - tags: infisical/backend:test + tags: infisical/backend:latest + platforms: linux/amd64,linux/arm64 + + + frontend-image: + name: Build frontend image + runs-on: ubuntu-latest + + steps: + - + name: โ˜๏ธ Checkout source + uses: actions/checkout@v3 - - name: Build and push frontend + name: ๐Ÿ”ง Set up QEMU + uses: docker/setup-qemu-action@v2 + - + name: ๐Ÿ”ง Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - + name: ๐Ÿ‹ Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + # - + # name: ๐Ÿ“ฆ Build frontend and export to Docker + # uses: docker/build-push-action@v3 + # with: + # load: true + # context: frontend + # tags: infisical/frontend:test + # build-args: | + # POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} + # - + # name: ๐Ÿงช Test frontend image + # run: | + # docker run --rm infisical/frontend:test + - + name: ๐Ÿ—๏ธ Build frontend and push uses: docker/build-push-action@v3 with: push: true - file: frontend/Dockerfile.dev context: frontend - tags: infisical/frontend:test + tags: infisical/frontend:latest + platforms: linux/amd64,linux/arm64 + build-args: | + POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} diff --git a/README.md b/README.md index 4215a9fa8..4e0b7a053 100644 --- a/README.md +++ b/README.md @@ -48,11 +48,17 @@ And more. -## Get started +## ๐Ÿš€ Get started To quickly get started, visit our [get started guide](https://infisical.com/docs/getting-started/introduction). -## What's cool about this? +

+ + + +

+ +## ๐Ÿ”ฅ What's cool about this? Infisical makes secret management simple and end-to-end encrypted by default. We're on a mission to make it more accessible to all developers, not just security teams. @@ -62,20 +68,22 @@ If you care about efficiency and security, then Infisical is right for you. We are currently working hard to make Infisical more extensive. Need any integrations or want a new feature? Feel free to [create an issue](https://github.com/Infisical/infisical/issues) or [contribute](https://infisical.com/docs/contributing/overview) directly to the repository. -## Contributing +## ๐ŸŒฑ Contributing Whether it's big or small, we love contributions โค๏ธ Check out our guide to see how to [get started](https://infisical.com/docs/contributing/overview). -Not sure where to get started? [Book a free, non-pressure pairing sessions with one of our teammates](mailto:tony@infisical.com?subject=Pairing%20session&body=I'd%20like%20to%20do%20a%20pairing%20session!)! +Not sure where to get started? You can: +- [Book a free, non-pressure pairing sessions with one of our teammates](mailto:tony@infisical.com?subject=Pairing%20session&body=I'd%20like%20to%20do%20a%20pairing%20session!)! +- Join our Slack, and ask us any questions there. -## Community & Support +## ๐Ÿ’š Community & Support - [Slack](https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g) (For live discussion with the community and the Infisical team) - [GitHub Discussions](https://github.com/Infisical/infisical/discussions) (For help with building and deeper conversations about features) - [GitHub Issues](https://github.com/Infisical/infisical-cli/issues) (For any bugs and errors you encounter using Infisical) - [Twitter](https://twitter.com/infisical) (Get news fast) -## Status +## ๐Ÿฅ Status - [x] Public Alpha: Anyone can sign up over at [infisical.com](https://infisical.com) but go easy on us, there are kinks and we're just getting started. - [ ] Public Beta: Stable enough for most non-enterprise use-cases. @@ -83,13 +91,13 @@ Not sure where to get started? [Book a free, non-pressure pairing sessions with We're currently in Public Alpha. -## Stay Up-to-Date +## ๐Ÿšจ Stay Up-to-Date Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of new features are coming very quickly. Watch **releases** of this repository to be notified about future updates: ![infisical-star-github](https://github.com/Infisical/infisical/blob/main/.github/images/star-infisical.gif?raw=true) -## Integrations +## ๐Ÿ”Œ Integrations We're currently setting the foundation and building [integrations](https://infisical.com/docs/integrations/overview) so secrets can be synced everywhere. Any help is welcome! :) @@ -261,15 +269,15 @@ We're currently setting the foundation and building [integrations](https://infis -## Open-source vs. paid +## ๐Ÿ˜ Open-source vs. paid This repo is entirely MIT licensed, with the exception of the `ee` directory which will contain premium enterprise features requiring a Infisical license in the future. We're currently focused on developing non-enterprise offerings first that should suit most use-cases. -## Security +## ๐Ÿ›ก Security Looking to report a security vulnerability? Please don't post about it in GitHub issue. Instead, refer to our [SECURITY.md](./SECURITY.md) file. -## Contributors ๐Ÿฆธ +## ๐Ÿฆธ Contributors [//]: contributor-faces @@ -277,4 +285,4 @@ Looking to report a security vulnerability? Please don't post about it in GitHub - + diff --git a/backend/package-lock.json b/backend/package-lock.json index 7e42688ef..19a42c3a1 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "@sentry/node": "^7.14.0", - "@sentry/tracing": "^7.14.0", + "@sentry/tracing": "^7.19.0", "@types/crypto-js": "^4.1.1", "axios": "^1.1.3", "bigint-conversion": "^2.2.2", @@ -19,13 +19,13 @@ "crypto-js": "^4.1.1", "dotenv": "^16.0.1", "express": "^4.18.1", - "express-rate-limit": "^6.5.1", + "express-rate-limit": "^6.7.0", "express-validator": "^6.14.2", "handlebars": "^4.7.7", "helmet": "^5.1.1", "jsonwebtoken": "^8.5.1", "jsrp": "^0.2.4", - "mongoose": "^6.7.1", + "mongoose": "^6.7.2", "nodemailer": "^6.8.0", "posthog-node": "^2.1.0", "query-string": "^7.1.1", @@ -33,7 +33,7 @@ "stripe": "^10.7.0", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", - "typescript": "^4.8.4" + "typescript": "^4.9.3" }, "devDependencies": { "@posthog/plugin-scaffold": "^1.3.4", @@ -2608,13 +2608,13 @@ } }, "node_modules/@sentry/node": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.17.4.tgz", - "integrity": "sha512-cR+Gsir9c/tzFWxvk4zXkMQy6tNRHEYixHrb88XIjZVYDqDS9l2/bKs5nJusdmaUeLtmPp5Et2o7RJyS7gvKTQ==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.19.0.tgz", + "integrity": "sha512-yG7Tx32WqOkEHVotFLrumCcT9qlaSDTkFNZ+yLSvZXx74ifsE781DzBA9W7K7bBdYO3op+p2YdsOKzf3nPpAyQ==", "dependencies": { - "@sentry/core": "7.17.4", - "@sentry/types": "7.17.4", - "@sentry/utils": "7.17.4", + "@sentry/core": "7.19.0", + "@sentry/types": "7.19.0", + "@sentry/utils": "7.19.0", "cookie": "^0.4.1", "https-proxy-agent": "^5.0.0", "lru_map": "^0.3.3", @@ -2624,14 +2624,80 @@ "node": ">=8" } }, - "node_modules/@sentry/tracing": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.17.4.tgz", - "integrity": "sha512-9Fz6DI16ddnd970mlB5MiCNRSmSXp4SVZ1Yv3L22oS3kQeNxjBTE+htYNwJzSPrQp9aL/LqTYwlnrCy24u9XQA==", + "node_modules/@sentry/node/node_modules/@sentry/core": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.19.0.tgz", + "integrity": "sha512-YF9cTBcAnO4R44092BJi5Wa2/EO02xn2ziCtmNgAVTN2LD31a/YVGxGBt/FDr4Y6yeuVehaqijVVvtpSmXrGJw==", "dependencies": { - "@sentry/core": "7.17.4", - "@sentry/types": "7.17.4", - "@sentry/utils": "7.17.4", + "@sentry/types": "7.19.0", + "@sentry/utils": "7.19.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/node/node_modules/@sentry/types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.19.0.tgz", + "integrity": "sha512-oGRAT6lfzoKrxO1mvxiSj0XHxWPd6Gd1wpPGuu6iJo03xgWDS+MIlD1h2unqL4N5fAzLjzmbC2D2lUw50Kn2pA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/node/node_modules/@sentry/utils": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.19.0.tgz", + "integrity": "sha512-2L6lq+c9Ol2uiRxQDdcgoapmHJp24MhMN0gIkn2alSfMJ+ls6bGXzQHx6JAIdoOiwFQXRZHKL9ecfAc8O+vItA==", + "dependencies": { + "@sentry/types": "7.19.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/tracing": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.19.0.tgz", + "integrity": "sha512-SWY17M3TsgBePaGowUcSqBwaT0TJQzuNexVnLojuU0k6F57L9hubvP9zaoosoCfARXQ/3NypAFWnlJyf570rFQ==", + "dependencies": { + "@sentry/core": "7.19.0", + "@sentry/types": "7.19.0", + "@sentry/utils": "7.19.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/tracing/node_modules/@sentry/core": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.19.0.tgz", + "integrity": "sha512-YF9cTBcAnO4R44092BJi5Wa2/EO02xn2ziCtmNgAVTN2LD31a/YVGxGBt/FDr4Y6yeuVehaqijVVvtpSmXrGJw==", + "dependencies": { + "@sentry/types": "7.19.0", + "@sentry/utils": "7.19.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/tracing/node_modules/@sentry/types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.19.0.tgz", + "integrity": "sha512-oGRAT6lfzoKrxO1mvxiSj0XHxWPd6Gd1wpPGuu6iJo03xgWDS+MIlD1h2unqL4N5fAzLjzmbC2D2lUw50Kn2pA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/tracing/node_modules/@sentry/utils": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.19.0.tgz", + "integrity": "sha512-2L6lq+c9Ol2uiRxQDdcgoapmHJp24MhMN0gIkn2alSfMJ+ls6bGXzQHx6JAIdoOiwFQXRZHKL9ecfAc8O+vItA==", + "dependencies": { + "@sentry/types": "7.19.0", "tslib": "^1.9.3" }, "engines": { @@ -4517,9 +4583,9 @@ } }, "node_modules/express-rate-limit": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.6.0.tgz", - "integrity": "sha512-HFN2+4ZGdkQOS8Qli4z6knmJFnw6lZed67o6b7RGplWeb1Z0s8VXaj3dUgPIdm9hrhZXTRpCTHXA0/2Eqex0vA==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz", + "integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==", "engines": { "node": ">= 12.9.0" }, @@ -6452,9 +6518,9 @@ } }, "node_modules/mongoose": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.7.1.tgz", - "integrity": "sha512-qbagtqSyvIhUz4EWzXC00EA0DJHFrQwlzTlNGX5DjiESoJiPKqkEga1k9hviFKRFgBna+OlW54mkdi+0+AqxCw==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.7.2.tgz", + "integrity": "sha512-lrP2V5U1qhaf+z33fiIn7aYAZZ1fVDly+TkFRjTujNBF/FIHESATj2RbgAOSlWqv32fsZXkXejXzeVfjbv35Ow==", "dependencies": { "bson": "^4.7.0", "kareem": "2.4.1", @@ -10508,9 +10574,9 @@ } }, "node_modules/typescript": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", - "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", + "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13058,28 +13124,80 @@ } }, "@sentry/node": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.17.4.tgz", - "integrity": "sha512-cR+Gsir9c/tzFWxvk4zXkMQy6tNRHEYixHrb88XIjZVYDqDS9l2/bKs5nJusdmaUeLtmPp5Et2o7RJyS7gvKTQ==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.19.0.tgz", + "integrity": "sha512-yG7Tx32WqOkEHVotFLrumCcT9qlaSDTkFNZ+yLSvZXx74ifsE781DzBA9W7K7bBdYO3op+p2YdsOKzf3nPpAyQ==", "requires": { - "@sentry/core": "7.17.4", - "@sentry/types": "7.17.4", - "@sentry/utils": "7.17.4", + "@sentry/core": "7.19.0", + "@sentry/types": "7.19.0", + "@sentry/utils": "7.19.0", "cookie": "^0.4.1", "https-proxy-agent": "^5.0.0", "lru_map": "^0.3.3", "tslib": "^1.9.3" + }, + "dependencies": { + "@sentry/core": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.19.0.tgz", + "integrity": "sha512-YF9cTBcAnO4R44092BJi5Wa2/EO02xn2ziCtmNgAVTN2LD31a/YVGxGBt/FDr4Y6yeuVehaqijVVvtpSmXrGJw==", + "requires": { + "@sentry/types": "7.19.0", + "@sentry/utils": "7.19.0", + "tslib": "^1.9.3" + } + }, + "@sentry/types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.19.0.tgz", + "integrity": "sha512-oGRAT6lfzoKrxO1mvxiSj0XHxWPd6Gd1wpPGuu6iJo03xgWDS+MIlD1h2unqL4N5fAzLjzmbC2D2lUw50Kn2pA==" + }, + "@sentry/utils": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.19.0.tgz", + "integrity": "sha512-2L6lq+c9Ol2uiRxQDdcgoapmHJp24MhMN0gIkn2alSfMJ+ls6bGXzQHx6JAIdoOiwFQXRZHKL9ecfAc8O+vItA==", + "requires": { + "@sentry/types": "7.19.0", + "tslib": "^1.9.3" + } + } } }, "@sentry/tracing": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.17.4.tgz", - "integrity": "sha512-9Fz6DI16ddnd970mlB5MiCNRSmSXp4SVZ1Yv3L22oS3kQeNxjBTE+htYNwJzSPrQp9aL/LqTYwlnrCy24u9XQA==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.19.0.tgz", + "integrity": "sha512-SWY17M3TsgBePaGowUcSqBwaT0TJQzuNexVnLojuU0k6F57L9hubvP9zaoosoCfARXQ/3NypAFWnlJyf570rFQ==", "requires": { - "@sentry/core": "7.17.4", - "@sentry/types": "7.17.4", - "@sentry/utils": "7.17.4", + "@sentry/core": "7.19.0", + "@sentry/types": "7.19.0", + "@sentry/utils": "7.19.0", "tslib": "^1.9.3" + }, + "dependencies": { + "@sentry/core": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.19.0.tgz", + "integrity": "sha512-YF9cTBcAnO4R44092BJi5Wa2/EO02xn2ziCtmNgAVTN2LD31a/YVGxGBt/FDr4Y6yeuVehaqijVVvtpSmXrGJw==", + "requires": { + "@sentry/types": "7.19.0", + "@sentry/utils": "7.19.0", + "tslib": "^1.9.3" + } + }, + "@sentry/types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.19.0.tgz", + "integrity": "sha512-oGRAT6lfzoKrxO1mvxiSj0XHxWPd6Gd1wpPGuu6iJo03xgWDS+MIlD1h2unqL4N5fAzLjzmbC2D2lUw50Kn2pA==" + }, + "@sentry/utils": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.19.0.tgz", + "integrity": "sha512-2L6lq+c9Ol2uiRxQDdcgoapmHJp24MhMN0gIkn2alSfMJ+ls6bGXzQHx6JAIdoOiwFQXRZHKL9ecfAc8O+vItA==", + "requires": { + "@sentry/types": "7.19.0", + "tslib": "^1.9.3" + } + } } }, "@sentry/types": { @@ -14523,9 +14641,9 @@ } }, "express-rate-limit": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.6.0.tgz", - "integrity": "sha512-HFN2+4ZGdkQOS8Qli4z6knmJFnw6lZed67o6b7RGplWeb1Z0s8VXaj3dUgPIdm9hrhZXTRpCTHXA0/2Eqex0vA==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz", + "integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==", "requires": {} }, "express-validator": { @@ -15964,9 +16082,9 @@ } }, "mongoose": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.7.1.tgz", - "integrity": "sha512-qbagtqSyvIhUz4EWzXC00EA0DJHFrQwlzTlNGX5DjiESoJiPKqkEga1k9hviFKRFgBna+OlW54mkdi+0+AqxCw==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.7.2.tgz", + "integrity": "sha512-lrP2V5U1qhaf+z33fiIn7aYAZZ1fVDly+TkFRjTujNBF/FIHESATj2RbgAOSlWqv32fsZXkXejXzeVfjbv35Ow==", "requires": { "bson": "^4.7.0", "kareem": "2.4.1", @@ -18825,9 +18943,9 @@ } }, "typescript": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", - "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==" + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", + "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==" }, "uglify-js": { "version": "3.17.4", diff --git a/backend/package.json b/backend/package.json index 2bd296d9c..73cf9311f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,7 +1,7 @@ { "dependencies": { "@sentry/node": "^7.14.0", - "@sentry/tracing": "^7.14.0", + "@sentry/tracing": "^7.19.0", "@types/crypto-js": "^4.1.1", "axios": "^1.1.3", "bigint-conversion": "^2.2.2", @@ -10,13 +10,13 @@ "crypto-js": "^4.1.1", "dotenv": "^16.0.1", "express": "^4.18.1", - "express-rate-limit": "^6.5.1", + "express-rate-limit": "^6.7.0", "express-validator": "^6.14.2", "handlebars": "^4.7.7", "helmet": "^5.1.1", "jsonwebtoken": "^8.5.1", "jsrp": "^0.2.4", - "mongoose": "^6.7.1", + "mongoose": "^6.7.2", "nodemailer": "^6.8.0", "posthog-node": "^2.1.0", "query-string": "^7.1.1", @@ -24,7 +24,7 @@ "stripe": "^10.7.0", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", - "typescript": "^4.8.4" + "typescript": "^4.9.3" }, "name": "infisical-api", "version": "1.0.0", diff --git a/backend/src/helpers/signup.ts b/backend/src/helpers/signup.ts index 3229910a5..ff92fbf12 100644 --- a/backend/src/helpers/signup.ts +++ b/backend/src/helpers/signup.ts @@ -106,7 +106,7 @@ const initializeDefaultOrg = async ({ // initialize a default workspace inside the new organization const workspace = await createWorkspace({ - name: `${user.firstName}'s Project`, + name: `Example Project`, organizationId: organization._id.toString() }); diff --git a/cli/packages/cmd/export.go b/cli/packages/cmd/export.go new file mode 100644 index 000000000..a0d89301f --- /dev/null +++ b/cli/packages/cmd/export.go @@ -0,0 +1,140 @@ +/* +Copyright ยฉ 2022 NAME HERE +*/ +package cmd + +import ( + "encoding/csv" + "encoding/json" + "fmt" + "strings" + + "github.com/Infisical/infisical-merge/packages/models" + "github.com/Infisical/infisical-merge/packages/util" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +const ( + FormatDotenv string = "dotenv" + FormatJson string = "json" + FormatCSV string = "csv" +) + +// exportCmd represents the export command +var exportCmd = &cobra.Command{ + Use: "export", + Short: "Used to export environment variables to a file", + DisableFlagsInUseLine: true, + Example: "infisical export --env=prod --format=json > secrets.json", + Args: cobra.NoArgs, + PreRun: toggleDebug, + Run: func(cmd *cobra.Command, args []string) { + envName, err := cmd.Flags().GetString("env") + if err != nil { + log.Errorln("Unable to parse the environment flag") + log.Debugln(err) + return + } + + shouldExpandSecrets, err := cmd.Flags().GetBool("expand") + if err != nil { + log.Errorln("Unable to parse the substitute flag") + log.Debugln(err) + return + } + + projectId, err := cmd.Flags().GetString("projectId") + if err != nil { + log.Errorln("Unable to parse the project id flag") + log.Debugln(err) + return + } + + format, err := cmd.Flags().GetString("format") + if err != nil { + log.Errorln("Unable to parse the format flag") + log.Debugln(err) + return + } + + envsFromApi, err := util.GetAllEnvironmentVariables(projectId, envName) + if err != nil { + log.Errorln("Something went wrong when pulling secrets using your Infisical token. Double check the token, project id or environment name (dev, prod, ect.)") + log.Debugln(err) + return + } + + var output string + if shouldExpandSecrets { + substitutions := util.SubstituteSecrets(envsFromApi) + output, err = formatEnvs(substitutions, format) + if err != nil { + log.Errorln(err) + return + } + } else { + output, err = formatEnvs(envsFromApi, format) + if err != nil { + log.Errorln(err) + return + } + } + fmt.Print(output) + }, +} + +func init() { + rootCmd.AddCommand(exportCmd) + exportCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from") + exportCmd.Flags().String("projectId", "", "The project ID from which your secrets should be pulled from") + exportCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") + exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)") +} + +// Format according to the format flag +func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string, error) { + switch strings.ToLower(format) { + case FormatDotenv: + return formatAsDotEnv(envs), nil + case FormatJson: + return formatAsJson(envs), nil + case FormatCSV: + return formatAsCSV(envs), nil + default: + return "", fmt.Errorf("invalid format flag: %s", format) + } +} + +// Format environment variables as a CSV file +func formatAsCSV(envs []models.SingleEnvironmentVariable) string { + csvString := &strings.Builder{} + writer := csv.NewWriter(csvString) + writer.Write([]string{"Key", "Value"}) + for _, env := range envs { + writer.Write([]string{env.Key, env.Value}) + } + writer.Flush() + return csvString.String() +} + +// Format environment variables as a dotenv file +func formatAsDotEnv(envs []models.SingleEnvironmentVariable) string { + var dotenv string + for _, env := range envs { + dotenv += fmt.Sprintf("%s='%s'\n", env.Key, env.Value) + } + return dotenv +} + +// Format environment variables as a JSON file +func formatAsJson(envs []models.SingleEnvironmentVariable) string { + // Dump as a json array + json, err := json.Marshal(envs) + if err != nil { + log.Errorln("Unable to marshal environment variables to JSON") + log.Debugln(err) + return "" + } + return string(json) +} diff --git a/cli/packages/cmd/init.go b/cli/packages/cmd/init.go index 55abe6d4f..2789f220d 100644 --- a/cli/packages/cmd/init.go +++ b/cli/packages/cmd/init.go @@ -36,7 +36,7 @@ var initCmd = &cobra.Command{ return } - if util.WorkspaceConfigFileExists() { + if util.WorkspaceConfigFileExistsInCurrentPath() { shouldOverride, err := shouldOverrideWorkspacePrompt() if err != nil { log.Errorln("Unable to parse your answer") diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index f84fad501..29e2c4935 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -114,7 +114,7 @@ func init() { func askForLoginCredentials() (email string, password string, err error) { validateEmail := func(input string) error { - matched, err := regexp.MatchString("^[\\w!#$%&'*+/=?`{|}~^-]+(?:\\.[\\w!#$%&'*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$", input) + matched, err := regexp.MatchString("^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$", input) if err != nil || !matched { return errors.New("this doesn't look like an email address") } diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go index 42d85e5b2..12e4351bb 100644 --- a/cli/packages/cmd/root.go +++ b/cli/packages/cmd/root.go @@ -15,7 +15,7 @@ var rootCmd = &cobra.Command{ Short: "Infisical CLI is used to inject environment variables into any process", Long: `Infisical is a simple, end-to-end encrypted service that enables teams to sync and manage their environment variables across their development life cycle.`, CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true}, - Version: "0.1.6", + Version: "0.1.8", } // Execute adds all child commands to the root command and sets flags appropriately. diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index 6b44fa548..ec5210b1f 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -47,53 +47,17 @@ var runCmd = &cobra.Command{ return } - var envsFromApi []models.SingleEnvironmentVariable - infisicalToken := os.Getenv(util.INFISICAL_TOKEN_NAME) - if infisicalToken == "" { - hasUserLoggedInbefore, loggedInUserEmail, err := util.IsUserLoggedIn() - if err != nil { - log.Info("Unexpected issue occurred while checking login status. To see more details, add flag --debug") - log.Debugln(err) - return - } - - if !hasUserLoggedInbefore { - log.Infoln("No logged in user. To login, please run command [infisical login]") - return - } - - userCreds, err := util.GetUserCredsFromKeyRing(loggedInUserEmail) - if err != nil { - log.Infoln("Unable to get user creds from key ring") - log.Debug(err) - return - } - - if !util.WorkspaceConfigFileExists() { - log.Infoln("Your project is not connected to a project yet. Run command [infisical init]") - return - } - - envsFromApi, err = util.GetSecretsFromAPIUsingCurrentLoggedInUser(envName, userCreds) - if err != nil { - log.Errorln("Something went wrong when pulling secrets using your logged in credentials. If the issue persists, double check your project id/try logging in again.") - log.Debugln(err) - return - } - } else { - envsFromApi, err = util.GetSecretsFromAPIUsingInfisicalToken(infisicalToken, envName, projectId) - if err != nil { - log.Errorln("Something went wrong when pulling secrets using your Infisical token. Double check the token, project id or environment name (dev, prod, ect.)") - log.Debugln(err) - return - } + secrets, err := util.GetAllEnvironmentVariables(projectId, envName) + if err != nil { + log.Debugln(err) + return } if shouldExpandSecrets { - substitutions := util.SubstituteSecrets(envsFromApi) - execCmd(args[0], args[1:], substitutions) + secretsWithSubstitutions := util.SubstituteSecrets(secrets) + execCmd(args[0], args[1:], secretsWithSubstitutions) } else { - execCmd(args[0], args[1:], envsFromApi) + execCmd(args[0], args[1:], secrets) } }, @@ -108,9 +72,12 @@ func init() { // Credit: inspired by AWS Valut func execCmd(command string, args []string, envs []models.SingleEnvironmentVariable) error { - log.Infof("\x1b[%dm%s\x1b[0m", 32, "\u2713 Injected Infisical secrets into your application process successfully") - log.Debugln("Secrets to inject:", envs) + numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(envs)) + + log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected) log.Debugf("executing command: %s %s \n", command, strings.Join(args, " ")) + log.Debugln("Secrets injected:", envs) + cmd := exec.Command(command, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout diff --git a/cli/packages/models/error.go b/cli/packages/models/error.go index 28e48d54d..f6d58cb5a 100644 --- a/cli/packages/models/error.go +++ b/cli/packages/models/error.go @@ -5,10 +5,13 @@ import log "github.com/sirupsen/logrus" // Custom error type so that we can give helpful messages in CLI type Error struct { Err error - DebugMessage string FriendlyMessage string } func (e *Error) printFriendlyMessage() { log.Infoln(e.FriendlyMessage) } + +func (e *Error) printDebuError() { + log.Debugln(e.Err) +} diff --git a/cli/packages/util/config.go b/cli/packages/util/config.go index c42f26fb5..0ee04b013 100644 --- a/cli/packages/util/config.go +++ b/cli/packages/util/config.go @@ -56,7 +56,7 @@ func ConfigFileExists() bool { } } -func WorkspaceConfigFileExists() bool { +func WorkspaceConfigFileExistsInCurrentPath() bool { if _, err := os.Stat(INFISICAL_WORKSPACE_CONFIG_FILE_NAME); err == nil { return true } else { @@ -90,3 +90,65 @@ func GetFullConfigFilePath() (fullPathToFile string, fullPathToDirectory string, fullDirPath := fmt.Sprintf("%s/%s", homeDir, CONFIG_FOLDER_NAME) return fullPath, fullDirPath, err } + +// Given a path to a workspace config, unmarshal workspace config +func GetWorkspaceConfigByPath(path string) (workspaceConfig models.WorkspaceConfigFile, err error) { + workspaceConfigFileAsBytes, err := os.ReadFile(path) + if err != nil { + return models.WorkspaceConfigFile{}, fmt.Errorf("GetWorkspaceConfigByPath: Unable to read workspace config file because [%s]", err) + } + + var workspaceConfigFile models.WorkspaceConfigFile + err = json.Unmarshal(workspaceConfigFileAsBytes, &workspaceConfigFile) + if err != nil { + return models.WorkspaceConfigFile{}, fmt.Errorf("GetWorkspaceConfigByPath: Unable to unmarshal workspace config file because [%s]", err) + } + + return workspaceConfigFile, nil +} + +// Will get the list of .infisical.json files that are located +// within the root of each sub folder from where the CLI is ran from +func GetAllWorkSpaceConfigsStartingFromCurrentPath() (workspaces []models.WorkspaceConfigFile, err error) { + currentDir, err := os.Getwd() + if err != nil { + return nil, fmt.Errorf("GetAllProjectConfigs: unable to get the current directory because [%s]", err) + } + + files, err := os.ReadDir(currentDir) + if err != nil { + return nil, fmt.Errorf("GetAllProjectConfigs: unable to read the contents of the current directory because [%s]", err) + } + + listOfWorkSpaceConfigs := []models.WorkspaceConfigFile{} + for _, file := range files { + if !file.IsDir() && file.Name() == INFISICAL_WORKSPACE_CONFIG_FILE_NAME { + pathToWorkspaceConfigFile := currentDir + "/" + INFISICAL_WORKSPACE_CONFIG_FILE_NAME + + workspaceConfig, err := GetWorkspaceConfigByPath(pathToWorkspaceConfigFile) + if err != nil { + return nil, fmt.Errorf("GetAllProjectConfigs: Unable to get config file because [%s]", err) + } + + listOfWorkSpaceConfigs = append(listOfWorkSpaceConfigs, workspaceConfig) + + } else if file.IsDir() { + pathToSubFolder := currentDir + "/" + file.Name() + pathToMaybeWorkspaceConfigFile := pathToSubFolder + "/" + INFISICAL_WORKSPACE_CONFIG_FILE_NAME + + _, err := os.Stat(pathToMaybeWorkspaceConfigFile) + if err != nil { + continue // workspace config file doesn't exist + } + + workspaceConfig, err := GetWorkspaceConfigByPath(pathToMaybeWorkspaceConfigFile) + if err != nil { + return nil, fmt.Errorf("GetAllProjectConfigs: Unable to get config file because [%s]", err) + } + + listOfWorkSpaceConfigs = append(listOfWorkSpaceConfigs, workspaceConfig) + } + } + + return listOfWorkSpaceConfigs, nil +} diff --git a/cli/packages/util/crypto.go b/cli/packages/util/crypto.go index b308ea93d..c6eee2d0c 100644 --- a/cli/packages/util/crypto.go +++ b/cli/packages/util/crypto.go @@ -3,12 +3,9 @@ package util import ( "crypto/aes" "crypto/cipher" - - log "github.com/sirupsen/logrus" ) func DecryptSymmetric(key []byte, encryptedPrivateKey []byte, tag []byte, IV []byte) ([]byte, error) { - log.Debugln("Key:", key, "encryptedPrivateKey", encryptedPrivateKey, "tag", tag, "IV", IV) block, err := aes.NewCipher(key) if err != nil { return nil, err diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 5cf76d48e..d7d4cdc37 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "errors" "fmt" + "os" "regexp" "strings" @@ -13,19 +14,7 @@ import ( "golang.org/x/crypto/nacl/box" ) -func GetSecretsFromAPIUsingCurrentLoggedInUser(envName string, userCreds models.UserCredentials) ([]models.SingleEnvironmentVariable, error) { - log.Debugln("envName", envName, "userCreds", userCreds) - // check if user has configured a workspace - workspace, err := GetWorkSpaceFromFile() - if err != nil { - return nil, fmt.Errorf("Unable to read workspace file:", err) - } - - // create http client - httpClient := resty.New(). - SetAuthToken(userCreds.JTWToken). - SetHeader("Accept", "application/json") - +func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string, workspace models.WorkspaceConfigFile, userCreds models.UserCredentials) (listOfSecrets []models.SingleEnvironmentVariable, err error) { var pullSecretsRequestResponse models.PullSecretsResponse response, err := httpClient. R(). @@ -34,14 +23,11 @@ func GetSecretsFromAPIUsingCurrentLoggedInUser(envName string, userCreds models. SetResult(&pullSecretsRequestResponse). Get(fmt.Sprintf("%v/v1/secret/%v", INFISICAL_URL, workspace.WorkspaceId)) // need to change workspace id - log.Debugln("Response from get secrets:", response) - if err != nil { return nil, err } if response.StatusCode() > 299 { - log.Debugln(response) return nil, fmt.Errorf(response.Status()) } @@ -66,7 +52,7 @@ func GetSecretsFromAPIUsingCurrentLoggedInUser(envName string, userCreds models. return nil, err } - log.Debugln("workspaceKey", workspaceKey, "nonce", nonce, "senderPublicKey", senderPublicKey, "currentUsersPrivateKey", currentUsersPrivateKey) + // log.Debugln("workspaceKey", workspaceKey, "nonce", nonce, "senderPublicKey", senderPublicKey, "currentUsersPrivateKey", currentUsersPrivateKey) workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey)) var listOfEnv []models.SingleEnvironmentVariable @@ -100,6 +86,32 @@ func GetSecretsFromAPIUsingCurrentLoggedInUser(envName string, userCreds models. return listOfEnv, nil } +func GetSecretsFromAPIUsingCurrentLoggedInUser(envName string, userCreds models.UserCredentials) ([]models.SingleEnvironmentVariable, error) { + log.Debugln("GetSecretsFromAPIUsingCurrentLoggedInUser", "envName", envName, "userCreds", userCreds) + // check if user has configured a workspace + workspaces, err := GetAllWorkSpaceConfigsStartingFromCurrentPath() + if err != nil { + return nil, fmt.Errorf("Unable to read workspace file(s):", err) + } + + // create http client + httpClient := resty.New(). + SetAuthToken(userCreds.JTWToken). + SetHeader("Accept", "application/json") + + secrets := []models.SingleEnvironmentVariable{} + for _, workspace := range workspaces { + secretsFromAPI, err := getSecretsByWorkspaceIdAndEnvName(*httpClient, envName, workspace, userCreds) + if err != nil { + return nil, fmt.Errorf("GetSecretsFromAPIUsingCurrentLoggedInUser: Unable to get secrets by workspace id and env name") + } + + secrets = append(secrets, secretsFromAPI...) + } + + return secrets, nil +} + func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string, projectId string) ([]models.SingleEnvironmentVariable, error) { if infisicalToken == "" || projectId == "" || envName == "" { return nil, errors.New("infisical token, project id and or environment name cannot be empty") @@ -126,7 +138,6 @@ func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string, } if response.StatusCode() > 299 { - log.Debugln(response) return nil, fmt.Errorf(response.Status()) } @@ -184,6 +195,58 @@ func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string, return listOfEnv, nil } +func GetAllEnvironmentVariables(projectId string, envName string) ([]models.SingleEnvironmentVariable, error) { + var envsFromApi []models.SingleEnvironmentVariable + infisicalToken := os.Getenv(INFISICAL_TOKEN_NAME) + + if infisicalToken == "" { + hasUserLoggedInbefore, loggedInUserEmail, err := IsUserLoggedIn() + if err != nil { + log.Info("Unexpected issue occurred while checking login status. To see more details, add flag --debug") + log.Debugln(err) + return envsFromApi, err + } + + if !hasUserLoggedInbefore { + log.Infoln("No logged in user. To login, please run command [infisical login]") + return envsFromApi, fmt.Errorf("user not logged in") + } + + userCreds, err := GetUserCredsFromKeyRing(loggedInUserEmail) + if err != nil { + log.Infoln("Unable to get user creds from key ring") + log.Debug(err) + return envsFromApi, err + } + + workspaceConfigs, err := GetAllWorkSpaceConfigsStartingFromCurrentPath() + if err != nil { + return nil, fmt.Errorf("unable to check if you have a %s file in your current directory", INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + } + + if len(workspaceConfigs) == 0 { + log.Infoln("Your local project is not connected to a Infisical project yet. Run command [infisical init]") + return envsFromApi, fmt.Errorf("project not initialized") + } + + envsFromApi, err = GetSecretsFromAPIUsingCurrentLoggedInUser(envName, userCreds) + if err != nil { + log.Errorln("Something went wrong when pulling secrets using your logged in credentials. If the issue persists, double check your project id/try logging in again.") + log.Debugln(err) + return envsFromApi, err + } + } else { + envsFromApi, err := GetSecretsFromAPIUsingInfisicalToken(infisicalToken, envName, projectId) + if err != nil { + log.Errorln("Something went wrong when pulling secrets using your Infisical token. Double check the token, project id or environment name (dev, prod, ect.)") + log.Debugln(err) + return envsFromApi, err + } + } + + return envsFromApi, nil +} + func GetWorkSpacesFromAPI(userCreds models.UserCredentials) (workspaces []models.Workspace, err error) { // create http client httpClient := resty.New(). diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 6eabfe4d9..949a96c7d 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -23,7 +23,6 @@ services: build: context: ./backend dockerfile: Dockerfile - image: infisical/backend volumes: - ./backend/src:/app/src - ./backend/nodemon.json:/app/nodemon.json @@ -43,7 +42,6 @@ services: build: context: ./frontend dockerfile: Dockerfile.dev - image: infisical/frontend volumes: - ./frontend/pages:/app/pages - ./frontend/public:/app/public @@ -52,12 +50,8 @@ services: env_file: .env environment: - NEXT_PUBLIC_ENV=development - - NEXT_PUBLIC_WEBSITE_URL=${SITE_URL} - - NEXT_PUBLIC_POSTHOG_HOST=${POSTHOG_HOST} - - NEXT_PUBLIC_POSTHOG_API_KEY=${POSTHOG_PROJECT_API_KEY} - NEXT_PUBLIC_STRIPE_PRODUCT_PRO=${STRIPE_PRODUCT_PRO} - NEXT_PUBLIC_STRIPE_PRODUCT_STARTER=${STRIPE_PRODUCT_STARTER} - - NEXT_PUBLIC_TELEMETRY_ENABLED=${TELEMETRY_ENABLED} networks: - infisical-dev @@ -78,6 +72,8 @@ services: container_name: infisical-dev-mongo-express image: mongo-express restart: always + depends_on: + - mongo env_file: .env environment: - ME_CONFIG_MONGODB_ADMINUSERNAME=${MONGO_USERNAME} @@ -93,4 +89,4 @@ volumes: driver: local networks: - infisical-dev: \ No newline at end of file + infisical-dev: diff --git a/docker-compose.yml b/docker-compose.yml index 3204f9257..cc92e1406 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,14 +17,10 @@ services: - infisical backend: - platform: linux/amd64 container_name: infisical-backend restart: unless-stopped depends_on: - mongo - build: - context: ./backend - dockerfile: Dockerfile image: infisical/backend command: npm run start env_file: .env @@ -34,24 +30,17 @@ services: - infisical frontend: - platform: linux/amd64 container_name: infisical-frontend restart: unless-stopped depends_on: - backend - build: - context: ./frontend - dockerfile: Dockerfile.prod image: infisical/frontend env_file: .env environment: - - NEXT_PUBLIC_ENV=production - - NEXT_PUBLIC_WEBSITE_URL=${SITE_URL} - - NEXT_PUBLIC_POSTHOG_HOST=${POSTHOG_HOST} - - NEXT_PUBLIC_POSTHOG_API_KEY=${POSTHOG_PROJECT_API_KEY} + # - NEXT_PUBLIC_POSTHOG_API_KEY=${POSTHOG_PROJECT_API_KEY} + - INFISICAL_TELEMETRY_ENABLED=${TELEMETRY_ENABLED} - NEXT_PUBLIC_STRIPE_PRODUCT_PRO=${STRIPE_PRODUCT_PRO} - NEXT_PUBLIC_STRIPE_PRODUCT_STARTER=${STRIPE_PRODUCT_STARTER} - - NEXT_PUBLIC_TELEMETRY_ENABLED=${TELEMETRY_ENABLED} networks: - infisical @@ -73,4 +62,4 @@ volumes: driver: local networks: - infisical: \ No newline at end of file + infisical: diff --git a/docs/cli/reference/commands.mdx b/docs/cli/commands/commands.mdx similarity index 100% rename from docs/cli/reference/commands.mdx rename to docs/cli/commands/commands.mdx diff --git a/docs/cli/commands/export.mdx b/docs/cli/commands/export.mdx new file mode 100644 index 000000000..fd58868ff --- /dev/null +++ b/docs/cli/commands/export.mdx @@ -0,0 +1,33 @@ +--- +title: "infisical export" +--- + +```bash +infisical export [options] +``` + +## Description + +Export environment variables from the platform into a file format. + +## Options + +| Option | Description | Default value | +| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` | +| `--projectId` | Only required if injecting via the [service token method](../token). If you are not using service token, the project id will be automatically retrieved from the `.infisical.json` located at the root of your local project. | `None` | +| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` | +| `--format` | Format of the output file. Accepted values: `dotenv`, `csv` and `json` | `dotenv` | + +## Examples + +```bash +# Export variables to a .env file +infisical export > .env + +# Export variables to a CSV file +infisical export --format=csv > secrets.csv + +# Export variables to a JSON file +infisical export --format=json > secrets.json +``` diff --git a/docs/cli/reference/init.mdx b/docs/cli/commands/init.mdx similarity index 100% rename from docs/cli/reference/init.mdx rename to docs/cli/commands/init.mdx diff --git a/docs/cli/reference/login.mdx b/docs/cli/commands/login.mdx similarity index 100% rename from docs/cli/reference/login.mdx rename to docs/cli/commands/login.mdx diff --git a/docs/cli/reference/run.mdx b/docs/cli/commands/run.mdx similarity index 100% rename from docs/cli/reference/run.mdx rename to docs/cli/commands/run.mdx diff --git a/docs/cli/overview.mdx b/docs/cli/overview.mdx index 8411b8e75..6267a66f0 100644 --- a/docs/cli/overview.mdx +++ b/docs/cli/overview.mdx @@ -1,5 +1,5 @@ --- -title: "Overview" +title: "Install" --- Prerequisite: Set up an account with [Infisical Cloud](https://app.infisical.com) or via a [self-hosted installation](/self-hosting/overview). diff --git a/docs/getting-started/dashboard/token.mdx b/docs/getting-started/dashboard/token.mdx index 85f46b799..9b3ddf79f 100644 --- a/docs/getting-started/dashboard/token.mdx +++ b/docs/getting-started/dashboard/token.mdx @@ -4,13 +4,16 @@ title: "Infisical Token" An Infisical Token is needed to authenticate the CLI when there isn't an easy way to input your login credentials. -It's useful for the [Docker](/integrations/platforms/docker) and [Docker Compose](/integrations/platforms/docker-compose) integrations. +It's useful for your CI/CD environments and integrations such as [Docker](/integrations/platforms/docker) and [Docker Compose](/integrations/platforms/docker-compose). + +To generate the the token, head over to your project settings as shown below. -It's possible to generate the token in the settings of a project. ![token add](../../images/project-token-add.png) The token grants read-only access to a particular environment and project for - a specified amount of time. + a specified amount of time. Once the token is expired, the CLI using it will no longer be able to make + requests with it. + diff --git a/docs/integrations/platforms/docker-compose.mdx b/docs/integrations/platforms/docker-compose.mdx index 9e0f90e55..f2cdd60c8 100644 --- a/docs/integrations/platforms/docker-compose.mdx +++ b/docs/integrations/platforms/docker-compose.mdx @@ -4,14 +4,14 @@ title: "Docker Compose" The Docker Compose integration enables you to inject environment variables from Infisical into the containers defined in your compose file. -## Add the CLI to your Dockerfile(s) +## Add the CLI to your Dockerfile(s) start command -Follow steps 1 through 3 on our [guide to configure Infisical CLI](../integrations/platforms/docker) in your Dockerfile. +Follow the [guide to configure Infisical CLI](./docker) in your your Dockerfile first. ## Generate Infisical Token In order for Infisical CLI to authenticate and retrieve your project's secrets without exposing your login credentials, you must generate a Infisical Token. -To learn how, visit [Infisical Token](../getting-started/cli/infisical-token). Once you have generated the token, keep it handy. +To learn how, visit [Infisical Token](../../getting-started/dashboard/token). Once you have generated the token, keep it handy. If you have multiple services and they do not use the same secrets, you will diff --git a/docs/mint.json b/docs/mint.json index 1d5246680..4fa0436d8 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -87,12 +87,12 @@ "cli/overview", "cli/usage", { - "group": "Reference", + "group": "Commands", "pages": [ - "cli/reference/commands", - "cli/reference/login", - "cli/reference/init", - "cli/reference/run" + "cli/commands/login", + "cli/commands/init", + "cli/commands/run", + "cli/commands/export" ] } ] diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index bea36da32..a55efbebc 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -9,24 +9,24 @@ Configuring Infisical requires setting some environment variables. There is a fi | Variable | Description | Default Value | | ---------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------- | -| `PRIVATE_KEY` | โ—๏ธ NaCl-generated server secret key | `None` | -| `PUBLIC_KEY` | โ—๏ธ NaCl-generated server public key | `None` | -| `ENCRYPTION_KEY` | โ—๏ธ Strong hex encryption key | `None` | -| `JWT_SIGNUP_SECRET` | โ—๏ธJWT token secret | `None` | -| `JWT_REFRESH_SECRET` | โ—๏ธ JWT token secret | `None` | -| `JWT_AUTH_SECRET` | โ—๏ธ JWT token secret | `None` | +| `PRIVATE_KEY` | โ—๏ธ NaCl-generated server secret key | `None` | +| `PUBLIC_KEY` | โ—๏ธ NaCl-generated server public key | `None` | +| `ENCRYPTION_KEY` | โ—๏ธ Strong hex encryption key | `None` | +| `JWT_SIGNUP_SECRET` | โ—๏ธ JWT token secret | `None` | +| `JWT_REFRESH_SECRET` | โ—๏ธ JWT token secret | `None` | +| `JWT_AUTH_SECRET` | โ—๏ธ JWT token secret | `None` | | `JWT_SIGNUP_LIFETIME` | JWT token lifetime expressed in seconds or a string describing a time span (e.g. 60, "2 days", "10h", "7d") | `15m` | | `JWT_REFRESH_LIFETIME` | JWT token lifetime expressed in seconds or a string describing a time span (e.g. 60, "2 days", "10h", "7d") | `90d` | | `JWT_AUTH_LIFETIME` | JWT token lifetime expressed in seconds or a string describing a time span (e.g. 60, "2 days", "10h", "7d") | `10d` | | `EMAIL_TOKEN_LIFETIME` | Email OTP/magic-link lifetime expressed in seconds | `86400` | -| `MONGO_URL` | โ—๏ธ MongoDB instance connection string either to container instance or MongoDB Cloud | `None` | +| `MONGO_URL` | โ—๏ธ MongoDB instance connection string either to container instance or MongoDB Cloud | `None` | | `MONGO_USERNAME` | MongoDB username if using container | `None` | | `MONGO_PASSWORD` | MongoDB password if using container | `None` | -| `SITE_URL` | โ—๏ธ Site URL - should be an absolute URL including the protocol (e.g. `https://app.infisical.com`) | `None` | -| `SMT_HOST` | Whether the user joined the community | `smtp.gmail.com` | -| `SMTP_NAME` | Hostname to connect to for establishing SMTP connections (e.g. `Team`) | `None` | -| `SMTP_USERNAME` | โ—๏ธ Credential to connect to host (e.g. `team@infisical.com`) | `None` | -| `SMTP_PASSWORD` | โ—๏ธ Credential to connect to host | `None` | +| `SITE_URL` | โ—๏ธ Site URL - should be an absolute URL including the protocol (e.g. `https://app.infisical.com`) | `None` | +| `SMTP_HOST` | Hostname to connect to for establishing SMTP connections | `smtp.gmail.com` | +| `SMTP_NAME` | Name label to be used in From field (e.g. `Team`) | `None` | +| `SMTP_USERNAME` | โ—๏ธ Credential to connect to host (e.g. `team@infisical.com`) | `None` | +| `SMTP_PASSWORD` | โ—๏ธ Credential to connect to host | `None` | | `TELEMETRY_ENABLED` | `true` or `false`. [More](../overview). | `true` | | `OAUTH_CLIENT_SECRET_HEROKU` | OAuth client secret for Heroku integration | `None` | | `OAUTH_TOKEN_URL_HEROKU` | OAuth token URL for Heroku integration | `None` | diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 000000000..5e59c68aa --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,64 @@ +ARG POSTHOG_HOST=https://app.posthog.com +ARG POSTHOG_API_KEY=posthog-api-key + +FROM node:16-alpine AS deps +# Install dependencies only when needed. Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +# RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Copy over dependency files +COPY package.json package-lock.json next.config.js ./ + +# Install dependencies +RUN npm ci --only-production + + +# Rebuild the source code only when needed +FROM node:16-alpine AS builder +WORKDIR /app + +# Copy dependencies +COPY --from=deps /app/node_modules ./node_modules +# Copy all files +COPY . . + +ENV NODE_ENV production +ENV NEXT_PUBLIC_ENV production +ARG POSTHOG_HOST +ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST +ARG POSTHOG_API_KEY +ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY + +# Build +RUN npm run build + + +# Production image +FROM node:16-alpine AS runner +WORKDIR /app + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +RUN mkdir -p /app/.next/cache/images && chown nextjs:nodejs /app/.next/cache/images +VOLUME /app/.next/cache/images + +ARG POSTHOG_API_KEY +ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \ + BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY + +COPY --chown=nextjs:nodejs --chmod=555 scripts ./scripts +COPY --from=builder /app/public ./public +RUN chown nextjs:nodejs ./public/data +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 +ENV NEXT_TELEMETRY_DISABLED 1 + + +CMD ["/app/scripts/start.sh"] diff --git a/frontend/components/analytics/posthog.js b/frontend/components/analytics/posthog.js index c8d51ad1b..44ee4fdb3 100644 --- a/frontend/components/analytics/posthog.js +++ b/frontend/components/analytics/posthog.js @@ -4,12 +4,11 @@ import { ENV, POSTHOG_API_KEY, POSTHOG_HOST, - TELEMETRY_ENABLED, } from "../utilities/config"; export const initPostHog = () => { if (typeof window !== "undefined") { - if (ENV == "production" && TELEMETRY_ENABLED) { + if (ENV == "production" && TELEMETRY_CAPTURING_ENABLED) { // eslint-disable-line posthog.init(POSTHOG_API_KEY, { api_host: POSTHOG_HOST, }); diff --git a/frontend/components/basic/InputField.js b/frontend/components/basic/InputField.tsx similarity index 89% rename from frontend/components/basic/InputField.js rename to frontend/components/basic/InputField.tsx index 8368d9797..02bb8a8cc 100644 --- a/frontend/components/basic/InputField.js +++ b/frontend/components/basic/InputField.tsx @@ -1,19 +1,27 @@ -import React from "react"; -import { useState } from "react"; +import React, { useState } from "react"; import { useRouter } from "next/router"; -import { - faCircle, - faCircleExclamation, - faE, - faEye, - faEyeSlash, -} from "@fortawesome/free-solid-svg-icons"; +import { faCircle, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import guidGenerator from "../utilities/randomId"; -import Error from "./Error"; -const InputField = (props) => { +interface InputFieldProps { + static?: boolean; + label: string; + type: string; + value: string; + placeholder?: string; + isRequired: boolean; + disabled?: boolean; + error?: boolean; + text?: string; + name?: string; + blurred?: boolean; + errorText?: string; + onChangeHandler: (value: string) => void; +} + +const InputField = (props: InputFieldProps) => { const [passwordVisible, setPasswordVisible] = useState(false); const router = useRouter(); @@ -67,7 +75,7 @@ const InputField = (props) => { > props.onChangeHandler(e.target.value)} - type={passwordVisible == false ? props.type : "text"} + type={passwordVisible === false ? props.type : "text"} placeholder={props.placeholder} value={props.value} required={props.isRequired} diff --git a/frontend/components/basic/layout.js b/frontend/components/basic/Layout.tsx similarity index 55% rename from frontend/components/basic/layout.js rename to frontend/components/basic/Layout.tsx index effe38942..4a40fdef0 100644 --- a/frontend/components/basic/layout.js +++ b/frontend/components/basic/Layout.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-unexpected-multiline */ /* eslint-disable react-hooks/exhaustive-deps */ import { useEffect, useState } from "react"; import Link from "next/link"; @@ -22,6 +23,7 @@ import getWorkspaces from "~/pages/api/workspace/getWorkspaces"; import uploadKeys from "~/pages/api/workspace/uploadKeys"; import NavBarDashboard from "../navigation/NavBarDashboard"; +import { tempLocalStorage } from "../utilities/checks/tempLocalStorage"; import { decryptAssymmetric, encryptAssymmetric, @@ -30,13 +32,17 @@ import Button from "./buttons/Button"; import AddWorkspaceDialog from "./dialog/AddWorkspaceDialog"; import Listbox from "./Listbox"; -export default function Layout({ children }) { +interface LayoutProps { + children: React.ReactNode; +} + +export default function Layout({ children }: LayoutProps) { const router = useRouter(); const [workspaceList, setWorkspaceList] = useState([]); const [workspaceMapping, setWorkspaceMapping] = useState([{ 1: 2 }]); const [workspaceSelected, setWorkspaceSelected] = useState("โˆž"); - let [newWorkspaceName, setNewWorkspaceName] = useState(""); - let [isOpen, setIsOpen] = useState(false); + const [newWorkspaceName, setNewWorkspaceName] = useState(""); + const [isOpen, setIsOpen] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(false); @@ -44,158 +50,186 @@ export default function Layout({ children }) { setIsOpen(false); } + function openModal() { + setIsOpen(true); + } + // TODO: what to do about the fact that 2ids can have the same name /** * When a user creates a new workspace, redirect them to the page of the new workspace. * @param {*} workspaceName */ - async function submitModal(workspaceName, addAllUsers) { + async function submitModal(workspaceName: string, addAllUsers: boolean) { setLoading(true); + // timeout code. setTimeout(() => setLoading(false), 1500); - const workspaces = await getWorkspaces(); - const currentWorkspaces = workspaces.map((workspace) => workspace.name); - if (!currentWorkspaces.includes(workspaceName)) { - const newWorkspace = await createWorkspace({ - workspaceName, - organizationId: localStorage.getItem("orgData.id") - }); - let newWorkspaceId; - try { - newWorkspaceId = newWorkspace._id; - } catch (error) { - console.log(error); - } - if (addAllUsers) { - let orgUsers = await getOrganizationUsers({ - orgId: localStorage.getItem("orgData.id"), + + try { + const workspaces = await getWorkspaces(); + const currentWorkspaces = workspaces.map((workspace) => workspace.name); + if (!currentWorkspaces.includes(workspaceName)) { + const newWorkspace = await createWorkspace({ + workspaceName, + organizationId: tempLocalStorage("orgData.id"), }); - orgUsers.map(async (user) => { - if (user.status == "accepted") { - let result = await addUserToWorkspace( - user.user.email, - newWorkspaceId - ); - if (result?.invitee && result?.latestKey) { - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); + const newWorkspaceId = newWorkspace._id; - // assymmetrically decrypt symmetric key with local private key - const key = decryptAssymmetric({ - ciphertext: result.latestKey.encryptedKey, - nonce: result.latestKey.nonce, - publicKey: result.latestKey.sender.publicKey, - privateKey: PRIVATE_KEY, - }); + if (addAllUsers) { + const orgUsers = await getOrganizationUsers({ + orgId: tempLocalStorage("orgData.id"), + }); + orgUsers.map(async (user: any) => { + if (user.status == "accepted") { + const result = await addUserToWorkspace( + user.user.email, + newWorkspaceId + ); + if (result?.invitee && result?.latestKey) { + const PRIVATE_KEY = tempLocalStorage("PRIVATE_KEY"); - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: key, - publicKey: result.invitee.publicKey, - privateKey: PRIVATE_KEY, - }); + // assymmetrically decrypt symmetric key with local private key + const key = decryptAssymmetric({ + ciphertext: result.latestKey.encryptedKey, + nonce: result.latestKey.nonce, + publicKey: result.latestKey.sender.publicKey, + privateKey: PRIVATE_KEY, + }); - uploadKeys(newWorkspaceId, result.invitee._id, ciphertext, nonce); + const { ciphertext, nonce } = encryptAssymmetric({ + plaintext: key, + publicKey: result.invitee.publicKey, + privateKey: PRIVATE_KEY, + }) as { ciphertext: string; nonce: string }; + + uploadKeys( + newWorkspaceId, + result.invitee._id, + ciphertext, + nonce + ); + } } - } - }); + }); + } + router.push("/dashboard/" + newWorkspaceId + "?Development"); + setIsOpen(false); + setNewWorkspaceName(""); + } else { + console.error("A project with this name already exists."); + setError(true); + setLoading(false); } - router.push("/dashboard/" + newWorkspaceId + "?Development"); - setIsOpen(false); - setNewWorkspaceName(""); - } else { - setError("A project with this name already exists."); + } catch (err) { + console.error(err); + setError(true); setLoading(false); } } - function openModal() { - setIsOpen(true); - } - const menuItems = [ { href: - "/dashboard/" + workspaceMapping[workspaceSelected] + "?Development", + "/dashboard/" + + workspaceMapping[workspaceSelected as any] + + "?Development", title: "Secrets", emoji: , }, { - href: "/users/" + workspaceMapping[workspaceSelected], + href: "/users/" + workspaceMapping[workspaceSelected as any], title: "Members", emoji: , }, { - href: "/integrations/" + workspaceMapping[workspaceSelected], + href: "/integrations/" + workspaceMapping[workspaceSelected as any], title: "Integrations", emoji: , }, { - href: "/settings/project/" + workspaceMapping[workspaceSelected], + href: "/settings/project/" + workspaceMapping[workspaceSelected as any], title: "Project Settings", emoji: , }, ]; - useEffect(async () => { + useEffect(() => { // Put a user in a workspace if they're not in one yet - if ( - localStorage.getItem("orgData.id") == null || - localStorage.getItem("orgData.id") == "" - ) { - const userOrgs = await getOrganizations(); - localStorage.setItem("orgData.id", userOrgs[0]._id); - } - - let orgUserProjects = await getOrganizationUserProjects({ - orgId: localStorage.getItem("orgData.id"), - }); - let userWorkspaces = orgUserProjects; - if ( - userWorkspaces.length == 0 && - router.asPath != "/noprojects" && - !router.asPath.includes("settings") - ) { - router.push("/noprojects"); - } else if (router.asPath != "/noprojects") { - const intendedWorkspaceId = router.asPath - .split("/")[router.asPath.split("/").length - 1].split("?")[0]; - // If a user is not a member of a workspace they are trying to access, just push them to one of theirs - if ( - intendedWorkspaceId != "heroku" && - !userWorkspaces - .map((workspace) => workspace._id) - .includes(intendedWorkspaceId) - ) { - router.push("/dashboard/" + userWorkspaces[0]._id + "?Development"); - } else { - setWorkspaceList(userWorkspaces.map((workspace) => workspace.name)); - setWorkspaceMapping( - Object.fromEntries( - userWorkspaces.map((workspace) => [workspace.name, workspace._id]) - ) - ); - setWorkspaceSelected( - Object.fromEntries( - userWorkspaces.map((workspace) => [workspace._id, workspace.name]) - )[ - router.asPath - .split("/")[router.asPath.split("/").length - 1].split("?")[0]] - ); + const putUserInWorkSpace = async () => { + if (tempLocalStorage("orgData.id") === "") { + const userOrgs = await getOrganizations(); + localStorage.setItem("orgData.id", userOrgs[0]._id); } - } + + const orgUserProjects = await getOrganizationUserProjects({ + orgId: tempLocalStorage("orgData.id"), + }); + const userWorkspaces = orgUserProjects; + if ( + userWorkspaces.length == 0 && + router.asPath != "/noprojects" && + !router.asPath.includes("settings") + ) { + router.push("/noprojects"); + } else if (router.asPath != "/noprojects") { + const intendedWorkspaceId = router.asPath + .split("/") + [router.asPath.split("/").length - 1].split("?")[0]; + // If a user is not a member of a workspace they are trying to access, just push them to one of theirs + if ( + intendedWorkspaceId != "heroku" && + !userWorkspaces + .map((workspace: { _id: string }) => workspace._id) + .includes(intendedWorkspaceId) + ) { + router.push("/dashboard/" + userWorkspaces[0]._id + "?Development"); + } else { + setWorkspaceList( + userWorkspaces.map((workspace: any) => workspace.name) + ); + setWorkspaceMapping( + Object.fromEntries( + userWorkspaces.map((workspace: any) => [ + workspace.name, + workspace._id, + ]) + ) as any + ); + setWorkspaceSelected( + Object.fromEntries( + userWorkspaces.map((workspace: any) => [ + workspace._id, + workspace.name, + ]) + )[ + router.asPath + .split("/") + [router.asPath.split("/").length - 1].split("?")[0] + ] + ); + } + } + }; + putUserInWorkSpace(); }, []); useEffect(() => { try { if ( - workspaceMapping[workspaceSelected] && - workspaceMapping[workspaceSelected] !== + workspaceMapping[Number(workspaceSelected)] && + `${workspaceMapping[Number(workspaceSelected)]}` !== router.asPath - .split("/")[router.asPath.split("/").length - 1].split("?")[0] + .split("/") + [router.asPath.split("/").length - 1].split("?")[0] ) { - router.push("/dashboard/" + workspaceMapping[workspaceSelected] + "?Development"); + router.push( + "/dashboard/" + + workspaceMapping[Number(workspaceSelected)] + + "?Development" + ); localStorage.setItem( "projectData.id", - workspaceMapping[workspaceSelected] + `${workspaceMapping[Number(workspaceSelected)]}` ); } } catch (error) { @@ -212,18 +246,18 @@ export default function Layout({ children }) {