diff --git a/.env.example b/.env.example index 932846bfe..169e6db0d 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,13 @@ # Keys # Required key for platform encryption/decryption ops -ENCRYPTION_KEY=replace_with_lengthy_secure_hex +ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 # JWT # Required secrets to sign JWT tokens -JWT_SIGNUP_SECRET=replace_with_lengthy_secure_hex -JWT_REFRESH_SECRET=replace_with_lengthy_secure_hex -JWT_AUTH_SECRET=replace_with_lengthy_secure_hex -JWT_SERVICE_SECRET=replace_with_lengthy_secure_hex +JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a +JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff +JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f +JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200 # JWT lifetime # Optional lifetimes for JWT tokens expressed in seconds or a string @@ -34,15 +34,12 @@ MONGO_PASSWORD=example SITE_URL=http://localhost:8080 # Mail/SMTP -# Required to send emails -# By default, SMTP_HOST is set to smtp.gmail.com, SMTP_PORT is set to 587, SMTP_TLS is set to false, and SMTP_FROM_NAME is set to Infisical -SMTP_HOST=smtp.gmail.com -# If STARTTLS is supported, the connection will be upgraded to TLS when SMTP_SECURE is set to false -SMTP_SECURE=false +SMTP_HOST= # required +SMTP_USERNAME= # required +SMTP_PASSWORD= # required SMTP_PORT=587 -SMTP_USERNAME= -SMTP_PASSWORD= -SMTP_FROM_ADDRESS= +SMTP_SECURE=false +SMTP_FROM_ADDRESS= # required SMTP_FROM_NAME=Infisical # Integration diff --git a/.github/workflows/release_build.yml b/.github/workflows/release_build.yml index 695b0ea24..da2a29435 100644 --- a/.github/workflows/release_build.yml +++ b/.github/workflows/release_build.yml @@ -13,7 +13,7 @@ permissions: jobs: goreleaser: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 with: @@ -24,6 +24,15 @@ jobs: go-version: '>=1.19.3' cache: true cache-dependency-path: cli/go.sum + - name: libssl1.1 => libssl1.0-dev for OSXCross + run: | + echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list + sudo apt update && apt-cache policy libssl1.0-dev + sudo apt-get install libssl1.0-dev + - name: OSXCross for CGO Support + run: | + mkdir ../../osxcross + git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target - uses: goreleaser/goreleaser-action@v2 with: distribution: goreleaser @@ -32,6 +41,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }} + AUR_KEY: ${{ secrets.AUR_KEY }} - uses: actions/setup-python@v4 - run: pip install --upgrade cloudsmith-cli - name: Publish to CloudSmith diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 9ac38524b..0b348b71a 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -7,28 +7,37 @@ # # you may remove this if you don't need go generate # - cd cli && go generate ./... builds: - - env: - - CGO_ENABLED=0 + - id: darwin-build binary: infisical - id: infisical + env: + - CGO_ENABLED=1 + - CC=/home/runner/work/osxcross/target/bin/o64-clang + - CXX=/home/runner/work/osxcross/target/bin/o64-clang++ goos: - darwin + ignore: + - goos: darwin + goarch: "386" + dir: ./cli + - id: all-other-builds + env: + - CGO_ENABLED=0 + binary: infisical + goos: - freebsd - linux - netbsd - openbsd - windows goarch: - - 386 + - "386" - amd64 - arm - arm64 goarm: - - 6 - - 7 + - "6" + - "7" ignore: - - goos: darwin - goarch: "386" - goos: windows goarch: "386" - goos: freebsd @@ -71,12 +80,12 @@ nfpms: - id: infisical package_name: infisical builds: - - infisical + - all-other-builds vendor: Infisical, Inc homepage: https://infisical.com/ maintainer: Infisical, Inc description: The offical Infisical CLI - license: Apache 2.0 + license: MIT formats: - rpm - deb @@ -92,7 +101,23 @@ scoop: email: ai@infisical.com homepage: "https://infisical.com" description: "The official Infisical CLI" - license: Apache-2.0 + license: MIT +aurs: + - + name: infisical-bin + homepage: "https://infisical.com" + description: "The official Infisical CLI" + maintainers: + - Infisical, Inc + license: MIT + private_key: '{{ .Env.AUR_KEY }}' + git_url: 'ssh://aur@aur.archlinux.org/infisical-bin.git' + package: |- + # bin + install -Dm755 "./infisical" "${pkgdir}/usr/bin/infisical" + # license + install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/infisical/LICENSE" + # dockers: # - dockerfile: goreleaser.dockerfile # goos: linux diff --git a/README.md b/README.md index 91bb67d5c..ac2f2a5b5 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ - **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure - **Navigate Multiple Environments** per project (e.g. development, staging, production, etc.) - **Personal/Shared** scoping for environment variables -- **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure (Heroku available, more coming soon) +- **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure - ๐Ÿ”œ **1-Click Deploy** to Digital Ocean and Heroku - ๐Ÿ”œ **Authentication/Authorization** for projects (read/write controls soon) - ๐Ÿ”œ **Automatic Secret Rotation** @@ -321,4 +321,10 @@ Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of - + + +## ๐ŸŒŽ Translations + +Infisical is currently aviable in English and Korean. Help us translate Infisical to your language! + +You can find all the info in [this issue](https://github.com/Infisical/infisical/issues/181). \ No newline at end of file diff --git a/backend/environment.d.ts b/backend/environment.d.ts index 33034fdd5..291a33550 100644 --- a/backend/environment.d.ts +++ b/backend/environment.d.ts @@ -14,6 +14,8 @@ declare global { JWT_SIGNUP_SECRET: string; MONGO_URL: string; NODE_ENV: 'development' | 'staging' | 'testing' | 'production'; + VERBOSE_ERROR_OUTPUT: string; + LOKI_HOST: string; CLIENT_ID_HEROKU: string; CLIENT_ID_VERCEL: string; CLIENT_ID_NETLIFY: string; diff --git a/backend/package-lock.json b/backend/package-lock.json index 42c8205bd..85b168c1e 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -37,7 +37,9 @@ "stripe": "^10.7.0", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", - "typescript": "^4.9.3" + "typescript": "^4.9.3", + "winston": "^3.8.2", + "winston-loki": "^6.0.6" }, "devDependencies": { "@jest/globals": "^29.3.1", @@ -2003,6 +2005,14 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -2015,6 +2025,16 @@ "node": ">=12" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@eslint/eslintrc": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", @@ -2567,6 +2587,21 @@ "maxmind": "^4.2.0" } }, + "node_modules/@napi-rs/snappy-darwin-arm64": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-darwin-arm64/-/snappy-darwin-arm64-7.1.1.tgz", + "integrity": "sha512-3LZyoAw3Qa5F7sCCTkSkhmGlydwUKU6L3Jl46eKHO2Ctm8Gcjxww6T7MfwlwGZ6JqAM6d1d++WLzUZPCGXVmag==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2758,6 +2793,60 @@ "@maxmind/geoip2-node": "^3.4.0" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "node_modules/@sentry/node": { "version": "7.19.0", "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.19.0.tgz", @@ -3075,6 +3164,11 @@ "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", "integrity": "sha512-BqI9B92u+cM3ccp8mpHf+HzJ8fBlRwdmyd6+fz3p99m3V6ifT5O3zmOMi612PGkpeFeG/G6loxUnzlDNhfjPSA==" }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -3095,6 +3189,12 @@ "@types/node": "*" } }, + "node_modules/@types/prettier": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", + "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", + "dev": true + }, "node_modules/@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -3560,6 +3660,19 @@ "node": ">=0.8" } }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3835,6 +3948,17 @@ "node": ">=6.9.0" } }, + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -4059,6 +4183,15 @@ "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", "dev": true }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4074,8 +4207,38 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } }, "node_modules/combined-stream": { "version": "1.0.8", @@ -4425,6 +4588,11 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -4921,6 +5089,11 @@ "bser": "2.1.1" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -5018,6 +5191,11 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, "node_modules/follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -5604,7 +5782,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "engines": { "node": ">=8" }, @@ -6152,6 +6329,7 @@ "@jest/transform": "^29.3.1", "@jest/types": "^29.3.1", "@types/babel__traverse": "^7.0.6", + "@types/prettier": "^2.1.5", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", "expect": "^29.3.1", @@ -6403,6 +6581,11 @@ "node": ">=6" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -6476,6 +6659,23 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/logform": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz", + "integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==", + "dependencies": { + "@colors/colors": "1.5.0", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + } + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "node_modules/lru_map": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", @@ -6939,6 +7139,9 @@ }, "bin": { "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" } }, "node_modules/normalize-path": { @@ -9563,6 +9766,14 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -9810,9 +10021,9 @@ } }, "node_modules/posthog-node": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.2.2.tgz", - "integrity": "sha512-aXYe/D+28kF63W8Cz53t09ypEORz+ULeDCahdAqhVrRm2scbOXFbtnn0GGhvMpYe45grepLKuwui9KxrZ2ZuMw==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.2.3.tgz", + "integrity": "sha512-dYlLZhrDus+uRov/Hh+EiRlMoMhRKchNjNa7mNE2iWmKg/ryOTipf0XYKS9UKdki7aU1NzWFhnLe11HF615XuA==", "dependencies": { "axios": "^0.27.0" }, @@ -9877,6 +10088,31 @@ "node": ">= 6" } }, + "node_modules/protobufjs": { + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", + "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -10184,6 +10420,14 @@ } ] }, + "node_modules/safe-stable-stringify": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.1.tgz", + "integrity": "sha512-dVHE6bMtS/bnL2mwualjc6IxEv1F+OCUpA46pKUj6F8uDbUM0jCCulPqRNPSnWwGNKx5etqMjZYdXtrm5KJZGA==", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -10332,6 +10576,19 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "node_modules/simple-update-notifier": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz", @@ -10377,6 +10634,34 @@ "npm": ">= 3.0.0" } }, + "node_modules/snappy": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/snappy/-/snappy-7.1.1.tgz", + "integrity": "sha512-mL7GGPJ+WdsaFT5aR/uEqCq8cPg2VbhyifDEP7AeqIVDsAC8LBGYbZP1Qzoa2Ym84OW7JEQXqIpwqFp1EQw5BA==", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/snappy-android-arm-eabi": "7.1.1", + "@napi-rs/snappy-android-arm64": "7.1.1", + "@napi-rs/snappy-darwin-arm64": "7.1.1", + "@napi-rs/snappy-darwin-x64": "7.1.1", + "@napi-rs/snappy-freebsd-x64": "7.1.1", + "@napi-rs/snappy-linux-arm-gnueabihf": "7.1.1", + "@napi-rs/snappy-linux-arm64-gnu": "7.1.1", + "@napi-rs/snappy-linux-arm64-musl": "7.1.1", + "@napi-rs/snappy-linux-x64-gnu": "7.1.1", + "@napi-rs/snappy-linux-x64-musl": "7.1.1", + "@napi-rs/snappy-win32-arm64-msvc": "7.1.1", + "@napi-rs/snappy-win32-ia32-msvc": "7.1.1", + "@napi-rs/snappy-win32-x64-msvc": "7.1.1" + } + }, "node_modules/socks": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", @@ -10431,6 +10716,14 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -10656,6 +10949,11 @@ "node": ">=8" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -10729,6 +11027,11 @@ "node": ">=12" } }, + "node_modules/triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, "node_modules/ts-jest": { "version": "29.0.3", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.3.tgz", @@ -11100,6 +11403,54 @@ "node": ">= 8" } }, + "node_modules/winston": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", + "integrity": "sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==", + "dependencies": { + "@colors/colors": "1.5.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.4.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.5.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-loki": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/winston-loki/-/winston-loki-6.0.6.tgz", + "integrity": "sha512-cll+nv5T/b9uJXqca0N2WKL1JJNuJND9E6WOOAuSGkZ44L9VQ/QK9F+/5VKbv6LIP9p0nvPSOYxtACCDb/9iWw==", + "dependencies": { + "async-exit-hook": "2.0.1", + "btoa": "^1.2.1", + "protobufjs": "^6.8.8", + "winston-transport": "^4.3.0" + }, + "optionalDependencies": { + "snappy": "7.1.1" + } + }, + "node_modules/winston-transport": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", + "dependencies": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, "node_modules/word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -12957,6 +13308,11 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" + }, "@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -12966,6 +13322,16 @@ "@jridgewell/trace-mapping": "0.3.9" } }, + "@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "requires": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "@eslint/eslintrc": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", @@ -13413,6 +13779,12 @@ "maxmind": "^4.2.0" } }, + "@napi-rs/snappy-darwin-arm64": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-darwin-arm64/-/snappy-darwin-arm64-7.1.1.tgz", + "integrity": "sha512-3LZyoAw3Qa5F7sCCTkSkhmGlydwUKU6L3Jl46eKHO2Ctm8Gcjxww6T7MfwlwGZ6JqAM6d1d++WLzUZPCGXVmag==", + "optional": true + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -13560,6 +13932,60 @@ "@maxmind/geoip2-node": "^3.4.0" } }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "@sentry/node": { "version": "7.19.0", "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.19.0.tgz", @@ -13857,6 +14283,11 @@ "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", "integrity": "sha512-BqI9B92u+cM3ccp8mpHf+HzJ8fBlRwdmyd6+fz3p99m3V6ifT5O3zmOMi612PGkpeFeG/G6loxUnzlDNhfjPSA==" }, + "@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, "@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -13877,6 +14308,12 @@ "@types/node": "*" } }, + "@types/prettier": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", + "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", + "dev": true + }, "@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -14204,6 +14641,16 @@ "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "dev": true }, + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, + "async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -14414,6 +14861,11 @@ "buffer": "^5.6.0" } }, + "btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==" + }, "buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -14565,6 +15017,30 @@ "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", "dev": true }, + "color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "requires": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + } + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -14577,8 +15053,25 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "requires": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } }, "combined-stream": { "version": "1.0.8", @@ -14846,6 +15339,11 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -15233,6 +15731,11 @@ "bser": "2.1.1" } }, + "fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -15311,6 +15814,11 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, "follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -15711,8 +16219,7 @@ "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" }, "isexe": { "version": "2.0.0", @@ -16129,6 +16636,7 @@ "@jest/transform": "^29.3.1", "@jest/types": "^29.3.1", "@types/babel__traverse": "^7.0.6", + "@types/prettier": "^2.1.5", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", "expect": "^29.3.1", @@ -16336,6 +16844,11 @@ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true }, + "kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -16397,6 +16910,23 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "logform": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz", + "integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==", + "requires": { + "@colors/colors": "1.5.0", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + } + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "lru_map": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", @@ -18578,6 +19108,14 @@ "wrappy": "1" } }, + "one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "requires": { + "fn.name": "1.x.x" + } + }, "onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -18752,9 +19290,9 @@ } }, "posthog-node": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.2.2.tgz", - "integrity": "sha512-aXYe/D+28kF63W8Cz53t09ypEORz+ULeDCahdAqhVrRm2scbOXFbtnn0GGhvMpYe45grepLKuwui9KxrZ2ZuMw==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.2.3.tgz", + "integrity": "sha512-dYlLZhrDus+uRov/Hh+EiRlMoMhRKchNjNa7mNE2iWmKg/ryOTipf0XYKS9UKdki7aU1NzWFhnLe11HF615XuA==", "requires": { "axios": "^0.27.0" }, @@ -18805,6 +19343,26 @@ "sisteransi": "^1.0.5" } }, + "protobufjs": { + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", + "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + } + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -18999,6 +19557,11 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, + "safe-stable-stringify": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.1.tgz", + "integrity": "sha512-dVHE6bMtS/bnL2mwualjc6IxEv1F+OCUpA46pKUj6F8uDbUM0jCCulPqRNPSnWwGNKx5etqMjZYdXtrm5KJZGA==" + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -19124,6 +19687,21 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + } + } + }, "simple-update-notifier": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz", @@ -19158,6 +19736,27 @@ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" }, + "snappy": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/snappy/-/snappy-7.1.1.tgz", + "integrity": "sha512-mL7GGPJ+WdsaFT5aR/uEqCq8cPg2VbhyifDEP7AeqIVDsAC8LBGYbZP1Qzoa2Ym84OW7JEQXqIpwqFp1EQw5BA==", + "optional": true, + "requires": { + "@napi-rs/snappy-android-arm-eabi": "7.1.1", + "@napi-rs/snappy-android-arm64": "7.1.1", + "@napi-rs/snappy-darwin-arm64": "7.1.1", + "@napi-rs/snappy-darwin-x64": "7.1.1", + "@napi-rs/snappy-freebsd-x64": "7.1.1", + "@napi-rs/snappy-linux-arm-gnueabihf": "7.1.1", + "@napi-rs/snappy-linux-arm64-gnu": "7.1.1", + "@napi-rs/snappy-linux-arm64-musl": "7.1.1", + "@napi-rs/snappy-linux-x64-gnu": "7.1.1", + "@napi-rs/snappy-linux-x64-musl": "7.1.1", + "@napi-rs/snappy-win32-arm64-msvc": "7.1.1", + "@napi-rs/snappy-win32-ia32-msvc": "7.1.1", + "@napi-rs/snappy-win32-x64-msvc": "7.1.1" + } + }, "socks": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", @@ -19202,6 +19801,11 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==" + }, "stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -19367,6 +19971,11 @@ "minimatch": "^3.0.4" } }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -19422,6 +20031,11 @@ "punycode": "^2.1.1" } }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, "ts-jest": { "version": "29.0.3", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.3.tgz", @@ -19663,6 +20277,46 @@ "isexe": "^2.0.0" } }, + "winston": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", + "integrity": "sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==", + "requires": { + "@colors/colors": "1.5.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.4.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.5.0" + } + }, + "winston-loki": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/winston-loki/-/winston-loki-6.0.6.tgz", + "integrity": "sha512-cll+nv5T/b9uJXqca0N2WKL1JJNuJND9E6WOOAuSGkZ44L9VQ/QK9F+/5VKbv6LIP9p0nvPSOYxtACCDb/9iWw==", + "requires": { + "async-exit-hook": "2.0.1", + "btoa": "^1.2.1", + "protobufjs": "^6.8.8", + "snappy": "7.1.1", + "winston-transport": "^4.3.0" + } + }, + "winston-transport": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", + "requires": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + } + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", diff --git a/backend/package.json b/backend/package.json index 266bb7b39..d1a03a74c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -28,7 +28,9 @@ "stripe": "^10.7.0", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", - "typescript": "^4.9.3" + "typescript": "^4.9.3", + "winston": "^3.8.2", + "winston-loki": "^6.0.6" }, "name": "infisical-api", "version": "1.0.0", diff --git a/backend/src/app.ts b/backend/src/app.ts index fa11b5f1c..8320a7d76 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,4 +1,5 @@ -/* eslint-disable no-console */ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { patchRouterParam } = require('./utils/patchAsyncRoutes'); import express from 'express'; import helmet from 'helmet'; @@ -11,24 +12,39 @@ import { PORT, NODE_ENV, SITE_URL } from './config'; import { apiLimiter } from './helpers/rateLimiter'; import { - signup as signupRouter, - auth as authRouter, - bot as botRouter, - organization as organizationRouter, - workspace as workspaceRouter, - membershipOrg as membershipOrgRouter, - membership as membershipRouter, - key as keyRouter, - inviteOrg as inviteOrgRouter, - user as userRouter, - userAction as userActionRouter, - secret as secretRouter, - serviceToken as serviceTokenRouter, - password as passwordRouter, - stripe as stripeRouter, - integration as integrationRouter, - integrationAuth as integrationAuthRouter -} from './routes'; + workspace as eeWorkspaceRouter, + secret as eeSecretRouter +} from './ee/routes/v1'; +import { + signup as v1SignupRouter, + auth as v1AuthRouter, + bot as v1BotRouter, + organization as v1OrganizationRouter, + workspace as v1WorkspaceRouter, + membershipOrg as v1MembershipOrgRouter, + membership as v1MembershipRouter, + key as v1KeyRouter, + inviteOrg as v1InviteOrgRouter, + user as v1UserRouter, + userAction as v1UserActionRouter, + secret as v1SecretRouter, + serviceToken as v1ServiceTokenRouter, + password as v1PasswordRouter, + stripe as v1StripeRouter, + integration as v1IntegrationRouter, + integrationAuth as v1IntegrationAuthRouter +} from './routes/v1'; +import { + secret as v2SecretRouter, + workspace as v2WorkspaceRouter +} from './routes/v2'; + +import { getLogger } from './utils/logger'; +import { RouteNotFoundError } from './utils/errors'; +import { requestErrorHandler } from './middleware/requestErrorHandler'; + +// patch async route params to handle Promise Rejections +patchRouterParam(); export const app = express(); @@ -50,25 +66,44 @@ if (NODE_ENV === 'production') { app.use(helmet()); } -// routers -app.use('/api/v1/signup', signupRouter); -app.use('/api/v1/auth', authRouter); -app.use('/api/v1/bot', botRouter); -app.use('/api/v1/user', userRouter); -app.use('/api/v1/user-action', userActionRouter); -app.use('/api/v1/organization', organizationRouter); -app.use('/api/v1/workspace', workspaceRouter); -app.use('/api/v1/membership-org', membershipOrgRouter); -app.use('/api/v1/membership', membershipRouter); -app.use('/api/v1/key', keyRouter); -app.use('/api/v1/invite-org', inviteOrgRouter); -app.use('/api/v1/secret', secretRouter); -app.use('/api/v1/service-token', serviceTokenRouter); -app.use('/api/v1/password', passwordRouter); -app.use('/api/v1/stripe', stripeRouter); -app.use('/api/v1/integration', integrationRouter); -app.use('/api/v1/integration-auth', integrationAuthRouter); +// (EE) routes +app.use('/api/v1/secret', eeSecretRouter); +app.use('/api/v1/workspace', eeWorkspaceRouter); + +// v1 routes +app.use('/api/v1/signup', v1SignupRouter); +app.use('/api/v1/auth', v1AuthRouter); +app.use('/api/v1/bot', v1BotRouter); +app.use('/api/v1/user', v1UserRouter); +app.use('/api/v1/user-action', v1UserActionRouter); +app.use('/api/v1/organization', v1OrganizationRouter); +app.use('/api/v1/workspace', v1WorkspaceRouter); +app.use('/api/v1/membership-org', v1MembershipOrgRouter); +app.use('/api/v1/membership', v1MembershipRouter); +app.use('/api/v1/key', v1KeyRouter); +app.use('/api/v1/invite-org', v1InviteOrgRouter); +app.use('/api/v1/secret', v1SecretRouter); +app.use('/api/v1/service-token', v1ServiceTokenRouter); +app.use('/api/v1/password', v1PasswordRouter); +app.use('/api/v1/stripe', v1StripeRouter); +app.use('/api/v1/integration', v1IntegrationRouter); +app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); + +// v2 routes +app.use('/api/v2/workspace', v2WorkspaceRouter); +app.use('/api/v2/secret', v2SecretRouter); + + +//* Handle unrouted requests and respond with proper error message as well as status code +app.use((req, res, next)=>{ + if(res.headersSent) return next(); + next(RouteNotFoundError({message: `The requested source '(${req.method})${req.url}' was not found`})) +}) + +//* Error Handling Middleware (must be after all routing logic) +app.use(requestErrorHandler) + export const server = app.listen(PORT, () => { - console.log(`Listening on PORT ${[PORT]}`); + getLogger("backend-main").info(`Server started listening at port ${PORT}`) }); diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 43a9026ac..3fb475099 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,6 +10,8 @@ const JWT_SIGNUP_LIFETIME = process.env.JWT_SIGNUP_LIFETIME! || '15m'; const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!; const MONGO_URL = process.env.MONGO_URL!; const NODE_ENV = process.env.NODE_ENV! || 'production'; +const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true; +const LOKI_HOST = process.env.LOKI_HOST || undefined; const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!; const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!; const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!; @@ -25,9 +27,9 @@ const POSTHOG_PROJECT_API_KEY = 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE'; const SENTRY_DSN = process.env.SENTRY_DSN!; const SITE_URL = process.env.SITE_URL!; -const SMTP_HOST = process.env.SMTP_HOST! || 'smtp.gmail.com'; -const SMTP_SECURE = process.env.SMTP_SECURE! || false; -const SMTP_PORT = process.env.SMTP_PORT! || 587; +const SMTP_HOST = process.env.SMTP_HOST!; +const SMTP_SECURE = process.env.SMTP_SECURE! === 'true' || false; +const SMTP_PORT = parseInt(process.env.SMTP_PORT!) || 587; const SMTP_USERNAME = process.env.SMTP_USERNAME!; const SMTP_PASSWORD = process.env.SMTP_PASSWORD!; const SMTP_FROM_ADDRESS = process.env.SMTP_FROM_ADDRESS!; @@ -39,6 +41,7 @@ const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY!; const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY!; const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!; const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED! !== 'false' && true; +const LICENSE_KEY = process.env.LICENSE_KEY!; export { PORT, @@ -53,6 +56,8 @@ export { JWT_SIGNUP_SECRET, MONGO_URL, NODE_ENV, + VERBOSE_ERROR_OUTPUT, + LOKI_HOST, CLIENT_ID_HEROKU, CLIENT_ID_VERCEL, CLIENT_ID_NETLIFY, @@ -79,5 +84,6 @@ export { STRIPE_PUBLISHABLE_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, - TELEMETRY_ENABLED + TELEMETRY_ENABLED, + LICENSE_KEY }; diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/v1/authController.ts similarity index 97% rename from backend/src/controllers/authController.ts rename to backend/src/controllers/v1/authController.ts index 20ac813d4..defd03d8a 100644 --- a/backend/src/controllers/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -4,14 +4,14 @@ import jwt from 'jsonwebtoken'; import * as Sentry from '@sentry/node'; import * as bigintConversion from 'bigint-conversion'; const jsrp = require('jsrp'); -import { User } from '../models'; -import { createToken, issueTokens, clearTokens } from '../helpers/auth'; +import { User } from '../../models'; +import { createToken, issueTokens, clearTokens } from '../../helpers/auth'; import { NODE_ENV, JWT_AUTH_LIFETIME, JWT_AUTH_SECRET, JWT_REFRESH_SECRET -} from '../config'; +} from '../../config'; declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { diff --git a/backend/src/controllers/botController.ts b/backend/src/controllers/v1/botController.ts similarity index 96% rename from backend/src/controllers/botController.ts rename to backend/src/controllers/v1/botController.ts index 7819e32df..ab86897cb 100644 --- a/backend/src/controllers/botController.ts +++ b/backend/src/controllers/v1/botController.ts @@ -1,7 +1,7 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { Bot, BotKey } from '../models'; -import { createBot } from '../helpers/bot'; +import { Bot, BotKey } from '../../models'; +import { createBot } from '../../helpers/bot'; interface BotKey { encryptedKey: string; diff --git a/backend/src/controllers/index.ts b/backend/src/controllers/v1/index.ts similarity index 100% rename from backend/src/controllers/index.ts rename to backend/src/controllers/v1/index.ts diff --git a/backend/src/controllers/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts similarity index 92% rename from backend/src/controllers/integrationAuthController.ts rename to backend/src/controllers/v1/integrationAuthController.ts index c242c239a..95d0066ae 100644 --- a/backend/src/controllers/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -2,10 +2,10 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; import axios from 'axios'; import { readFileSync } from 'fs'; -import { IntegrationAuth, Integration } from '../models'; -import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../variables'; -import { IntegrationService } from '../services'; -import { getApps, revokeAccess } from '../integrations'; +import { IntegrationAuth, Integration } from '../../models'; +import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../../variables'; +import { IntegrationService } from '../../services'; +import { getApps, revokeAccess } from '../../integrations'; export const getIntegrationOptions = async ( req: Request, diff --git a/backend/src/controllers/integrationController.ts b/backend/src/controllers/v1/integrationController.ts similarity index 94% rename from backend/src/controllers/integrationController.ts rename to backend/src/controllers/v1/integrationController.ts index 910c7e825..c05794959 100644 --- a/backend/src/controllers/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -1,9 +1,9 @@ import { Request, Response } from 'express'; import { readFileSync } from 'fs'; import * as Sentry from '@sentry/node'; -import { Integration, Bot, BotKey } from '../models'; -import { EventService } from '../services'; -import { eventPushSecrets } from '../events'; +import { Integration, Bot, BotKey } from '../../models'; +import { EventService } from '../../services'; +import { eventPushSecrets } from '../../events'; interface Key { encryptedKey: string; diff --git a/backend/src/controllers/keyController.ts b/backend/src/controllers/v1/keyController.ts similarity index 93% rename from backend/src/controllers/keyController.ts rename to backend/src/controllers/v1/keyController.ts index 70446a76c..332215894 100644 --- a/backend/src/controllers/keyController.ts +++ b/backend/src/controllers/v1/keyController.ts @@ -1,8 +1,8 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { Key } from '../models'; -import { findMembership } from '../helpers/membership'; -import { GRANTED } from '../variables'; +import { Key } from '../../models'; +import { findMembership } from '../../helpers/membership'; +import { GRANTED } from '../../variables'; /** * Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with diff --git a/backend/src/controllers/membershipController.ts b/backend/src/controllers/v1/membershipController.ts similarity index 95% rename from backend/src/controllers/membershipController.ts rename to backend/src/controllers/v1/membershipController.ts index f2ed3db6e..187e8127c 100644 --- a/backend/src/controllers/membershipController.ts +++ b/backend/src/controllers/v1/membershipController.ts @@ -1,13 +1,13 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { Membership, MembershipOrg, User, Key } from '../models'; +import { Membership, MembershipOrg, User, Key } from '../../models'; import { findMembership, deleteMembership as deleteMember -} from '../helpers/membership'; -import { sendMail } from '../helpers/nodemailer'; -import { SITE_URL } from '../config'; -import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../variables'; +} from '../../helpers/membership'; +import { sendMail } from '../../helpers/nodemailer'; +import { SITE_URL } from '../../config'; +import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../../variables'; /** * Check that user is a member of workspace with id [workspaceId] diff --git a/backend/src/controllers/membershipOrgController.ts b/backend/src/controllers/v1/membershipOrgController.ts similarity index 86% rename from backend/src/controllers/membershipOrgController.ts rename to backend/src/controllers/v1/membershipOrgController.ts index a2159bcd0..5628cda1a 100644 --- a/backend/src/controllers/membershipOrgController.ts +++ b/backend/src/controllers/v1/membershipOrgController.ts @@ -1,14 +1,14 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; import crypto from 'crypto'; -import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config'; -import { MembershipOrg, Organization, User, Token } from '../models'; -import { deleteMembershipOrg as deleteMemberFromOrg } from '../helpers/membershipOrg'; -import { checkEmailVerification } from '../helpers/signup'; -import { createToken } from '../helpers/auth'; -import { updateSubscriptionOrgQuantity } from '../helpers/organization'; -import { sendMail } from '../helpers/nodemailer'; -import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../variables'; +import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config'; +import { MembershipOrg, Organization, User, Token } from '../../models'; +import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg'; +import { checkEmailVerification } from '../../helpers/signup'; +import { createToken } from '../../helpers/auth'; +import { updateSubscriptionOrgQuantity } from '../../helpers/organization'; +import { sendMail } from '../../helpers/nodemailer'; +import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../../variables'; /** * Delete organization membership with id [membershipOrgId] from organization @@ -80,14 +80,14 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => { // TODO let membershipToChangeRole; - try { - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to change organization membership role' - }); - } + // try { + // } catch (err) { + // Sentry.setUser({ email: req.user.email }); + // Sentry.captureException(err); + // return res.status(400).send({ + // message: 'Failed to change organization membership role' + // }); + // } return res.status(200).send({ membershipOrg: membershipToChangeRole @@ -218,12 +218,6 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => { const { email, code } = req.body; user = await User.findOne({ email }).select('+publicKey'); - if (user && user?.publicKey) { - // case: user has already completed account - return res.status(403).send({ - error: 'Failed email magic link verification for complete account' - }); - } const membershipOrg = await MembershipOrg.findOne({ inviteEmail: email, @@ -238,6 +232,18 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => { code }); + if (user && user?.publicKey) { + // case: user has already completed account + // membership can be approved and redirected to login/dashboard + membershipOrg.status = ACCEPTED; + await membershipOrg.save(); + + return res.status(200).send({ + message: 'Successfully verified email', + user, + }); + } + if (!user) { // initialize user account user = await new User({ diff --git a/backend/src/controllers/organizationController.ts b/backend/src/controllers/v1/organizationController.ts similarity index 97% rename from backend/src/controllers/organizationController.ts rename to backend/src/controllers/v1/organizationController.ts index 056990acc..44e140b4e 100644 --- a/backend/src/controllers/organizationController.ts +++ b/backend/src/controllers/v1/organizationController.ts @@ -6,7 +6,7 @@ import { STRIPE_PRODUCT_STARTER, STRIPE_PRODUCT_PRO, STRIPE_PRODUCT_CARD_AUTH -} from '../config'; +} from '../../config'; import Stripe from 'stripe'; const stripe = new Stripe(STRIPE_SECRET_KEY, { @@ -18,10 +18,10 @@ import { Organization, Workspace, IncidentContactOrg -} from '../models'; -import { createOrganization as create } from '../helpers/organization'; -import { addMembershipsOrg } from '../helpers/membershipOrg'; -import { OWNER, ACCEPTED } from '../variables'; +} from '../../models'; +import { createOrganization as create } from '../../helpers/organization'; +import { addMembershipsOrg } from '../../helpers/membershipOrg'; +import { OWNER, ACCEPTED } from '../../variables'; const productToPriceMap = { starter: STRIPE_PRODUCT_STARTER, diff --git a/backend/src/controllers/passwordController.ts b/backend/src/controllers/v1/passwordController.ts similarity index 96% rename from backend/src/controllers/passwordController.ts rename to backend/src/controllers/v1/passwordController.ts index b029bc0be..27d712a6b 100644 --- a/backend/src/controllers/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -1,13 +1,14 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; import crypto from 'crypto'; +// eslint-disable-next-line @typescript-eslint/no-var-requires const jsrp = require('jsrp'); import * as bigintConversion from 'bigint-conversion'; -import { User, Token, BackupPrivateKey } from '../models'; -import { checkEmailVerification } from '../helpers/signup'; -import { createToken } from '../helpers/auth'; -import { sendMail } from '../helpers/nodemailer'; -import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../config'; +import { User, Token, BackupPrivateKey } from '../../models'; +import { checkEmailVerification } from '../../helpers/signup'; +import { createToken } from '../../helpers/auth'; +import { sendMail } from '../../helpers/nodemailer'; +import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config'; const clientPublicKeys: any = {}; diff --git a/backend/src/controllers/secretController.ts b/backend/src/controllers/v1/secretController.ts similarity index 91% rename from backend/src/controllers/secretController.ts rename to backend/src/controllers/v1/secretController.ts index bfd9aee1f..238b38ced 100644 --- a/backend/src/controllers/secretController.ts +++ b/backend/src/controllers/v1/secretController.ts @@ -1,16 +1,16 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { Key } from '../models'; +import { Key, Secret } from '../../models'; import { - pushSecrets as push, + v1PushSecrets as push, pullSecrets as pull, reformatPullSecrets -} from '../helpers/secret'; -import { pushKeys } from '../helpers/key'; -import { eventPushSecrets } from '../events'; -import { EventService } from '../services'; -import { ENV_SET } from '../variables'; -import { postHogClient } from '../services'; +} from '../../helpers/secret'; +import { pushKeys } from '../../helpers/key'; +import { eventPushSecrets } from '../../events'; +import { EventService } from '../../services'; +import { ENV_SET } from '../../variables'; +import { postHogClient } from '../../services'; interface PushSecret { ciphertextKey: string; @@ -21,6 +21,10 @@ interface PushSecret { ivValue: string; tagValue: string; hashValue: string; + ciphertextComment: string; + ivComment: string; + tagComment: string; + hashComment: string; type: 'shared' | 'personal'; } @@ -169,9 +173,6 @@ export const pullSecrets = async (req: Request, res: Response) => { * @returns */ export const pullSecretsServiceToken = async (req: Request, res: Response) => { - // get (encrypted) secrets from workspace with id [workspaceId] - // service token route - let secrets; let key; try { @@ -225,4 +226,4 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => { secrets: reformatPullSecrets({ secrets }), key }); -}; +}; \ No newline at end of file diff --git a/backend/src/controllers/serviceTokenController.ts b/backend/src/controllers/v1/serviceTokenController.ts similarity index 87% rename from backend/src/controllers/serviceTokenController.ts rename to backend/src/controllers/v1/serviceTokenController.ts index 4cc53c4f9..e21cd7695 100644 --- a/backend/src/controllers/serviceTokenController.ts +++ b/backend/src/controllers/v1/serviceTokenController.ts @@ -1,8 +1,8 @@ import { Request, Response } from 'express'; -import { ServiceToken } from '../models'; -import { createToken } from '../helpers/auth'; -import { ENV_SET } from '../variables'; -import { JWT_SERVICE_SECRET } from '../config'; +import { ServiceToken } from '../../models'; +import { createToken } from '../../helpers/auth'; +import { ENV_SET } from '../../variables'; +import { JWT_SERVICE_SECRET } from '../../config'; /** * Return service token on request diff --git a/backend/src/controllers/signupController.ts b/backend/src/controllers/v1/signupController.ts similarity index 89% rename from backend/src/controllers/signupController.ts rename to backend/src/controllers/v1/signupController.ts index 90fad4744..62e5a62a3 100644 --- a/backend/src/controllers/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -1,15 +1,16 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config'; -import { User, MembershipOrg } from '../models'; -import { completeAccount } from '../helpers/user'; +import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config'; +import { User, MembershipOrg } from '../../models'; +import { completeAccount } from '../../helpers/user'; import { sendEmailVerification, checkEmailVerification, initializeDefaultOrg -} from '../helpers/signup'; -import { issueTokens, createToken } from '../helpers/auth'; -import { INVITED, ACCEPTED } from '../variables'; +} from '../../helpers/signup'; +import { issueTokens, createToken } from '../../helpers/auth'; +import { INVITED, ACCEPTED } from '../../variables'; +import axios from 'axios'; /** * Signup step 1: Initialize account for user under email [email] and send a verification code @@ -179,6 +180,21 @@ export const completeAccountSignup = async (req: Request, res: Response) => { token = tokens.token; refreshToken = tokens.refreshToken; + + // sending a welcome email to new users + if (process.env.LOOPS_API_KEY) { + await axios.post("https://app.loops.so/api/v1/events/send", { + "email": email, + "eventName": "Sign Up", + "firstName": firstName, + "lastName": lastName + }, { + headers: { + "Accept": "application/json", + "Authorization": "Bearer " + process.env.LOOPS_API_KEY + }, + }); + } } catch (err) { Sentry.setUser(null); Sentry.captureException(err); diff --git a/backend/src/ee/controllers/stripeController.ts b/backend/src/controllers/v1/stripeController.ts similarity index 100% rename from backend/src/ee/controllers/stripeController.ts rename to backend/src/controllers/v1/stripeController.ts diff --git a/backend/src/controllers/userActionController.ts b/backend/src/controllers/v1/userActionController.ts similarity index 97% rename from backend/src/controllers/userActionController.ts rename to backend/src/controllers/v1/userActionController.ts index 8203aa427..c7a3c2337 100644 --- a/backend/src/controllers/userActionController.ts +++ b/backend/src/controllers/v1/userActionController.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { UserAction } from '../models'; +import { UserAction } from '../../models'; /** * Add user action [action] diff --git a/backend/src/controllers/userController.ts b/backend/src/controllers/v1/userController.ts similarity index 100% rename from backend/src/controllers/userController.ts rename to backend/src/controllers/v1/userController.ts diff --git a/backend/src/controllers/workspaceController.ts b/backend/src/controllers/v1/workspaceController.ts similarity index 97% rename from backend/src/controllers/workspaceController.ts rename to backend/src/controllers/v1/workspaceController.ts index a402834d7..bd2ecb15a 100644 --- a/backend/src/controllers/workspaceController.ts +++ b/backend/src/controllers/v1/workspaceController.ts @@ -7,14 +7,14 @@ import { Integration, IntegrationAuth, IUser, - ServiceToken -} from '../models'; + ServiceToken, +} from '../../models'; import { createWorkspace as create, deleteWorkspace as deleteWork -} from '../helpers/workspace'; -import { addMemberships } from '../helpers/membership'; -import { ADMIN, COMPLETED, GRANTED } from '../variables'; +} from '../../helpers/workspace'; +import { addMemberships } from '../../helpers/membership'; +import { ADMIN, COMPLETED, GRANTED } from '../../variables'; /** * Return public keys of members of workspace with id [workspaceId] diff --git a/backend/src/controllers/v2/index.ts b/backend/src/controllers/v2/index.ts new file mode 100644 index 000000000..dc6977c91 --- /dev/null +++ b/backend/src/controllers/v2/index.ts @@ -0,0 +1,5 @@ +import * as workspaceController from './workspaceController'; + +export { + workspaceController +} diff --git a/backend/src/controllers/v2/secretController.ts b/backend/src/controllers/v2/secretController.ts new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts new file mode 100644 index 000000000..86693b6c4 --- /dev/null +++ b/backend/src/controllers/v2/workspaceController.ts @@ -0,0 +1,565 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import { + Workspace, + Membership, + MembershipOrg, + Integration, + IntegrationAuth, + Key, + IUser, + ServiceToken, +} from '../../models'; +import { + createWorkspace as create, + deleteWorkspace as deleteWork +} from '../../helpers/workspace'; +import { + v2PushSecrets as push, + pullSecrets as pull, + reformatPullSecrets +} from '../../helpers/secret'; +import { pushKeys } from '../../helpers/key'; +import { addMemberships } from '../../helpers/membership'; +import { postHogClient, EventService } from '../../services'; +import { eventPushSecrets } from '../../events'; +import { ADMIN, COMPLETED, GRANTED, ENV_SET } from '../../variables'; +interface V2PushSecret { + type: string; // personal or shared + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretKeyHash: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretValueHash: string; + secretCommentCiphertext?: string; + secretCommentIV?: string; + secretCommentTag?: string; + secretCommentHash?: string; +} + +/** + * Return public keys of members of workspace with id [workspaceId] + * @param req + * @param res + * @returns + */ +export const getWorkspacePublicKeys = async (req: Request, res: Response) => { + let publicKeys; + try { + const { workspaceId } = req.params; + + publicKeys = ( + await Membership.find({ + workspace: workspaceId + }).populate<{ user: IUser }>('user', 'publicKey') + ) + .filter((m) => m.status === COMPLETED || m.status === GRANTED) + .map((member) => { + return { + publicKey: member.user.publicKey, + userId: member.user._id + }; + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace member public keys' + }); + } + + return res.status(200).send({ + publicKeys + }); +}; + +/** + * Return memberships for workspace with id [workspaceId] + * @param req + * @param res + * @returns + */ +export const getWorkspaceMemberships = async (req: Request, res: Response) => { + let users; + try { + const { workspaceId } = req.params; + + users = await Membership.find({ + workspace: workspaceId + }).populate('user', '+publicKey'); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace members' + }); + } + + return res.status(200).send({ + users + }); +}; + +/** + * Return workspaces that user is part of + * @param req + * @param res + * @returns + */ +export const getWorkspaces = async (req: Request, res: Response) => { + let workspaces; + try { + workspaces = ( + await Membership.find({ + user: req.user._id + }).populate('workspace') + ).map((m) => m.workspace); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspaces' + }); + } + + return res.status(200).send({ + workspaces + }); +}; + +/** + * Return workspace with id [workspaceId] + * @param req + * @param res + * @returns + */ +export const getWorkspace = async (req: Request, res: Response) => { + let workspace; + try { + const { workspaceId } = req.params; + + workspace = await Workspace.findOne({ + _id: workspaceId + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace' + }); + } + + return res.status(200).send({ + workspace + }); +}; + +/** + * Create new workspace named [workspaceName] under organization with id + * [organizationId] and add user as admin + * @param req + * @param res + * @returns + */ +export const createWorkspace = async (req: Request, res: Response) => { + let workspace; + try { + const { workspaceName, organizationId } = req.body; + + // validate organization membership + const membershipOrg = await MembershipOrg.findOne({ + user: req.user._id, + organization: organizationId + }); + + if (!membershipOrg) { + throw new Error('Failed to validate organization membership'); + } + + if (workspaceName.length < 1) { + throw new Error('Workspace names must be at least 1-character long'); + } + + // create workspace and add user as member + workspace = await create({ + name: workspaceName, + organizationId + }); + + await addMemberships({ + userIds: [req.user._id], + workspaceId: workspace._id.toString(), + roles: [ADMIN], + statuses: [GRANTED] + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to create workspace' + }); + } + + return res.status(200).send({ + workspace + }); +}; + +/** + * Delete workspace with id [workspaceId] + * @param req + * @param res + * @returns + */ +export const deleteWorkspace = async (req: Request, res: Response) => { + try { + const { workspaceId } = req.params; + + // delete workspace + await deleteWork({ + id: workspaceId + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to delete workspace' + }); + } + + return res.status(200).send({ + message: 'Successfully deleted workspace' + }); +}; + +/** + * Change name of workspace with id [workspaceId] to [name] + * @param req + * @param res + * @returns + */ +export const changeWorkspaceName = async (req: Request, res: Response) => { + let workspace; + try { + const { workspaceId } = req.params; + const { name } = req.body; + + workspace = await Workspace.findOneAndUpdate( + { + _id: workspaceId + }, + { + name + }, + { + new: true + } + ); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to change workspace name' + }); + } + + return res.status(200).send({ + message: 'Successfully changed workspace name', + workspace + }); +}; + +/** + * Return integrations for workspace with id [workspaceId] + * @param req + * @param res + * @returns + */ +export const getWorkspaceIntegrations = async (req: Request, res: Response) => { + let integrations; + try { + const { workspaceId } = req.params; + + integrations = await Integration.find({ + workspace: workspaceId + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace integrations' + }); + } + + return res.status(200).send({ + integrations + }); +}; + +/** + * Return (integration) authorizations for workspace with id [workspaceId] + * @param req + * @param res + * @returns + */ +export const getWorkspaceIntegrationAuthorizations = async ( + req: Request, + res: Response +) => { + let authorizations; + try { + const { workspaceId } = req.params; + + authorizations = await IntegrationAuth.find({ + workspace: workspaceId + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace integration authorizations' + }); + } + + return res.status(200).send({ + authorizations + }); +}; + +/** + * Return service service tokens for workspace [workspaceId] belonging to user + * @param req + * @param res + * @returns + */ +export const getWorkspaceServiceTokens = async ( + req: Request, + res: Response +) => { + let serviceTokens; + try { + const { workspaceId } = req.params; + + serviceTokens = await ServiceToken.find({ + user: req.user._id, + workspace: workspaceId + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace service tokens' + }); + } + + return res.status(200).send({ + serviceTokens + }); +} + +/** + * Upload (encrypted) secrets to workspace with id [workspaceId] + * for environment [environment] + * @param req + * @param res + * @returns + */ +export const pushWorkspaceSecrets = async (req: Request, res: Response) => { + // upload (encrypted) secrets to workspace with id [workspaceId] + + try { + let { secrets }: { secrets: V2PushSecret[] } = req.body; + const { keys, environment, channel } = req.body; + const { workspaceId } = req.params; + + // validate environment + if (!ENV_SET.has(environment)) { + throw new Error('Failed to validate environment'); + } + + // sanitize secrets + secrets = secrets.filter( + (s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== '' + ); + + await push({ + userId: req.user._id, + workspaceId, + environment, + secrets + }); + + await pushKeys({ + userId: req.user._id, + workspaceId, + keys + }); + + + if (postHogClient) { + postHogClient.capture({ + event: 'secrets pushed', + distinctId: req.user.email, + properties: { + numberOfSecrets: secrets.length, + environment, + workspaceId, + channel: channel ? channel : 'cli' + } + }); + } + + // trigger event - push secrets + EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId + }) + }); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to upload workspace secrets' + }); + } + + return res.status(200).send({ + message: 'Successfully uploaded workspace secrets' + }); +}; + +/** + * Return (encrypted) secrets for workspace with id [workspaceId] + * for environment [environment] and (encrypted) workspace key + * @param req + * @param res + * @returns + */ +export const pullSecrets = async (req: Request, res: Response) => { + // TODO: only return secrets, do not return workspace key + + let secrets; + let key; + try { + const environment: string = req.query.environment as string; + const channel: string = req.query.channel as string; + const { workspaceId } = req.params; + + // validate environment + if (!ENV_SET.has(environment)) { + throw new Error('Failed to validate environment'); + } + + secrets = await pull({ + userId: req.user._id.toString(), + workspaceId, + environment + }); + + key = await Key.findOne({ + workspace: workspaceId, + receiver: req.user._id + }) + .sort({ createdAt: -1 }) + .populate('sender', '+publicKey'); + + if (channel !== 'cli') { + secrets = reformatPullSecrets({ secrets }); + } + + if (postHogClient) { + // capture secrets pushed event in production + postHogClient.capture({ + distinctId: req.user.email, + event: 'secrets pulled', + properties: { + numberOfSecrets: secrets.length, + environment, + workspaceId, + channel: channel ? channel : 'cli' + } + }); + } + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to pull workspace secrets' + }); + } + + return res.status(200).send({ + secrets, + key + }); +}; + +// TODO: modify based on upcoming serviceTokenData changes + +/** + * Return (encrypted) secrets for workspace with id [workspaceId] + * for environment [environment] and (encrypted) workspace key + * via service token + * @param req + * @param res + * @returns + */ + export const pullSecretsServiceToken = async (req: Request, res: Response) => { + let secrets; + let key; + try { + const environment: string = req.query.environment as string; + const channel: string = req.query.channel as string; + const { workspaceId } = req.params; + + // validate environment + if (!ENV_SET.has(environment)) { + throw new Error('Failed to validate environment'); + } + + secrets = await pull({ + userId: req.serviceToken.user._id.toString(), + workspaceId, + environment + }); + + key = { + encryptedKey: req.serviceToken.encryptedKey, + nonce: req.serviceToken.nonce, + sender: { + publicKey: req.serviceToken.publicKey + }, + receiver: req.serviceToken.user, + workspace: req.serviceToken.workspace + }; + + if (postHogClient) { + // capture secrets pulled event in production + postHogClient.capture({ + distinctId: req.serviceToken.user.email, + event: 'secrets pulled', + properties: { + numberOfSecrets: secrets.length, + environment, + workspaceId, + channel: channel ? channel : 'cli' + } + }); + } + } catch (err) { + Sentry.setUser({ email: req.serviceToken.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to pull workspace secrets' + }); + } + + return res.status(200).send({ + secrets: reformatPullSecrets({ secrets }), + key + }); +}; \ No newline at end of file diff --git a/backend/src/ee/controllers/index.ts b/backend/src/ee/controllers/index.ts deleted file mode 100644 index e4fb89a8e..000000000 --- a/backend/src/ee/controllers/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as stripeController from './stripeController'; - -export { - stripeController -} \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/index.ts b/backend/src/ee/controllers/v1/index.ts new file mode 100644 index 000000000..23880070d --- /dev/null +++ b/backend/src/ee/controllers/v1/index.ts @@ -0,0 +1,9 @@ +import * as stripeController from './stripeController'; +import * as secretController from './secretController'; +import * as workspaceController from './workspaceController'; + +export { + stripeController, + secretController, + workspaceController +} \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/secretController.ts b/backend/src/ee/controllers/v1/secretController.ts new file mode 100644 index 000000000..b2d66ab33 --- /dev/null +++ b/backend/src/ee/controllers/v1/secretController.ts @@ -0,0 +1,35 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import { SecretVersion } from '../../models'; + +/** + * Return secret versions for secret with id [secretId] + * @param req + * @param res + */ + export const getSecretVersions = async (req: Request, res: Response) => { + let secretVersions; + try { + const { secretId } = req.params; + + const offset: number = parseInt(req.query.offset as string); + const limit: number = parseInt(req.query.limit as string); + + secretVersions = await SecretVersion.find({ + secret: secretId + }) + .skip(offset) + .limit(limit); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get secret versions' + }); + } + + return res.status(200).send({ + secretVersions + }); +} \ No newline at end of file diff --git a/backend/src/controllers/stripeController.ts b/backend/src/ee/controllers/v1/stripeController.ts similarity index 91% rename from backend/src/controllers/stripeController.ts rename to backend/src/ee/controllers/v1/stripeController.ts index 99a85ee2b..faef14cea 100644 --- a/backend/src/controllers/stripeController.ts +++ b/backend/src/ee/controllers/v1/stripeController.ts @@ -1,7 +1,7 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; import Stripe from 'stripe'; -import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../config'; +import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../../../config'; const stripe = new Stripe(STRIPE_SECRET_KEY, { apiVersion: '2022-08-01' }); diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts new file mode 100644 index 000000000..8b7ba422e --- /dev/null +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -0,0 +1,35 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import { SecretSnapshot } from '../../models'; + +/** + * Return secret snapshots for workspace with id [workspaceId] + * @param req + * @param res + */ + export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => { + let secretSnapshots; + try { + const { workspaceId } = req.params; + + const offset: number = parseInt(req.query.offset as string); + const limit: number = parseInt(req.query.limit as string); + + secretSnapshots = await SecretSnapshot.find({ + workspace: workspaceId + }) + .skip(offset) + .limit(limit); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get secret snapshots' + }); + } + + return res.status(200).send({ + secretSnapshots + }); +} \ No newline at end of file diff --git a/backend/src/ee/helpers/license.ts b/backend/src/ee/helpers/license.ts deleted file mode 100644 index 256bdc23a..000000000 --- a/backend/src/ee/helpers/license.ts +++ /dev/null @@ -1,21 +0,0 @@ - -/** - * @param {Object} obj - * @param {Object} obj.licenseKey - Infisical license key - */ -const checkLicenseKey = ({ - licenseKey -}: { - licenseKey: string -}) => { - try { - // TODO - - } catch (err) { - - } -} - -export { - checkLicenseKey -} \ No newline at end of file diff --git a/backend/src/ee/helpers/secret.ts b/backend/src/ee/helpers/secret.ts new file mode 100644 index 000000000..a688a108f --- /dev/null +++ b/backend/src/ee/helpers/secret.ts @@ -0,0 +1,74 @@ +import * as Sentry from '@sentry/node'; +import { + Secret +} from '../../models'; +import { + SecretSnapshot, + SecretVersion, + ISecretVersion +} from '../models'; + +/** + * Save a copy of the current state of secrets in workspace with id + * [workspaceId] under a new snapshot with incremented version under the + * secretsnapshots collection. + * @param {Object} obj + * @param {String} obj.workspaceId + */ + const takeSecretSnapshotHelper = async ({ + workspaceId +}: { + workspaceId: string; +}) => { + try { + const secrets = await Secret.find({ + workspace: workspaceId + }); + + const latestSecretSnapshot = await SecretSnapshot.findOne({ + workspace: workspaceId + }).sort({ version: -1 }); + + if (!latestSecretSnapshot) { + // case: no snapshots exist for workspace -> create first snapshot + await new SecretSnapshot({ + workspace: workspaceId, + version: 1, + secrets + }).save(); + + return; + } + + // case: snapshots exist for workspace + await new SecretSnapshot({ + workspace: workspaceId, + version: latestSecretSnapshot.version + 1, + secrets + }).save(); + + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to take a secret snapshot'); + } +} + +const addSecretVersionsHelper = async ({ + secretVersions +}: { + secretVersions: ISecretVersion[] +}) => { + try { + await SecretVersion.insertMany(secretVersions); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to add secret versions'); + } +} + +export { + takeSecretSnapshotHelper, + addSecretVersionsHelper +} \ No newline at end of file diff --git a/backend/src/ee/models/index.ts b/backend/src/ee/models/index.ts new file mode 100644 index 000000000..35d41c19a --- /dev/null +++ b/backend/src/ee/models/index.ts @@ -0,0 +1,9 @@ +import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot"; +import SecretVersion, { ISecretVersion } from "./secretVersion"; + +export { + SecretSnapshot, + ISecretSnapshot, + SecretVersion, + ISecretVersion +} \ No newline at end of file diff --git a/backend/src/ee/models/secretSnapshot.ts b/backend/src/ee/models/secretSnapshot.ts new file mode 100644 index 000000000..69633a92e --- /dev/null +++ b/backend/src/ee/models/secretSnapshot.ts @@ -0,0 +1,109 @@ +import { Schema, model, Types } from 'mongoose'; +import { + SECRET_SHARED, + SECRET_PERSONAL, + ENV_DEV, + ENV_TESTING, + ENV_STAGING, + ENV_PROD +} from '../../variables'; + +export interface ISecretSnapshot { + workspace: Types.ObjectId; + version: number; + secrets: { + version: number; + workspace: Types.ObjectId; + type: string; + user: Types.ObjectId; + environment: string; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretKeyHash: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretValueHash: string; + }[] +} + +const secretSnapshotSchema = new Schema( + { + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + version: { + type: Number, + required: true + }, + secrets: [{ + version: { + type: Number, + default: 1, + required: true + }, + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + type: { + type: String, + enum: [SECRET_SHARED, SECRET_PERSONAL], + required: true + }, + user: { + // user associated with the personal secret + type: Schema.Types.ObjectId, + ref: 'User' + }, + environment: { + type: String, + enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], + required: true + }, + secretKeyCiphertext: { + type: String, + required: true + }, + secretKeyIV: { + type: String, // symmetric + required: true + }, + secretKeyTag: { + type: String, // symmetric + required: true + }, + secretKeyHash: { + type: String, + required: true + }, + secretValueCiphertext: { + type: String, + required: true + }, + secretValueIV: { + type: String, // symmetric + required: true + }, + secretValueTag: { + type: String, // symmetric + required: true + }, + secretValueHash: { + type: String, + required: true + } + }] + }, + { + timestamps: true + } +); + +const SecretSnapshot = model('SecretSnapshot', secretSnapshotSchema); + +export default SecretSnapshot; \ No newline at end of file diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts new file mode 100644 index 000000000..a93a037f6 --- /dev/null +++ b/backend/src/ee/models/secretVersion.ts @@ -0,0 +1,75 @@ +import { Schema, model, Types } from 'mongoose'; + +export interface ISecretVersion { + _id?: Types.ObjectId; + secret: Types.ObjectId; + version: number; + isDeleted: boolean; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretKeyHash: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretValueHash: string; +} + +const secretVersionSchema = new Schema( + { + secret: { // could be deleted + type: Schema.Types.ObjectId, + ref: 'Secret', + required: true + }, + version: { + type: Number, + default: 1, + required: true + }, + isDeleted: { + type: Boolean, + default: false, + required: true + }, + secretKeyCiphertext: { + type: String, + required: true + }, + secretKeyIV: { + type: String, // symmetric + required: true + }, + secretKeyTag: { + type: String, // symmetric + required: true + }, + secretKeyHash: { + type: String, + required: true + }, + secretValueCiphertext: { + type: String, + required: true + }, + secretValueIV: { + type: String, // symmetric + required: true + }, + secretValueTag: { + type: String, // symmetric + required: true + }, + secretValueHash: { + type: String, + required: true + } + }, + { + timestamps: true + } +); + +const SecretVersion = model('SecretVersion', secretVersionSchema); + +export default SecretVersion; \ No newline at end of file diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts new file mode 100644 index 000000000..960665f4a --- /dev/null +++ b/backend/src/ee/routes/v1/index.ts @@ -0,0 +1,7 @@ +import secret from './secret'; +import workspace from './workspace'; + +export { + secret, + workspace +} \ No newline at end of file diff --git a/backend/src/ee/routes/v1/secret.ts b/backend/src/ee/routes/v1/secret.ts new file mode 100644 index 000000000..a866a6320 --- /dev/null +++ b/backend/src/ee/routes/v1/secret.ts @@ -0,0 +1,26 @@ +import express from 'express'; +const router = express.Router(); +import { + requireAuth, + requireWorkspaceAuth, + validateRequest +} from '../../../middleware'; +import { body, query, param } from 'express-validator'; +import { secretController } from '../../controllers/v1'; +import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../../variables'; + +router.get( + '/:secretId/secret-versions', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + param('secretId').exists().trim(), + query('offset').exists().isInt(), + query('limit').exists().isInt(), + validateRequest, + secretController.getSecretVersions +); + +export default router; \ No newline at end of file diff --git a/backend/src/ee/routes/stripe.ts b/backend/src/ee/routes/v1/stripe.ts similarity index 71% rename from backend/src/ee/routes/stripe.ts rename to backend/src/ee/routes/v1/stripe.ts index 6f89f5655..02d68c4ea 100644 --- a/backend/src/ee/routes/stripe.ts +++ b/backend/src/ee/routes/v1/stripe.ts @@ -1,6 +1,6 @@ import express from 'express'; const router = express.Router(); -import { stripeController } from '../controllers'; +import { stripeController } from '../../controllers/v1'; router.post('/webhook', stripeController.handleWebhook); diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts new file mode 100644 index 000000000..6756269e5 --- /dev/null +++ b/backend/src/ee/routes/v1/workspace.ts @@ -0,0 +1,27 @@ +import express from 'express'; +const router = express.Router(); +import { + requireAuth, + requireWorkspaceAuth, + validateRequest +} from '../../../middleware'; +import { param, query } from 'express-validator'; +import { ADMIN, MEMBER, GRANTED } from '../../../variables'; +import { workspaceController } from '../../controllers/v1'; + +router.get( + '/:workspaceId/secret-snapshots', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + query('offset').exists().isInt(), + query('limit').exists().isInt(), + validateRequest, + workspaceController.getWorkspaceSecretSnapshots +); + + +export default router; \ No newline at end of file diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts new file mode 100644 index 000000000..f31482dde --- /dev/null +++ b/backend/src/ee/services/EELicenseService.ts @@ -0,0 +1,19 @@ +import { LICENSE_KEY } from '../../config'; + +/** + * Class to handle Enterprise Edition license actions + */ +class EELicenseService { + + private readonly _isLicenseValid: boolean; + + constructor(licenseKey: string) { + this._isLicenseValid = true; + } + + public get isLicenseValid(): boolean { + return this._isLicenseValid; + } +} + +export default new EELicenseService(LICENSE_KEY); \ No newline at end of file diff --git a/backend/src/ee/services/EESecretService.ts b/backend/src/ee/services/EESecretService.ts new file mode 100644 index 000000000..643f763f1 --- /dev/null +++ b/backend/src/ee/services/EESecretService.ts @@ -0,0 +1,47 @@ +import { ISecretVersion } from '../models'; +import { + takeSecretSnapshotHelper, + addSecretVersionsHelper +} from '../helpers/secret'; +import EELicenseService from './EELicenseService'; + +/** + * Class to handle Enterprise Edition secret actions + */ +class EESecretService { + + /** + * Save a copy of the current state of secrets in workspace with id + * [workspaceId] under a new snapshot with incremented version under the + * SecretSnapshot collection. + * Requires a valid license key [licenseKey] + * @param {Object} obj + * @param {String} obj.workspaceId + */ + static async takeSecretSnapshot({ + workspaceId + }: { + workspaceId: string; + }) { + if (!EELicenseService.isLicenseValid) return; + await takeSecretSnapshotHelper({ workspaceId }); + } + + /** + * Adds secret versions [secretVersions] to the SecretVersion collection. + * @param {Object} obj + * @param {SecretVersion} obj.secretVersions + */ + static async addSecretVersions({ + secretVersions + }: { + secretVersions: ISecretVersion[]; + }) { + if (!EELicenseService.isLicenseValid) return; + await addSecretVersionsHelper({ + secretVersions + }); + } +} + +export default EESecretService; \ No newline at end of file diff --git a/backend/src/ee/services/index.ts b/backend/src/ee/services/index.ts new file mode 100644 index 000000000..3cec256bb --- /dev/null +++ b/backend/src/ee/services/index.ts @@ -0,0 +1,7 @@ +import EELicenseService from "./EELicenseService"; +import EESecretService from "./EESecretService"; + +export { + EELicenseService, + EESecretService +} \ No newline at end of file diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts index abaf73af4..b3f276b53 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -12,7 +12,6 @@ import { decryptSymmetric, decryptAsymmetric } from '../utils/crypto'; -import { decryptSecrets } from '../helpers/secret'; import { ENCRYPTION_KEY } from '../config'; import { SECRET_SHARED } from '../variables'; diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index ccfd72a53..d92156ece 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -2,18 +2,18 @@ import * as Sentry from '@sentry/node'; import { Bot, Integration, - IIntegration, IntegrationAuth, - IIntegrationAuth } from '../models'; import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations'; -import { BotService, IntegrationService } from '../services'; +import { BotService } from '../services'; import { ENV_DEV, EVENT_PUSH_SECRETS, INTEGRATION_VERCEL, INTEGRATION_NETLIFY } from '../variables'; +import { UnauthorizedRequestError } from '../utils/errors'; +import RequestError from '../utils/requestError'; interface Update { workspace: string; @@ -176,12 +176,13 @@ const syncIntegrationsHelper = async ({ */ const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => { let refreshToken; + try { const integrationAuth = await IntegrationAuth .findById(integrationAuthId) .select('+refreshCiphertext +refreshIV +refreshTag'); - if (!integrationAuth) throw new Error('Failed to find integration auth'); + if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); refreshToken = await BotService.decryptSymmetric({ workspaceId: integrationAuth.workspace.toString(), @@ -193,7 +194,10 @@ const syncIntegrationsHelper = async ({ } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get integration refresh token'); + if(err instanceof RequestError) + throw err + else + throw new Error('Failed to get integration refresh token'); } return refreshToken; @@ -209,12 +213,13 @@ const syncIntegrationsHelper = async ({ */ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => { let accessToken; + try { const integrationAuth = await IntegrationAuth .findById(integrationAuthId) .select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext'); - if (!integrationAuth) throw new Error('Failed to find integration auth'); + if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); accessToken = await BotService.decryptSymmetric({ workspaceId: integrationAuth.workspace.toString(), @@ -240,7 +245,10 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get integration access token'); + if(err instanceof RequestError) + throw err + else + throw new Error('Failed to get integration access token'); } return accessToken; diff --git a/backend/src/helpers/membership.ts b/backend/src/helpers/membership.ts index 1ff542f2a..b237803f1 100644 --- a/backend/src/helpers/membership.ts +++ b/backend/src/helpers/membership.ts @@ -21,11 +21,12 @@ const validateMembership = async ({ }) => { let membership; + //TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors try { membership = await Membership.findOne({ user: userId, workspace: workspaceId - }); + }).populate("workspace"); if (!membership) throw new Error('Failed to find membership'); diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index 042aba4fa..f055971ae 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -1,12 +1,21 @@ import * as Sentry from '@sentry/node'; import { Secret, - ISecret + ISecret, } from '../models'; +import { + EESecretService +} from '../ee/services'; +import { + SecretVersion +} from '../ee/models'; +import { + takeSecretSnapshotHelper +} from '../ee/helpers/secret'; import { decryptSymmetric } from '../utils/crypto'; import { SECRET_SHARED, SECRET_PERSONAL } from '../variables'; -interface PushSecret { +interface V1PushSecret { ciphertextKey: string; ivKey: string; tagKey: string; @@ -15,11 +24,31 @@ interface PushSecret { ivValue: string; tagValue: string; hashValue: string; + ciphertextComment: string; + ivComment: string; + tagComment: string; + hashComment: string; type: 'shared' | 'personal'; } +interface V2PushSecret { + type: string; // personal or shared + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretKeyHash: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretValueHash: string; + secretCommentCiphertext?: string; + secretCommentIV?: string; + secretCommentTag?: string; + secretCommentHash?: string; +} + interface Update { - [index: string]: string; + [index: string]: any; } type DecryptSecretType = 'text' | 'object' | 'expanded'; @@ -35,7 +64,7 @@ type DecryptSecretType = 'text' | 'object' | 'expanded'; * @param {String} obj.environment - environment for secrets * @param {Object[]} obj.secrets - secrets to push */ -const pushSecrets = async ({ +const v1PushSecrets = async ({ userId, workspaceId, environment, @@ -44,8 +73,9 @@ const pushSecrets = async ({ userId: string; workspaceId: string; environment: string; - secrets: PushSecret[]; + secrets: V1PushSecret[]; }): Promise => { + // TODO: clean up function and fix up types try { // construct useful data structures const oldSecrets = await pullSecrets({ @@ -53,74 +83,133 @@ const pushSecrets = async ({ workspaceId, environment }); - const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => { - return { ...accumulator, [s.secretKeyHash]: s }; - }, {}); - const newSecretsObj = secrets.reduce((accumulator, s) => { - return { ...accumulator, [s.hashKey]: s }; - }, {}); + + const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => + ({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s }) + , {}); + const newSecretsObj: any = secrets.reduce((accumulator, s) => + ({ ...accumulator, [`${s.type}-${s.hashKey}`]: s }) + , {}); // handle deleting secrets - const toDelete = oldSecrets.filter( - (s: ISecret) => !(s.secretKeyHash in newSecretsObj) - ); + const toDelete = oldSecrets + .filter( + (s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj) + ) + .map((s) => s._id); if (toDelete.length > 0) { await Secret.deleteMany({ - _id: { $in: toDelete.map((s) => s._id) } + _id: { $in: toDelete } + }); + + await SecretVersion.updateMany({ + secret: { $in: toDelete } + }, { + isDeleted: true }); } - - // handle modifying secrets where type or value changed - const operations = secrets + + const toUpdate = oldSecrets .filter((s) => { - if (s.hashKey in oldSecretsObj) { - if (s.hashValue !== oldSecretsObj[s.hashKey].secretValueHash) { - // case: filter secrets where value changed + if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) { + if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue + || s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment) { + // case: filter secrets where value or comment changed return true; } - if (s.type !== oldSecretsObj[s.hashKey].type) { - // case: filter secrets where type changed + if (!s.version) { + // case: filter (legacy) secrets that were not versioned return true; } } - + return false; - }) + }); + + const operations = toUpdate .map((s) => { + const { + ciphertextValue, + ivValue, + tagValue, + hashValue, + ciphertextComment, + ivComment, + tagComment, + hashComment + } = newSecretsObj[`${s.type}-${s.secretKeyHash}`]; + const update: Update = { - type: s.type, - secretValueCiphertext: s.ciphertextValue, - secretValueIV: s.ivValue, - secretValueTag: s.tagValue, - secretValueHash: s.hashValue - }; + secretValueCiphertext: ciphertextValue, + secretValueIV: ivValue, + secretValueTag: tagValue, + secretValueHash: hashValue, + secretCommentCiphertext: ciphertextComment, + secretCommentIV: ivComment, + secretCommentTag: tagComment, + secretCommentHash: hashComment, + } + + if (!s.version) { + // case: (legacy) secret was not versioned + update.version = 1; + } else { + update['$inc'] = { + version: 1 + } + } if (s.type === SECRET_PERSONAL) { - // attach user assocaited with the personal secret + // attach user associated with the personal secret update['user'] = userId; } return { updateOne: { filter: { - workspace: workspaceId, - _id: oldSecretsObj[s.hashKey]._id + _id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id }, update } }; }); - const a = await Secret.bulkWrite(operations as any); + await Secret.bulkWrite(operations as any); + + // (EE) add secret versions for updated secrets + await EESecretService.addSecretVersions({ + secretVersions: toUpdate.map(({ + _id, + version, + type, + secretKeyHash, + }) => { + const newSecret = newSecretsObj[`${type}-${secretKeyHash}`]; + return ({ + secret: _id, + version: version ? version + 1 : 1, + isDeleted: false, + secretKeyCiphertext: newSecret.ciphertextKey, + secretKeyIV: newSecret.ivKey, + secretKeyTag: newSecret.tagKey, + secretKeyHash: newSecret.hashKey, + secretValueCiphertext: newSecret.ciphertextValue, + secretValueIV: newSecret.ivValue, + secretValueTag: newSecret.tagValue, + secretValueHash: newSecret.hashValue + }) + }) + }); // handle adding new secrets - const toAdd = secrets.filter((s) => !(s.hashKey in oldSecretsObj)); + const toAdd = secrets.filter((s) => !(`${s.type}-${s.hashKey}` in oldSecretsObj)); if (toAdd.length > 0) { // add secrets - await Secret.insertMany( + const newSecrets = await Secret.insertMany( toAdd.map((s, idx) => { - let obj: any = { + const obj: any = { + version: 1, workspace: workspaceId, type: toAdd[idx].type, environment, @@ -131,7 +220,11 @@ const pushSecrets = async ({ secretValueCiphertext: s.ciphertextValue, secretValueIV: s.ivValue, secretValueTag: s.tagValue, - secretValueHash: s.hashValue + secretValueHash: s.hashValue, + secretCommentCiphertext: s.ciphertextComment, + secretCommentIV: s.ivComment, + secretCommentTag: s.tagComment, + secretCommentHash: s.hashComment }; if (toAdd[idx].type === 'personal') { @@ -141,7 +234,282 @@ const pushSecrets = async ({ return obj; }) ); + + // (EE) add secret versions for new secrets + EESecretService.addSecretVersions({ + secretVersions: newSecrets.map(({ + _id, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash + }) => ({ + secret: _id, + version: 1, + isDeleted: false, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash + })) + }); } + + // (EE) take a secret snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId + }) + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to push shared and personal secrets'); + } +}; + +/** + * Push secrets for user with id [userId] to workspace + * with id [workspaceId] with environment [environment]. Follow steps: + * 1. Handle shared secrets (insert, delete) + * 2. handle personal secrets (insert, delete) + * @param {Object} obj + * @param {String} obj.userId - id of user to push secrets for + * @param {String} obj.workspaceId - id of workspace to push to + * @param {String} obj.environment - environment for secrets + * @param {Object[]} obj.secrets - secrets to push + */ + const v2PushSecrets = async ({ + userId, + workspaceId, + environment, + secrets +}: { + userId: string; + workspaceId: string; + environment: string; + secrets: V2PushSecret[]; +}): Promise => { + // TODO: clean up function and fix up types + try { + // construct useful data structures + const oldSecrets = await pullSecrets({ + userId, + workspaceId, + environment + }); + + const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => + ({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s }) + , {}); + const newSecretsObj: any = secrets.reduce((accumulator, s) => + ({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s }) + , {}); + + // handle deleting secrets + const toDelete = oldSecrets + .filter( + (s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj) + ) + .map((s) => s._id); + if (toDelete.length > 0) { + await Secret.deleteMany({ + _id: { $in: toDelete } + }); + + await SecretVersion.updateMany({ + secret: { $in: toDelete } + }, { + isDeleted: true + }); + } + + const toUpdate = oldSecrets + .filter((s) => { + if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) { + if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretValueHash + || s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretCommentHash) { + // case: filter secrets where value or comment changed + return true; + } + + if (!s.version) { + // case: filter (legacy) secrets that were not versioned + return true; + } + } + + return false; + }); + + const operations = toUpdate + .map((s) => { + const { + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentHash, + } = newSecretsObj[`${s.type}-${s.secretKeyHash}`]; + + const update: Update = { + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentHash, + } + + if (!s.version) { + // case: (legacy) secret was not versioned + update.version = 1; + } else { + update['$inc'] = { + version: 1 + } + } + + if (s.type === SECRET_PERSONAL) { + // attach user associated with the personal secret + update['user'] = userId; + } + + return { + updateOne: { + filter: { + _id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id + }, + update + } + }; + }); + await Secret.bulkWrite(operations as any); + + // (EE) add secret versions for updated secrets + await EESecretService.addSecretVersions({ + secretVersions: toUpdate.map((s) => { + const { + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentHash, + } = newSecretsObj[`${s.type}-${s.secretKeyHash}`]; + + return ({ + secret: s._id, + version: s.version ? s.version + 1 : 1, + isDeleted: false, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash + }) + }) + }); + + // handle adding new secrets + const toAdd = secrets.filter((s) => !(`${s.type}-${s.secretKeyHash}` in oldSecretsObj)); + + if (toAdd.length > 0) { + // add secrets + const newSecrets = await Secret.insertMany( + toAdd.map(({ + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentHash, + }, idx) => { + const obj: any = { + version: 1, + workspace: workspaceId, + type: toAdd[idx].type, + environment, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentHash + }; + + if (toAdd[idx].type === 'personal') { + obj['user' as keyof typeof obj] = userId; + } + + return obj; + }) + ); + + // (EE) add secret versions for new secrets + EESecretService.addSecretVersions({ + secretVersions: newSecrets.map(({ + _id, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash + }) => ({ + secret: _id, + version: 1, + isDeleted: false, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash + })) + }); + } + + // (EE) take a secret snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId + }) } catch (err) { Sentry.setUser(null); Sentry.captureException(err); @@ -222,6 +590,13 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => { iv: s.secretValueIV, tag: s.secretValueTag, hash: s.secretValueHash + }, + secretComment: { + workspace: s.workspace, + ciphertext: s.secretCommentCiphertext, + iv: s.secretCommentIV, + tag: s.secretCommentTag, + hash: s.secretCommentHash } })); } catch (err) { @@ -233,71 +608,9 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => { return reformatedSecrets; }; -/** - * Return decrypted secrets in format [format] - * @param {Object} obj - * @param {Object[]} obj.secrets - array of (encrypted) secret key-value pair objects - * @param {String} obj.key - symmetric key to decrypt secret key-value pairs - * @param {String} obj.format - desired return format that is either "text," "object," or "expanded" - * @return {String|Object} (decrypted) secrets also called the content - */ -const decryptSecrets = ({ - secrets, - key, - format -}: { - secrets: PushSecret[]; - key: string; - format: DecryptSecretType; -}) => { - // init content - let content: any = format === 'text' ? '' : {}; - - // decrypt secrets - secrets.forEach((s, idx) => { - const secretKey = decryptSymmetric({ - ciphertext: s.ciphertextKey, - iv: s.ivKey, - tag: s.tagKey, - key - }); - - const secretValue = decryptSymmetric({ - ciphertext: s.ciphertextValue, - iv: s.ivValue, - tag: s.tagValue, - key - }); - - switch (format) { - case 'text': - content += secretKey; - content += '='; - content += secretValue; - - if (idx < secrets.length) { - content += '\n'; - } - break; - case 'object': - content[secretKey] = secretValue; - break; - case 'expanded': - content[secretKey] = { - ...s, - plaintextKey: secretKey, - plaintextValue: secretValue - }; - break; - } - }); - - return content; -}; - export { - pushSecrets, + v1PushSecrets, + v2PushSecrets, pullSecrets, - reformatPullSecrets, - decryptSecrets + reformatPullSecrets }; diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts index dafddc785..3e7076118 100644 --- a/backend/src/integrations/exchange.ts +++ b/backend/src/integrations/exchange.ts @@ -9,8 +9,7 @@ import { INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_GITHUB_TOKEN_URL, - INTEGRATION_GITHUB_API_URL, - ACTION_PUSH_TO_HEROKU + INTEGRATION_GITHUB_API_URL } from '../variables'; import { SITE_URL, @@ -22,7 +21,6 @@ import { CLIENT_SECRET_NETLIFY, CLIENT_SECRET_GITHUB } from '../config'; -import { user } from '../routes'; interface ExchangeCodeHerokuResponse { token_type: string; diff --git a/backend/src/integrations/refresh.ts b/backend/src/integrations/refresh.ts index 8ddb6a651..ea232f1e5 100644 --- a/backend/src/integrations/refresh.ts +++ b/backend/src/integrations/refresh.ts @@ -53,6 +53,7 @@ const exchangeRefreshHeroku = async ({ refreshToken: string; }) => { let accessToken; + //TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors try { const res = await axios.post( INTEGRATION_HEROKU_TOKEN_URL, diff --git a/backend/src/middleware/requestErrorHandler.ts b/backend/src/middleware/requestErrorHandler.ts new file mode 100644 index 000000000..36f1dce49 --- /dev/null +++ b/backend/src/middleware/requestErrorHandler.ts @@ -0,0 +1,29 @@ +import { ErrorRequestHandler } from "express"; + +import * as Sentry from '@sentry/node'; +import { InternalServerError } from "../utils/errors"; +import { getLogger } from "../utils/logger"; +import RequestError, { LogLevel } from "../utils/requestError"; + + +export const requestErrorHandler: ErrorRequestHandler = (error: RequestError|Error, req, res, next) => { + if(res.headersSent) return next(); + //TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError + if(!(error instanceof RequestError)){ + error = InternalServerError({context: {exception: error.message}, stack: error.stack}) + getLogger('backend-main').log((error).levelName.toLowerCase(), (error).message) + } + + //* Set Sentry user identification if req.user is populated + if(req.user !== undefined && req.user !== null){ + Sentry.setUser({ email: req.user.email }) + } + //* Only sent error to Sentry if LogLevel is one of the following level 'ERROR', 'EMERGENCY' or 'CRITICAL' + //* with this we will eliminate false-positive errors like 'BadRequestError', 'UnauthorizedRequestError' and so on + if([LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes((error).level)){ + Sentry.captureException(error) + } + + res.status((error).statusCode).json((error).format(req)) + next() +} \ No newline at end of file diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index a6bd79073..d917d362a 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -1,8 +1,8 @@ import jwt from 'jsonwebtoken'; import { Request, Response, NextFunction } from 'express'; -import * as Sentry from '@sentry/node'; import { User } from '../models'; import { JWT_AUTH_SECRET } from '../config'; +import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors'; declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -20,32 +20,25 @@ declare module 'jsonwebtoken' { */ const requireAuth = async (req: Request, res: Response, next: NextFunction) => { // JWT authentication middleware - try { - if (!req.headers?.authorization) - throw new Error('Failed to locate authorization header'); + const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null] + if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`})) + if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`})) + if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) - const token = req.headers.authorization.split(' ')[1]; - const decodedToken = ( - jwt.verify(token, JWT_AUTH_SECRET) - ); + const decodedToken = ( + jwt.verify(AUTH_TOKEN_VALUE, JWT_AUTH_SECRET) + ); - const user = await User.findOne({ - _id: decodedToken.userId - }).select('+publicKey'); + const user = await User.findOne({ + _id: decodedToken.userId + }).select('+publicKey'); - if (!user) throw new Error('Failed to authenticate unfound user'); - if (!user?.publicKey) - throw new Error('Failed to authenticate not fully set up account'); + if (!user) return next(AccountNotFoundError({message: 'Failed to locate User account'})) + if (!user?.publicKey) + return next(UnauthorizedRequestError({message: 'Unable to authenticate due to partially set up account'})) - req.user = user; - return next(); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(401).send({ - error: 'Failed to authenticate user. Try logging in' - }); - } + req.user = user; + return next(); }; export default requireAuth; diff --git a/backend/src/middleware/requireBotAuth.ts b/backend/src/middleware/requireBotAuth.ts index 6c5a3820a..e39f0d1b5 100644 --- a/backend/src/middleware/requireBotAuth.ts +++ b/backend/src/middleware/requireBotAuth.ts @@ -1,7 +1,7 @@ -import * as Sentry from '@sentry/node'; import { Request, Response, NextFunction } from 'express'; import { Bot } from '../models'; import { validateMembership } from '../helpers/membership'; +import { AccountNotFoundError } from '../utils/errors'; type req = 'params' | 'body' | 'query'; @@ -15,30 +15,22 @@ const requireBotAuth = ({ location?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { - try { - const bot = await Bot.findOne({ _id: req[location].botId }); - - if (!bot) { - throw new Error('Failed to find bot'); - } - - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: bot.workspace.toString(), - acceptedRoles, - acceptedStatuses - }); - - req.bot = bot; - - next(); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(401).send({ - error: 'Failed bot authorization' - }); + const bot = await Bot.findOne({ _id: req[location].botId }); + + if (!bot) { + return next(AccountNotFoundError({message: 'Failed to locate Bot account'})) } + + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: bot.workspace.toString(), + acceptedRoles, + acceptedStatuses + }); + + req.bot = bot; + + next(); } } diff --git a/backend/src/middleware/requireIntegrationAuth.ts b/backend/src/middleware/requireIntegrationAuth.ts index fe653dbc0..4389028ab 100644 --- a/backend/src/middleware/requireIntegrationAuth.ts +++ b/backend/src/middleware/requireIntegrationAuth.ts @@ -1,8 +1,8 @@ -import * as Sentry from '@sentry/node'; import { Request, Response, NextFunction } from 'express'; -import { Bot, Integration, IntegrationAuth, Membership } from '../models'; +import { Integration, IntegrationAuth } from '../models'; import { IntegrationService } from '../services'; import { validateMembership } from '../helpers/membership'; +import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/errors'; /** * Validate if user on request is a member of workspace with proper roles associated @@ -21,48 +21,40 @@ const requireIntegrationAuth = ({ return async (req: Request, res: Response, next: NextFunction) => { // integration authorization middleware - try { - const { integrationId } = req.params; + const { integrationId } = req.params; - // validate integration accessibility - const integration = await Integration.findOne({ - _id: integrationId - }); + // validate integration accessibility + const integration = await Integration.findOne({ + _id: integrationId + }); - if (!integration) { - throw new Error('Failed to find integration'); - } - - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: integration.workspace.toString(), - acceptedRoles, - acceptedStatuses - }); - - const integrationAuth = await IntegrationAuth.findOne({ - _id: integration.integrationAuth - }).select( - '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' - ); - - if (!integrationAuth) { - throw new Error('Failed to find integration authorization'); - } - - req.integration = integration; - req.accessToken = await IntegrationService.getIntegrationAuthAccess({ - integrationAuthId: integrationAuth._id.toString() - }); - - return next(); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(401).send({ - error: 'Failed integration authorization' - }); + if (!integration) { + return next(IntegrationNotFoundError({message: 'Failed to locate Integration'})) } + + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: integration.workspace.toString(), + acceptedRoles, + acceptedStatuses + }); + + const integrationAuth = await IntegrationAuth.findOne({ + _id: integration.integrationAuth + }).select( + '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' + ); + + if (!integrationAuth) { + return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'})) + } + + req.integration = integration; + req.accessToken = await IntegrationService.getIntegrationAuthAccess({ + integrationAuthId: integrationAuth._id.toString() + }); + + return next(); }; }; diff --git a/backend/src/middleware/requireIntegrationAuthorizationAuth.ts b/backend/src/middleware/requireIntegrationAuthorizationAuth.ts index ed44ffec5..278716e60 100644 --- a/backend/src/middleware/requireIntegrationAuthorizationAuth.ts +++ b/backend/src/middleware/requireIntegrationAuthorizationAuth.ts @@ -3,6 +3,7 @@ import { Request, Response, NextFunction } from 'express'; import { IntegrationAuth } from '../models'; import { IntegrationService } from '../services'; import { validateMembership } from '../helpers/membership'; +import { UnauthorizedRequestError } from '../utils/errors'; /** * Validate if user on request is a member of workspace with proper roles associated @@ -22,41 +23,33 @@ const requireIntegrationAuthorizationAuth = ({ attachAccessToken?: boolean; }) => { return async (req: Request, res: Response, next: NextFunction) => { - try { - const { integrationAuthId } = req.params; + const { integrationAuthId } = req.params; - const integrationAuth = await IntegrationAuth.findOne({ - _id: integrationAuthId - }).select( - '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' - ); + const integrationAuth = await IntegrationAuth.findOne({ + _id: integrationAuthId + }).select( + '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' + ); - if (!integrationAuth) { - throw new Error('Failed to find integration authorization'); - } - - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: integrationAuth.workspace.toString(), - acceptedRoles, - acceptedStatuses - }); + if (!integrationAuth) { + return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authorization credentials'})) + } + + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: integrationAuth.workspace.toString(), + acceptedRoles, + acceptedStatuses + }); - req.integrationAuth = integrationAuth; - if (attachAccessToken) { - req.accessToken = await IntegrationService.getIntegrationAuthAccess({ - integrationAuthId: integrationAuth._id.toString() - }); - } - - return next(); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(401).send({ - error: 'Failed (authorization) integration authorizationt' + req.integrationAuth = integrationAuth; + if (attachAccessToken) { + req.accessToken = await IntegrationService.getIntegrationAuthAccess({ + integrationAuthId: integrationAuth._id.toString() }); } + + return next(); }; }; diff --git a/backend/src/middleware/requireOrganizationAuth.ts b/backend/src/middleware/requireOrganizationAuth.ts index c77bd312e..04542b429 100644 --- a/backend/src/middleware/requireOrganizationAuth.ts +++ b/backend/src/middleware/requireOrganizationAuth.ts @@ -1,6 +1,6 @@ -import * as Sentry from '@sentry/node'; import { Request, Response, NextFunction } from 'express'; import { IOrganization, MembershipOrg } from '../models'; +import { UnauthorizedRequestError, ValidationError } from '../utils/errors'; /** * Validate if user on request is a member with proper roles for organization @@ -19,35 +19,28 @@ const requireOrganizationAuth = ({ return async (req: Request, res: Response, next: NextFunction) => { // organization authorization middleware - try { - // validate organization membership - const membershipOrg = await MembershipOrg.findOne({ - user: req.user._id, - organization: req.params.organizationId - }).populate<{ organization: IOrganization }>('organization'); + // validate organization membership + const membershipOrg = await MembershipOrg.findOne({ + user: req.user._id, + organization: req.params.organizationId + }).populate<{ organization: IOrganization }>('organization'); - if (!membershipOrg) { - throw new Error('Failed to find organization membership'); - } - if (!acceptedRoles.includes(membershipOrg.role)) { - throw new Error('Failed to validate organization membership role'); - } - - if (!acceptedStatuses.includes(membershipOrg.status)) { - throw new Error('Failed to validate organization membership status'); - } - - req.membershipOrg = membershipOrg; - - return next(); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(401).send({ - error: 'Failed organization authorization' - }); + if (!membershipOrg) { + return next(UnauthorizedRequestError({message: "You're not a member of this Organization."})) } + //TODO is this important to validate? I mean is it possible to save wrong role to database or get wrong role from databse? - Zamion101 + if (!acceptedRoles.includes(membershipOrg.role)) { + return next(ValidationError({message: 'Failed to validate Organization Membership Role'})) + } + + if (!acceptedStatuses.includes(membershipOrg.status)) { + return next(ValidationError({message: 'Failed to validate Organization Membership Status'})) + } + + req.membershipOrg = membershipOrg; + + return next(); }; }; diff --git a/backend/src/middleware/requireServiceTokenAuth.ts b/backend/src/middleware/requireServiceTokenAuth.ts index d403e8fe5..94e8363ff 100644 --- a/backend/src/middleware/requireServiceTokenAuth.ts +++ b/backend/src/middleware/requireServiceTokenAuth.ts @@ -1,8 +1,8 @@ import jwt from 'jsonwebtoken'; import { Request, Response, NextFunction } from 'express'; -import * as Sentry from '@sentry/node'; import { ServiceToken } from '../models'; import { JWT_SERVICE_SECRET } from '../config'; +import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -24,33 +24,27 @@ const requireServiceTokenAuth = async ( next: NextFunction ) => { // JWT service token middleware - try { - if (!req.headers?.authorization) - throw new Error('Failed to locate authorization header'); + + const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null] + if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`})) + //TODO: Determine what is the actual Token Type for Service Token Authentication (ex. Bearer) + //if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(UnauthorizedRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`})) + if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) - const token = req.headers.authorization.split(' ')[1]; + const decodedToken = ( + jwt.verify(AUTH_TOKEN_VALUE, JWT_SERVICE_SECRET) + ); - const decodedToken = ( - jwt.verify(token, JWT_SERVICE_SECRET) - ); + const serviceToken = await ServiceToken.findOne({ + _id: decodedToken.serviceTokenId + }) + .populate('user', '+publicKey') + .select('+encryptedKey +publicKey +nonce'); - const serviceToken = await ServiceToken.findOne({ - _id: decodedToken.serviceTokenId - }) - .populate('user', '+publicKey') - .select('+encryptedKey +publicKey +nonce'); + if (!serviceToken) return next(UnauthorizedRequestError({message: 'The service token does not match the record in the database'})) - if (!serviceToken) throw new Error('Failed to find service token'); - - req.serviceToken = serviceToken; - return next(); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(401).send({ - error: 'Failed to authenticate service token' - }); - } + req.serviceToken = serviceToken; + return next(); }; export default requireServiceTokenAuth; diff --git a/backend/src/middleware/requireSignupAuth.ts b/backend/src/middleware/requireSignupAuth.ts index 9384387ee..3318bd8d3 100644 --- a/backend/src/middleware/requireSignupAuth.ts +++ b/backend/src/middleware/requireSignupAuth.ts @@ -1,8 +1,8 @@ import jwt from 'jsonwebtoken'; import { Request, Response, NextFunction } from 'express'; -import * as Sentry from '@sentry/node'; import { User } from '../models'; import { JWT_SIGNUP_SECRET } from '../config'; +import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -21,32 +21,24 @@ const requireSignupAuth = async ( ) => { // JWT (temporary) authentication middleware for complete signup - try { - if (!req.headers?.authorization) - throw new Error('Failed to locate authorization header'); + const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null] + if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`})) + if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`})) + if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) + + const decodedToken = ( + jwt.verify(AUTH_TOKEN_VALUE, JWT_SIGNUP_SECRET) + ); - const token = req.headers.authorization.split(' ')[1]; - const decodedToken = ( - jwt.verify(token, JWT_SIGNUP_SECRET) - ); + const user = await User.findOne({ + _id: decodedToken.userId + }).select('+publicKey'); - const user = await User.findOne({ - _id: decodedToken.userId - }).select('+publicKey'); + if (!user) + return next(UnauthorizedRequestError({message: 'Unable to authenticate for User account completion. Try logging in again'})) - if (!user) - throw new Error('Failed to temporarily authenticate unfound user'); - - req.user = user; - return next(); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(401).send({ - error: - 'Failed to temporarily authenticate user for complete account. Try logging in' - }); - } + req.user = user; + return next(); }; export default requireSignupAuth; diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index a27611f2d..e5b8898f3 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -1,6 +1,6 @@ -import * as Sentry from '@sentry/node'; import { Request, Response, NextFunction } from 'express'; import { validateMembership } from '../helpers/membership'; +import { UnauthorizedRequestError } from '../utils/errors'; type req = 'params' | 'body' | 'query'; @@ -36,11 +36,7 @@ const requireWorkspaceAuth = ({ return next(); } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(401).send({ - error: 'Failed workspace authorization' - }); + return next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'})) } }; }; diff --git a/backend/src/middleware/validateRequest.ts b/backend/src/middleware/validateRequest.ts index 4dcafc13b..484b02cab 100644 --- a/backend/src/middleware/validateRequest.ts +++ b/backend/src/middleware/validateRequest.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from 'express'; -import * as Sentry from '@sentry/node'; import { validationResult } from 'express-validator'; +import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; /** * Validate intended inputs on [req] via express-validator @@ -15,16 +15,12 @@ const validate = (req: Request, res: Response, next: NextFunction) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); + return next(BadRequestError({context: {errors: errors.array}})) } return next(); } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(401).send({ - error: "Looks like you're unauthenticated . Try logging in" - }); + return next(UnauthorizedRequestError({message: 'Unauthenticated requests are not allowed. Try logging in'})) } }; diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index b83ef728d..d36e32b91 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -10,6 +10,7 @@ import { export interface ISecret { _id: Types.ObjectId; + version: number; workspace: Types.ObjectId; type: string; user: Types.ObjectId; @@ -22,10 +23,18 @@ export interface ISecret { secretValueIV: string; secretValueTag: string; secretValueHash: string; + secretCommentCiphertext?: string; + secretCommentIV?: string; + secretCommentTag?: string; + secretCommentHash?: string; } const secretSchema = new Schema( { + version: { + type: Number, + required: true + }, workspace: { type: Schema.Types.ObjectId, ref: 'Workspace', @@ -77,6 +86,22 @@ const secretSchema = new Schema( secretValueHash: { type: String, required: true + }, + secretCommentCiphertext: { + type: String, + required: false + }, + secretCommentIV: { + type: String, // symmetric + required: false + }, + secretCommentTag: { + type: String, // symmetric + required: false + }, + secretCommentHash: { + type: String, + required: false } }, { diff --git a/backend/src/routes/auth.ts b/backend/src/routes/v1/auth.ts similarity index 79% rename from backend/src/routes/auth.ts rename to backend/src/routes/v1/auth.ts index 6be9c09f7..ace15421e 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -1,9 +1,9 @@ import express from 'express'; const router = express.Router(); import { body } from 'express-validator'; -import { requireAuth, validateRequest } from '../middleware'; -import { authController } from '../controllers'; -import { loginLimiter } from '../helpers/rateLimiter'; +import { requireAuth, validateRequest } from '../../middleware'; +import { authController } from '../../controllers/v1'; +import { loginLimiter } from '../../helpers/rateLimiter'; router.post('/token', validateRequest, authController.getNewToken); diff --git a/backend/src/routes/bot.ts b/backend/src/routes/v1/bot.ts similarity index 83% rename from backend/src/routes/bot.ts rename to backend/src/routes/v1/bot.ts index 3189bec44..90a0aa285 100644 --- a/backend/src/routes/bot.ts +++ b/backend/src/routes/v1/bot.ts @@ -6,9 +6,9 @@ import { requireBotAuth, requireWorkspaceAuth, validateRequest -} from '../middleware'; -import { botController } from '../controllers'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; +} from '../../middleware'; +import { botController } from '../../controllers/v1'; +import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; router.get( '/:workspaceId', diff --git a/backend/src/routes/index.ts b/backend/src/routes/v1/index.ts similarity index 100% rename from backend/src/routes/index.ts rename to backend/src/routes/v1/index.ts diff --git a/backend/src/routes/integration.ts b/backend/src/routes/v1/integration.ts similarity index 85% rename from backend/src/routes/integration.ts rename to backend/src/routes/v1/integration.ts index e6738a803..f16aad23b 100644 --- a/backend/src/routes/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -4,10 +4,10 @@ import { requireAuth, requireIntegrationAuth, validateRequest -} from '../middleware'; -import { ADMIN, MEMBER, GRANTED } from '../variables'; +} from '../../middleware'; +import { ADMIN, MEMBER, GRANTED } from '../../variables'; import { body, param } from 'express-validator'; -import { integrationController } from '../controllers'; +import { integrationController } from '../../controllers/v1'; router.patch( '/:integrationId', diff --git a/backend/src/routes/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts similarity index 89% rename from backend/src/routes/integrationAuth.ts rename to backend/src/routes/v1/integrationAuth.ts index ef80a2dcc..004706786 100644 --- a/backend/src/routes/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -6,9 +6,9 @@ import { requireWorkspaceAuth, requireIntegrationAuthorizationAuth, validateRequest -} from '../middleware'; -import { ADMIN, MEMBER, GRANTED } from '../variables'; -import { integrationAuthController } from '../controllers'; +} from '../../middleware'; +import { ADMIN, MEMBER, GRANTED } from '../../variables'; +import { integrationAuthController } from '../../controllers/v1'; router.get( '/integration-options', diff --git a/backend/src/routes/inviteOrg.ts b/backend/src/routes/v1/inviteOrg.ts similarity index 80% rename from backend/src/routes/inviteOrg.ts rename to backend/src/routes/v1/inviteOrg.ts index 16ab64f85..5c89b022f 100644 --- a/backend/src/routes/inviteOrg.ts +++ b/backend/src/routes/v1/inviteOrg.ts @@ -1,8 +1,8 @@ import express from 'express'; const router = express.Router(); import { body } from 'express-validator'; -import { requireAuth, validateRequest } from '../middleware'; -import { membershipOrgController } from '../controllers'; +import { requireAuth, validateRequest } from '../../middleware'; +import { membershipOrgController } from '../../controllers/v1'; router.post( '/signup', diff --git a/backend/src/routes/key.ts b/backend/src/routes/v1/key.ts similarity index 82% rename from backend/src/routes/key.ts rename to backend/src/routes/v1/key.ts index a67a729b1..094d18e72 100644 --- a/backend/src/routes/key.ts +++ b/backend/src/routes/v1/key.ts @@ -4,10 +4,10 @@ import { requireAuth, requireWorkspaceAuth, validateRequest -} from '../middleware'; +} from '../../middleware'; import { body, param } from 'express-validator'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; -import { keyController } from '../controllers'; +import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; +import { keyController } from '../../controllers/v1'; router.post( '/:workspaceId', diff --git a/backend/src/routes/membership.ts b/backend/src/routes/v1/membership.ts similarity index 83% rename from backend/src/routes/membership.ts rename to backend/src/routes/v1/membership.ts index 04d94f576..4d81aa6dc 100644 --- a/backend/src/routes/membership.ts +++ b/backend/src/routes/v1/membership.ts @@ -1,8 +1,8 @@ import express from 'express'; const router = express.Router(); import { body, param } from 'express-validator'; -import { requireAuth, validateRequest } from '../middleware'; -import { membershipController } from '../controllers'; +import { requireAuth, validateRequest } from '../../middleware'; +import { membershipController } from '../../controllers/v1'; router.get( // used for CLI (deprecate) '/:workspaceId/connect', diff --git a/backend/src/routes/membershipOrg.ts b/backend/src/routes/v1/membershipOrg.ts similarity index 78% rename from backend/src/routes/membershipOrg.ts rename to backend/src/routes/v1/membershipOrg.ts index b6cd6313f..c74d20449 100644 --- a/backend/src/routes/membershipOrg.ts +++ b/backend/src/routes/v1/membershipOrg.ts @@ -1,8 +1,8 @@ import express from 'express'; const router = express.Router(); import { param } from 'express-validator'; -import { requireAuth, validateRequest } from '../middleware'; -import { membershipOrgController } from '../controllers'; +import { requireAuth, validateRequest } from '../../middleware'; +import { membershipOrgController } from '../../controllers/v1'; router.post( // TODO diff --git a/backend/src/routes/organization.ts b/backend/src/routes/v1/organization.ts similarity index 95% rename from backend/src/routes/organization.ts rename to backend/src/routes/v1/organization.ts index 276e26095..1fff9b597 100644 --- a/backend/src/routes/organization.ts +++ b/backend/src/routes/v1/organization.ts @@ -5,9 +5,9 @@ import { requireAuth, requireOrganizationAuth, validateRequest -} from '../middleware'; -import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../variables'; -import { organizationController } from '../controllers'; +} from '../../middleware'; +import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables'; +import { organizationController } from '../../controllers/v1'; router.get( '/', diff --git a/backend/src/routes/password.ts b/backend/src/routes/v1/password.ts similarity index 94% rename from backend/src/routes/password.ts rename to backend/src/routes/v1/password.ts index 8032cba83..2cb811de5 100644 --- a/backend/src/routes/password.ts +++ b/backend/src/routes/v1/password.ts @@ -1,9 +1,9 @@ import express from 'express'; const router = express.Router(); import { body } from 'express-validator'; -import { requireAuth, requireSignupAuth, validateRequest } from '../middleware'; -import { passwordController } from '../controllers'; -import { passwordLimiter } from '../helpers/rateLimiter'; +import { requireAuth, requireSignupAuth, validateRequest } from '../../middleware'; +import { passwordController } from '../../controllers/v1'; +import { passwordLimiter } from '../../helpers/rateLimiter'; router.post( '/srp1', diff --git a/backend/src/routes/secret.ts b/backend/src/routes/v1/secret.ts similarity index 87% rename from backend/src/routes/secret.ts rename to backend/src/routes/v1/secret.ts index 98b3009de..e2dcb3368 100644 --- a/backend/src/routes/secret.ts +++ b/backend/src/routes/v1/secret.ts @@ -5,10 +5,10 @@ import { requireWorkspaceAuth, requireServiceTokenAuth, validateRequest -} from '../middleware'; +} from '../../middleware'; import { body, query, param } from 'express-validator'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; -import { secretController } from '../controllers'; +import { secretController } from '../../controllers/v1'; +import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; router.post( '/:workspaceId', diff --git a/backend/src/routes/serviceToken.ts b/backend/src/routes/v1/serviceToken.ts similarity index 85% rename from backend/src/routes/serviceToken.ts rename to backend/src/routes/v1/serviceToken.ts index 00195edee..57031f3e0 100644 --- a/backend/src/routes/serviceToken.ts +++ b/backend/src/routes/v1/serviceToken.ts @@ -5,10 +5,10 @@ import { requireWorkspaceAuth, requireServiceTokenAuth, validateRequest -} from '../middleware'; +} from '../../middleware'; import { body } from 'express-validator'; -import { ADMIN, MEMBER, GRANTED } from '../variables'; -import { serviceTokenController } from '../controllers'; +import { ADMIN, MEMBER, GRANTED } from '../../variables'; +import { serviceTokenController } from '../../controllers/v1'; // TODO: revoke service token diff --git a/backend/src/routes/signup.ts b/backend/src/routes/v1/signup.ts similarity index 89% rename from backend/src/routes/signup.ts rename to backend/src/routes/v1/signup.ts index 40b5929b0..3ea3b2738 100644 --- a/backend/src/routes/signup.ts +++ b/backend/src/routes/v1/signup.ts @@ -1,9 +1,9 @@ import express from 'express'; const router = express.Router(); import { body } from 'express-validator'; -import { requireSignupAuth, validateRequest } from '../middleware'; -import { signupController } from '../controllers'; -import { signupLimiter } from '../helpers/rateLimiter'; +import { requireSignupAuth, validateRequest } from '../../middleware'; +import { signupController } from '../../controllers/v1'; +import { signupLimiter } from '../../helpers/rateLimiter'; router.post( '/email/signup', diff --git a/backend/src/routes/stripe.ts b/backend/src/routes/v1/stripe.ts similarity index 71% rename from backend/src/routes/stripe.ts rename to backend/src/routes/v1/stripe.ts index ba5706562..cfcca77cb 100644 --- a/backend/src/routes/stripe.ts +++ b/backend/src/routes/v1/stripe.ts @@ -1,6 +1,6 @@ import express from 'express'; const router = express.Router(); -import { stripeController } from '../controllers'; +import { stripeController } from '../../controllers/v1'; router.post('/webhook', stripeController.handleWebhook); diff --git a/backend/src/routes/user.ts b/backend/src/routes/v1/user.ts similarity index 58% rename from backend/src/routes/user.ts rename to backend/src/routes/v1/user.ts index 4a25c48fb..393978922 100644 --- a/backend/src/routes/user.ts +++ b/backend/src/routes/v1/user.ts @@ -1,7 +1,7 @@ import express from 'express'; const router = express.Router(); -import { requireAuth } from '../middleware'; -import { userController } from '../controllers'; +import { requireAuth } from '../../middleware'; +import { userController } from '../../controllers/v1'; router.get('/', requireAuth, userController.getUser); diff --git a/backend/src/routes/userAction.ts b/backend/src/routes/v1/userAction.ts similarity index 73% rename from backend/src/routes/userAction.ts rename to backend/src/routes/v1/userAction.ts index 5ea454156..544b948e8 100644 --- a/backend/src/routes/userAction.ts +++ b/backend/src/routes/v1/userAction.ts @@ -1,8 +1,8 @@ import express from 'express'; const router = express.Router(); -import { requireAuth, validateRequest } from '../middleware'; +import { requireAuth, validateRequest } from '../../middleware'; import { body, query } from 'express-validator'; -import { userActionController } from '../controllers'; +import { userActionController } from '../../controllers/v1'; router.post( '/', diff --git a/backend/src/routes/workspace.ts b/backend/src/routes/v1/workspace.ts similarity index 92% rename from backend/src/routes/workspace.ts rename to backend/src/routes/v1/workspace.ts index 1d20c102a..6a1ae9034 100644 --- a/backend/src/routes/workspace.ts +++ b/backend/src/routes/v1/workspace.ts @@ -1,13 +1,13 @@ import express from 'express'; const router = express.Router(); -import { body, param } from 'express-validator'; +import { body, param, query } from 'express-validator'; import { requireAuth, requireWorkspaceAuth, validateRequest -} from '../middleware'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; -import { workspaceController, membershipController } from '../controllers'; +} from '../../middleware'; +import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; +import { workspaceController, membershipController } from '../../controllers/v1'; router.get( '/:workspaceId/keys', diff --git a/backend/src/routes/v2/index.ts b/backend/src/routes/v2/index.ts new file mode 100644 index 000000000..6e6758753 --- /dev/null +++ b/backend/src/routes/v2/index.ts @@ -0,0 +1,7 @@ +import secret from './secret'; +import workspace from './workspace'; + +export { + secret, + workspace +} diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts new file mode 100644 index 000000000..17a91d39c --- /dev/null +++ b/backend/src/routes/v2/secret.ts @@ -0,0 +1,4 @@ +import express from 'express'; +const router = express.Router(); + +export default router; diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts new file mode 100644 index 000000000..98eacd9ea --- /dev/null +++ b/backend/src/routes/v2/workspace.ts @@ -0,0 +1,176 @@ +import express from 'express'; +const router = express.Router(); +import { body, param, query } from 'express-validator'; +import { + requireAuth, + requireWorkspaceAuth, + requireServiceTokenAuth, + validateRequest +} from '../../middleware'; +import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; +import { membershipController } from '../../controllers/v1'; +import { workspaceController } from '../../controllers/v2'; + +router.get( + '/:workspaceId/keys', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.getWorkspacePublicKeys +); + +router.get( + '/:workspaceId/users', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.getWorkspaceMemberships +); + +router.get('/', requireAuth, workspaceController.getWorkspaces); + +router.get( + '/:workspaceId', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.getWorkspace +); + +router.post( + '/', + requireAuth, + body('workspaceName').exists().trim().notEmpty(), + body('organizationId').exists().trim().notEmpty(), + validateRequest, + workspaceController.createWorkspace +); + +router.delete( + '/:workspaceId', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.deleteWorkspace +); + +router.post( + '/:workspaceId/name', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + param('workspaceId').exists().trim(), + body('name').exists().trim().notEmpty(), + validateRequest, + workspaceController.changeWorkspaceName +); + +router.post( + '/:workspaceId/invite-signup', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + body('email').exists().trim().notEmpty(), + validateRequest, + membershipController.inviteUserToWorkspace +); + +router.get( + '/:workspaceId/integrations', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.getWorkspaceIntegrations +); + +router.get( + '/:workspaceId/authorizations', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.getWorkspaceIntegrationAuthorizations +); + +router.get( // TODO: modify + '/:workspaceId/service-tokens', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.getWorkspaceServiceTokens +); + +router.post( + '/:workspaceId/secrets', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + body('secrets').exists(), + body('keys').exists(), + body('environment').exists().trim().notEmpty(), + body('channel'), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.pushWorkspaceSecrets +); + +router.get( + '/:workspaceId/secrets', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + query('environment').exists().trim(), + query('channel'), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.pullSecrets +); + +router.get( // TODO: modify based on upcoming serviceTokenData changes + '/:workspaceId/secrets-service-token', + requireServiceTokenAuth, + query('environment').exists().trim(), + query('channel'), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.pullSecretsServiceToken +); + + +export default router; diff --git a/backend/src/services/PostHogClient.ts b/backend/src/services/PostHogClient.ts index 4096ac8ec..4ce0117f0 100644 --- a/backend/src/services/PostHogClient.ts +++ b/backend/src/services/PostHogClient.ts @@ -5,8 +5,16 @@ import { POSTHOG_PROJECT_API_KEY, TELEMETRY_ENABLED } from '../config'; +import { getLogger } from '../utils/logger'; -console.log('TELEMETRY_ENABLED: ', TELEMETRY_ENABLED); +if(TELEMETRY_ENABLED){ + getLogger("backend-main").info([ + "", + "Infisical collects telemetry data about general usage.", + "The data helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth for investors as we support Infisical as open-source software.", + "To opt out of telemetry, you can set `TELEMETRY_ENABLED=false` within the environment variables", + ].join('\n')) +} let postHogClient: any; if (NODE_ENV === 'production' && TELEMETRY_ENABLED) { diff --git a/backend/src/services/database.ts b/backend/src/services/database.ts index 5fc4955f1..85f39c1b2 100644 --- a/backend/src/services/database.ts +++ b/backend/src/services/database.ts @@ -1,10 +1,10 @@ -/* eslint-disable no-console */ import mongoose from 'mongoose'; +import { getLogger } from '../utils/logger'; export const initDatabase = (MONGO_URL: string) => { mongoose .connect(MONGO_URL) - .then(() => console.log('Successfully connected to DB')) - .catch((e) => console.log('Failed to connect to DB ', e)); + .then(() => getLogger("database").info("Database connection established")) + .catch((e) => getLogger("database").error(`Unable to establish Database connection due to the error.\n${e}`)); return mongoose.connection; }; diff --git a/backend/src/services/health.ts b/backend/src/services/health.ts index b4ab2a97b..9c441ba9d 100644 --- a/backend/src/services/health.ts +++ b/backend/src/services/health.ts @@ -1,10 +1,10 @@ -/* eslint-disable no-console */ import mongoose from 'mongoose'; import { createTerminus } from '@godaddy/terminus'; +import { getLogger } from '../utils/logger'; export const setUpHealthEndpoint = (server: T) => { const onSignal = () => { - console.log('Server is starting clean-up'); + getLogger('backend-main').info('Server is starting clean-up'); return Promise.all([ new Promise((resolve) => { if (mongoose.connection && mongoose.connection.readyState == 1) { diff --git a/backend/src/services/smtp.ts b/backend/src/services/smtp.ts index 14d543439..12841eee7 100644 --- a/backend/src/services/smtp.ts +++ b/backend/src/services/smtp.ts @@ -1,13 +1,14 @@ import nodemailer from 'nodemailer'; import { SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_SECURE } from '../config'; +import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from '../variables'; import SMTPConnection from 'nodemailer/lib/smtp-connection'; import * as Sentry from '@sentry/node'; const mailOpts: SMTPConnection.Options = { host: SMTP_HOST, - secure: SMTP_SECURE as boolean, port: SMTP_PORT as number }; + if (SMTP_USERNAME && SMTP_PASSWORD) { mailOpts.auth = { user: SMTP_USERNAME, @@ -15,6 +16,23 @@ if (SMTP_USERNAME && SMTP_PASSWORD) { }; } +if (SMTP_SECURE) { + switch (SMTP_HOST) { + case SMTP_HOST_SENDGRID: + mailOpts.requireTLS = true; + break; + case SMTP_HOST_MAILGUN: + mailOpts.requireTLS = true; + mailOpts.tls = { + ciphers: 'TLSv1.2' + } + break; + default: + mailOpts.secure = true; + break; + } +} + export const initSmtp = () => { const transporter = nodemailer.createTransport(mailOpts); transporter diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index 9a9e81449..319562fd0 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -1,5 +1,6 @@ import * as express from 'express'; + // TODO: fix (any) types declare global { namespace Express { diff --git a/backend/src/utils/errors.ts b/backend/src/utils/errors.ts new file mode 100644 index 000000000..40c467131 --- /dev/null +++ b/backend/src/utils/errors.ts @@ -0,0 +1,116 @@ +import RequestError, { LogLevel, RequestErrorContext } from "./requestError" + +//* ----->[GENERAL HTTP ERRORS]<----- +export const RouteNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.INFO, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'route_not_found', + message: error?.message ?? 'The requested source was not found', + context: error?.context, + stack: error?.stack +}) + +export const MethodNotAllowedError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.INFO, + statusCode: error?.statusCode ?? 405, + type: error?.type ?? 'method_not_allowed', + message: error?.message ?? 'The requested method is not allowed for the resource', + context: error?.context, + stack: error?.stack +}) + +export const UnauthorizedRequestError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.INFO, + statusCode: error?.statusCode ?? 401, + type: error?.type ?? 'unauthorized', + message: error?.message ?? 'You are not authorized to access this resource', + context: error?.context, + stack: error?.stack +}) + +export const ForbiddenRequestError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.INFO, + statusCode: error?.statusCode ?? 403, + type: error?.type ?? 'forbidden', + message: error?.message ?? 'You are not allowed to access this resource', + context: error?.context, + stack: error?.stack +}) + +export const BadRequestError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.INFO, + statusCode: error?.statusCode ?? 400, + type: error?.type ?? 'bad_request', + message: error?.message ?? 'The request is invalid or cannot be served', + context: error?.context, + stack: error?.stack +}) + +export const InternalServerError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 500, + type: error?.type ?? 'internal_server_error', + message: error?.message ?? 'The server encountered an error while processing the request', + context: error?.context, + stack: error?.stack +}) + +export const ServiceUnavailableError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 503, + type: error?.type ?? 'service_unavailable', + message: error?.message ?? 'The service is currently unavailable. Please try again later.', + context: error?.context, + stack: error?.stack +}) + +export const ValidationError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 400, + type: error?.type ?? 'validation_error', + message: error?.message ?? 'The request failed validation', + context: error?.context, + stack: error?.stack +}) + +//* ----->[INTEGRATION ERRORS]<----- +export const IntegrationNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'integration_not_found_error', + message: error?.message ?? 'The requested integration was not found', + context: error?.context, + stack: error?.stack +}) + +//* ----->[WORKSPACE ERRORS]<----- +export const WorkspaceNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'workspace_not_found_error', + message: error?.message ?? 'The requested workspace was not found', + context: error?.context, + stack: error?.stack +}) + +//* ----->[ORGANIZATION ERRORS]<----- +export const OrganizationNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'organization_not_found_error', + message: error?.message ?? 'The requested organization was not found', + context: error?.context, + stack: error?.stack +}) + +//* ----->[ACCOUNT ERRORS]<----- +export const AccountNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'account_not_found_error', + message: error?.message ?? 'The requested account was not found', + context: error?.context, + stack: error?.stack +}) + +//* ----->[MISC ERRORS]<----- diff --git a/backend/src/utils/logger.ts b/backend/src/utils/logger.ts new file mode 100644 index 000000000..64c65ea49 --- /dev/null +++ b/backend/src/utils/logger.ts @@ -0,0 +1,65 @@ +/* eslint-disable no-console */ +import { createLogger, format, transports } from 'winston'; +import LokiTransport from 'winston-loki'; +import { LOKI_HOST, NODE_ENV } from '../config'; + +const { combine, colorize, label, printf, splat, timestamp } = format; + +const logFormat = (prefix: string) => combine( + timestamp(), + splat(), + label({ label: prefix }), + printf((info) => `${info.timestamp} ${info.label} ${info.level}: ${info.message}`) +); + +const createLoggerWithLabel = (level: string, label: string) => { + const _level = level.toLowerCase() || 'info' + //* Always add Console output to transports + const _transports: any[] = [ + new transports.Console({ + format: combine( + colorize(), + logFormat(label), + // format.json() + ) + }) + ] + //* Add LokiTransport if it's enabled + if(LOKI_HOST !== undefined){ + _transports.push( + new LokiTransport({ + host: LOKI_HOST, + handleExceptions: true, + handleRejections: true, + batching: true, + level: _level, + timeout: 30000, + format: format.combine( + format.json() + ), + labels: {app: process.env.npm_package_name, version: process.env.npm_package_version, environment: NODE_ENV}, + onConnectionError: (err: Error)=> console.error('Connection error while connecting to Loki Server.\n', err) + }) + ) + } + + + return createLogger({ + level: _level, + transports: _transports, + format: format.combine( + logFormat(label), + format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'label'] }) + ) + }); +} + +const DEFAULT_LOGGERS = { + "backend-main": createLoggerWithLabel('info', '[IFSC:backend-main]'), + "database": createLoggerWithLabel('info', '[IFSC:database]'), +} +type LoggerNames = keyof typeof DEFAULT_LOGGERS + +export const getLogger = (loggerName: LoggerNames) => { + return DEFAULT_LOGGERS[loggerName] +} diff --git a/backend/src/utils/patchAsyncRoutes.js b/backend/src/utils/patchAsyncRoutes.js new file mode 100644 index 000000000..24fe007f9 --- /dev/null +++ b/backend/src/utils/patchAsyncRoutes.js @@ -0,0 +1,69 @@ +/* +Original work Copyright (c) 2016, Nikolay Nemshilov +Modified work Copyright (c) 2016, David Banham + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +*/ + +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-env node */ +const Layer = require('express/lib/router/layer'); +const Router = require('express/lib/router'); + +const last = (arr = []) => arr[arr.length - 1]; +const noop = Function.prototype; + +function copyFnProps(oldFn, newFn) { + Object.keys(oldFn).forEach((key) => { + newFn[key] = oldFn[key]; + }); + return newFn; +} + +function wrap(fn) { + const newFn = function newFn(...args) { + const ret = fn.apply(this, args); + const next = (args.length === 5 ? args[2] : last(args)) || noop; + if (ret && ret.catch) ret.catch(err => next(err)); + return ret; + }; + Object.defineProperty(newFn, 'length', { + value: fn.length, + writable: false, + }); + return copyFnProps(fn, newFn); +} + +function patchRouterParam() { + const originalParam = Router.prototype.constructor.param; + Router.prototype.constructor.param = function param(name, fn) { + fn = wrap(fn); + return originalParam.call(this, name, fn); + }; +} + +Object.defineProperty(Layer.prototype, 'handle', { + enumerable: true, + get() { + return this.__handle; + }, + set(fn) { + fn = wrap(fn); + this.__handle = fn; + }, +}); + +module.exports = { + patchRouterParam +}; diff --git a/backend/src/utils/requestError.ts b/backend/src/utils/requestError.ts new file mode 100644 index 000000000..da2803da7 --- /dev/null +++ b/backend/src/utils/requestError.ts @@ -0,0 +1,113 @@ +import { Request } from 'express' +import { VERBOSE_ERROR_OUTPUT } from '../config' + +export enum LogLevel { + DEBUG = 100, + INFO = 200, + NOTICE = 250, + WARNING = 300, + ERROR = 400, + CRITICAL = 500, + ALERT = 550, + EMERGENCY = 600, +} + +export type RequestErrorContext = { + logLevel?: LogLevel, + statusCode: number, + type: string, + message: string, + context?: Record, + stack?: string|undefined +} + +export default class RequestError extends Error{ + + private _logLevel: LogLevel + private _logName: string + statusCode: number + type: string + context: Record + extra: Record[] + private stacktrace: string|undefined|string[] + + constructor( + {logLevel, statusCode, type, message, context, stack} : RequestErrorContext + ){ + super(message) + this._logLevel = logLevel || LogLevel.INFO + this._logName = LogLevel[this._logLevel] + this.statusCode = statusCode + this.type = type + this.context = context || {} + this.extra = [] + + if(stack) this.stack = stack + else Error.captureStackTrace(this, this.constructor) + this.stacktrace = this.stack?.split('\n') + } + + static convertFrom(error: Error) { + //This error was not handled by error handler. Please report this incident to the staff. + return new RequestError({ + logLevel: LogLevel.ERROR, + statusCode: 500, + type: 'internal_server_error', + message: 'This error was not handled by error handler. Please report this incident to the staff', + context: { + message: error.message, + name: error.name + }, + stack: error.stack + }) + } + + get level(){ return this._logLevel } + get levelName(){ return this._logName } + + withTags(...tags: string[]|number[]){ + this.context['tags'] = Object.assign(tags, this.context['tags']) + return this + } + + withExtras(...extras: Record[]){ + this.extra = Object.assign(extras, this.extra) + return this + } + + private _omit(obj: any, keys: string[]): typeof obj{ + const exclude = new Set(keys) + obj = Object.fromEntries(Object.entries(obj).filter(e => !exclude.has(e[0]))) + return obj + } + + public format(req: Request){ + let _context = Object.assign({ + stacktrace: this.stacktrace + }, this.context) + + //* Omit sensitive information from context that can leak internal workings of this program if user is not developer + if(!VERBOSE_ERROR_OUTPUT){ + _context = this._omit(_context, [ + 'stacktrace', + 'exception', + ]) + } + + const formatObject = { + type: this.type, + message: this.message, + context: _context, + level: this.level, + level_name: this.levelName, + status_code: this.statusCode, + datetime_iso: new Date().toISOString(), + application: process.env.npm_package_name || 'unknown', + request_id: req.headers["Request-Id"], + extra: this.extra + } + + return formatObject + + } +} \ No newline at end of file diff --git a/backend/src/variables/action.ts b/backend/src/variables/action.ts deleted file mode 100644 index 1f913bbe9..000000000 --- a/backend/src/variables/action.ts +++ /dev/null @@ -1,5 +0,0 @@ -const ACTION_PUSH_TO_HEROKU = 'pushToHeroku'; - -export { - ACTION_PUSH_TO_HEROKU -} \ No newline at end of file diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index c69ed8176..e284d6d5c 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -32,9 +32,9 @@ import { GRANTED } from './organization'; import { SECRET_SHARED, SECRET_PERSONAL } from './secret'; -import { PLAN_STARTER, PLAN_PRO } from './stripe'; import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event'; -import { ACTION_PUSH_TO_HEROKU } from './action'; +import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp'; +import { PLAN_STARTER, PLAN_PRO } from './stripe'; export { OWNER, @@ -44,8 +44,6 @@ export { ACCEPTED, COMPLETED, GRANTED, - PLAN_STARTER, - PLAN_PRO, SECRET_SHARED, SECRET_PERSONAL, ENV_DEV, @@ -69,6 +67,9 @@ export { INTEGRATION_GITHUB_API_URL, EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS, - ACTION_PUSH_TO_HEROKU, - INTEGRATION_OPTIONS + INTEGRATION_OPTIONS, + SMTP_HOST_SENDGRID, + SMTP_HOST_MAILGUN, + PLAN_STARTER, + PLAN_PRO, }; diff --git a/backend/src/variables/smtp.ts b/backend/src/variables/smtp.ts new file mode 100644 index 000000000..4db7c9f12 --- /dev/null +++ b/backend/src/variables/smtp.ts @@ -0,0 +1,7 @@ +const SMTP_HOST_SENDGRID = 'smtp.sendgrid.net'; +const SMTP_HOST_MAILGUN = 'smtp.mailgun.org'; + +export { + SMTP_HOST_SENDGRID, + SMTP_HOST_MAILGUN +} \ No newline at end of file diff --git a/cli/go.mod b/cli/go.mod index 1c175e2f0..86cc1763f 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -3,24 +3,37 @@ module github.com/Infisical/infisical-merge go 1.19 require ( + github.com/99designs/keyring v1.2.2 github.com/spf13/cobra v1.6.1 golang.org/x/crypto v0.3.0 + golang.org/x/term v0.3.0 ) require ( - github.com/alessio/shellescape v1.4.1 // indirect + github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/danieljoos/wincred v1.1.2 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/dvsekhvalnov/jose2go v1.5.0 // indirect + github.com/go-openapi/errors v0.20.2 // indirect + github.com/go-openapi/strfmt v0.21.3 // indirect + github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mitchellh/mapstructure v1.3.3 // indirect + github.com/mtibben/percent v0.2.1 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + go.mongodb.org/mongo-driver v1.10.0 // indirect golang.org/x/net v0.2.0 // indirect - golang.org/x/sys v0.2.0 // indirect + golang.org/x/sys v0.3.0 // indirect ) require ( github.com/go-resty/resty/v2 v2.7.0 github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/jedib0t/go-pretty v4.3.0+incompatible github.com/manifoldco/promptui v0.9.0 github.com/sirupsen/logrus v1.9.0 github.com/spf13/pflag v1.0.5 // indirect - github.com/zalando/go-keyring v0.2.1 ) diff --git a/cli/go.sum b/cli/go.sum index 975332397..3419b8051 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -1,5 +1,9 @@ -github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= -github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= +github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= @@ -10,23 +14,57 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= +github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8= +github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o= +github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= -github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= +github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -34,32 +72,52 @@ github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/zalando/go-keyring v0.2.1 h1:MBRN/Z8H4U5wEKXiD67YbDAr5cj/DOStmSga70/2qKc= -github.com/zalando/go-keyring v0.2.1/go.mod h1:g63M2PPn0w5vjmEbwAX3ib5I+41zdm4esSETOn9Y6Dw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg= +go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index 29e2c4935..9c0c22930 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -91,7 +91,9 @@ var loginCmd = &cobra.Command{ err = util.StoreUserCredsInKeyRing(userCredentialsToBeStored) if err != nil { - log.Errorln("Unable to store your credentials in system key ring") + currentVault, _ := util.GetCurrentVaultBackend() + log.Errorf("Unable to store your credentials in system vault [%s]. Rerun with flag -d to see full logs", currentVault) + log.Errorln("To trouble shoot further, read https://infisical.com/docs/cli/faq") log.Debugln(err) return } diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go index 7317b3eea..173df36df 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.11", + Version: "0.1.16", } // Execute adds all child commands to the root command and sets flags appropriately. @@ -31,4 +31,6 @@ func init() { rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") rootCmd.PersistentFlags().BoolVarP(&debugLogging, "debug", "d", false, "Enable verbose logging") rootCmd.PersistentFlags().StringVar(&util.INFISICAL_URL, "domain", "https://app.infisical.com/api", "Point the CLI to your own backend") + // rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { + // } } diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index 7518fe98d..6e0133c8e 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -60,6 +60,13 @@ var runCmd = &cobra.Command{ return } + secretOverriding, err := cmd.Flags().GetBool("secret-overriding") + if err != nil { + log.Errorln("Unable to parse the secret-overriding flag") + log.Debugln(err) + return + } + shouldExpandSecrets, err := cmd.Flags().GetBool("expand") if err != nil { log.Errorln("Unable to parse the substitute flag") @@ -84,6 +91,10 @@ var runCmd = &cobra.Command{ secrets = util.SubstituteSecrets(secrets) } + if secretOverriding { + secrets = util.OverrideWithPersonalSecrets(secrets) + } + if cmd.Flags().Changed("command") { command := cmd.Flag("command").Value.String() err = executeMultipleCommandWithEnvs(command, secrets) @@ -108,6 +119,7 @@ func init() { runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from") runCmd.Flags().String("projectId", "", "The project ID from which your secrets should be pulled from") runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") + runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets with the same name over shared secrets") runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") } diff --git a/cli/packages/cmd/vault.go b/cli/packages/cmd/vault.go new file mode 100644 index 000000000..a2b03dbec --- /dev/null +++ b/cli/packages/cmd/vault.go @@ -0,0 +1,98 @@ +/* +Copyright ยฉ 2022 NAME HERE +*/ +package cmd + +import ( + "github.com/99designs/keyring" + "github.com/Infisical/infisical-merge/packages/util" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var vaultSetCmd = &cobra.Command{ + Example: `infisical vault set pass`, + Use: "set [vault-name]", + Short: "Used to set the vault backend to store your login details securely at rest", + DisableFlagsInUseLine: true, + PreRun: toggleDebug, + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + wantedVaultTypeName := args[0] + currentVaultBackend, err := util.GetCurrentVaultBackend() + if err != nil { + log.Errorf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err) + return + } + + if wantedVaultTypeName == string(currentVaultBackend) { + log.Errorf("You are already on vault backend [%s]", currentVaultBackend) + return + } + + if isVaultToSwitchToValid(wantedVaultTypeName) { + configFile, err := util.GetConfigFile() + if err != nil { + log.Errorf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err) + return + } + + configFile.VaultBackendType = keyring.BackendType(wantedVaultTypeName) // save selected vault + configFile.LoggedInUserEmail = "" // reset the logged in user to prompt them to re login + + err = util.WriteConfigFile(&configFile) + if err != nil { + log.Errorf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]", wantedVaultTypeName, err) + return + } + + log.Infof("Successfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]", currentVaultBackend, wantedVaultTypeName) + } else { + log.Errorf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, keyring.AvailableBackends()) + } + }, +} + +// runCmd represents the run command +var vaultCmd = &cobra.Command{ + Use: "vault", + Short: "Used to manage where your Infisical login token is saved on your machine", + DisableFlagsInUseLine: true, + PreRun: toggleDebug, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + printAvailableVaultBackends() + }, +} + +func printAvailableVaultBackends() { + log.Infof("The following vaults are available on your system:") + for _, backend := range keyring.AvailableBackends() { + log.Infof("- %s", backend) + } + + currentVaultBackend, err := util.GetCurrentVaultBackend() + if err != nil { + log.Errorf("printAvailableVaultBackends: unable to print the available vault backend because of error [err=%s]", err) + } + + log.Infof("\nYou are currently using [%s] vault to store your login credentials", string(currentVaultBackend)) +} + +// Checks if the vault that the user wants to switch to is a valid available vault +func isVaultToSwitchToValid(vaultNameToSwitchTo string) bool { + isFound := false + for _, backend := range keyring.AvailableBackends() { + if vaultNameToSwitchTo == string(backend) { + isFound = true + break + } + } + + return isFound +} + +func init() { + vaultCmd.AddCommand(vaultSetCmd) + rootCmd.AddCommand(vaultCmd) +} diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 708a9ed8e..8ba1c4627 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -1,5 +1,7 @@ package models +import "github.com/99designs/keyring" + type UserCredentials struct { Email string `json:"email"` PrivateKey string `json:"privateKey"` @@ -8,12 +10,14 @@ type UserCredentials struct { // The file struct for Infisical config file type ConfigFile struct { - LoggedInUserEmail string `json:"loggedInUserEmail"` + LoggedInUserEmail string `json:"loggedInUserEmail"` + VaultBackendType keyring.BackendType `json:"vaultBackendType"` } type SingleEnvironmentVariable struct { Key string `json:"key"` Value string `json:"value"` + Type string `json:"type"` } type WorkspaceConfigFile struct { diff --git a/cli/packages/util/common.go b/cli/packages/util/common.go index 37c881994..f3ee274b3 100644 --- a/cli/packages/util/common.go +++ b/cli/packages/util/common.go @@ -19,6 +19,7 @@ func GetHomeDir() (string, error) { return directory, err } +// write file to given path. If path does not exist throw error func WriteToFile(fileName string, dataToWrite []byte, filePerm os.FileMode) error { err := os.WriteFile(fileName, dataToWrite, filePerm) if err != nil { diff --git a/cli/packages/util/config.go b/cli/packages/util/config.go index 0ee04b013..48bb57241 100644 --- a/cli/packages/util/config.go +++ b/cli/packages/util/config.go @@ -24,8 +24,15 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error { } } + // get existing config + existingConfigFile, err := GetConfigFile() + if err != nil { + return fmt.Errorf("writeInitalConfig: unable to write config file because [err=%s]", err) + } + configFile := models.ConfigFile{ LoggedInUserEmail: userCredentials.Email, + VaultBackendType: existingConfigFile.VaultBackendType, } configFileMarshalled, err := json.Marshal(configFile) @@ -152,3 +159,62 @@ func GetAllWorkSpaceConfigsStartingFromCurrentPath() (workspaces []models.Worksp return listOfWorkSpaceConfigs, nil } + +// Get the infisical config file and if it doesn't exist, return empty config model, otherwise raise error +func GetConfigFile() (models.ConfigFile, error) { + fullConfigFilePath, _, err := GetFullConfigFilePath() + if err != nil { + return models.ConfigFile{}, err + } + + configFileAsBytes, err := os.ReadFile(fullConfigFilePath) + if err != nil { + if err, ok := err.(*os.PathError); ok { + return models.ConfigFile{}, nil + } else { + return models.ConfigFile{}, err + } + } + + var configFile models.ConfigFile + err = json.Unmarshal(configFileAsBytes, &configFile) + if err != nil { + return models.ConfigFile{}, err + } + + return configFile, nil +} + +// Write a ConfigFile to disk. Raise error if unable to save the model to ask +func WriteConfigFile(configFile *models.ConfigFile) error { + fullConfigFilePath, fullConfigFileDirPath, err := GetFullConfigFilePath() + if err != nil { + return fmt.Errorf("writeConfigFile: unable to write config file because an error occurred when getting config file path [err=%s]", err) + } + + configFileMarshalled, err := json.Marshal(configFile) + if err != nil { + return fmt.Errorf("writeConfigFile: unable to write config file because an error occurred when marshalling the config file [err=%s]", err) + } + + // check if config folder exists and if not create it + if _, err := os.Stat(fullConfigFileDirPath); errors.Is(err, os.ErrNotExist) { + err := os.Mkdir(fullConfigFileDirPath, os.ModePerm) + if err != nil { + return err + } + } + + // Create file in directory + err = os.WriteFile(fullConfigFilePath, configFileMarshalled, os.ModePerm) + if err != nil { + return fmt.Errorf("writeConfigFile: Unable to write to file [err=%s]", err) + } + + if err != nil { + return fmt.Errorf("writeConfigFile: unable to write config file because an error occurred when write the config to file [err=%s]", err) + + } + + return nil +} diff --git a/cli/packages/util/credentials.go b/cli/packages/util/credentials.go index c5396f698..80c98fa9a 100644 --- a/cli/packages/util/credentials.go +++ b/cli/packages/util/credentials.go @@ -3,12 +3,11 @@ package util import ( "encoding/json" "fmt" - "os" + "github.com/99designs/keyring" "github.com/Infisical/infisical-merge/packages/models" "github.com/go-resty/resty/v2" log "github.com/sirupsen/logrus" - "github.com/zalando/go-keyring" ) const SERVICE_NAME = "infisical" @@ -17,32 +16,48 @@ const SERVICE_NAME = "infisical" func StoreUserCredsInKeyRing(userCred *models.UserCredentials) error { userCredMarshalled, err := json.Marshal(userCred) if err != nil { - return fmt.Errorf("Something went wrong when marshalling user creds:", err) + return fmt.Errorf("StoreUserCredsInKeyRing: something went wrong when marshalling user creds [err=%s]", err) } - err = keyring.Set(SERVICE_NAME, userCred.Email, string(userCredMarshalled)) + // Get keyring + configuredKeyring, err := GetKeyRing() if err != nil { - return fmt.Errorf("Unable to store user credentials:", err) + return fmt.Errorf("StoreUserCredsInKeyRing: unable to get keyring instance with [err=%s]", err) + } + + err = configuredKeyring.Set(keyring.Item{ + Key: userCred.Email, + Data: []byte(string(userCredMarshalled)), + }) + + if err != nil { + return fmt.Errorf("StoreUserCredsInKeyRing: unable to store user credentials because [err=%s]", err) } return err } func GetUserCredsFromKeyRing(userEmail string) (credentials models.UserCredentials, err error) { - credentialsString, err := keyring.Get(SERVICE_NAME, userEmail) + // Get keyring + configuredKeyring, err := GetKeyRing() if err != nil { - return models.UserCredentials{}, fmt.Errorf("Unable to get key from Keyring:", err) + return models.UserCredentials{}, fmt.Errorf("GetUserCredsFromKeyRing: unable to get keyring instance with [err=%s]", err) + } + + credentialsValue, err := configuredKeyring.Get(userEmail) + if err != nil { + return models.UserCredentials{}, fmt.Errorf("GetUserCredsFromKeyRing: unable to get key from Keyring. could not find login credentials in your Keyring. This is common if you have switched vault backend recently. If so, please login in again and retry [err=%s]", err) } var userCredentials models.UserCredentials - err = json.Unmarshal([]byte(credentialsString), &userCredentials) + err = json.Unmarshal([]byte(credentialsValue.Data), &userCredentials) if err != nil { - return models.UserCredentials{}, fmt.Errorf("Something went wrong when unmarshalling user creds:", err) + return models.UserCredentials{}, fmt.Errorf("getUserCredsFromKeyRing: Something went wrong when unmarshalling user creds [err=%s]", err) } if err != nil { - return models.UserCredentials{}, fmt.Errorf("Unable to store user credentials", err) + return models.UserCredentials{}, fmt.Errorf("GetUserCredsFromKeyRing: Unable to store user credentials [err=%s]", err) } return userCredentials, err @@ -50,23 +65,13 @@ func GetUserCredsFromKeyRing(userEmail string) (credentials models.UserCredentia func IsUserLoggedIn() (hasUserLoggedIn bool, theUsersEmail string, err error) { if ConfigFileExists() { - fullConfigFilePath, _, err := GetFullConfigFilePath() + configFile, err := GetConfigFile() if err != nil { - log.Debugln("Error gettting full path:", err) - return false, "", err + return false, "", fmt.Errorf("IsUserLoggedIn: unable to get logged in user from config file [err=%s]", err) } - configFileAsBytes, err := os.ReadFile(fullConfigFilePath) - if err != nil { - log.Debugln("Unable to read config file:", err) - return false, "", err - } - - var configFile models.ConfigFile - err = json.Unmarshal(configFileAsBytes, &configFile) - if err != nil { - log.Debugln("Unable to unmarshal config file:", err) - return false, "", err + if configFile.LoggedInUserEmail == "" { + return false, "", nil } userCreds, err := GetUserCredsFromKeyRing(configFile.LoggedInUserEmail) @@ -89,7 +94,7 @@ func IsUserLoggedIn() (hasUserLoggedIn bool, theUsersEmail string, err error) { if response.StatusCode() > 299 { log.Infoln("Login expired, please login again.") - return false, "", fmt.Errorf("Login expired, please login again.") + return false, "", fmt.Errorf("GetUserCredsFromKeyRing: Login expired, please login again.") } return true, configFile.LoggedInUserEmail, nil diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index c127111b1..f88aed96b 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -14,6 +14,9 @@ import ( "golang.org/x/crypto/nacl/box" ) +const PERSONAL_SECRET_TYPE_NAME = "personal" +const SHARED_SECRET_TYPE_NAME = "shared" + 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. @@ -78,6 +81,7 @@ func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string, env := models.SingleEnvironmentVariable{ Key: string(plainTextKey), Value: string(plainTextValue), + Type: string(secret.Type), } listOfEnv = append(listOfEnv, env) @@ -187,6 +191,7 @@ func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string, env := models.SingleEnvironmentVariable{ Key: string(plainTextKey), Value: string(plainTextValue), + Type: string(secret.Type), } listOfEnv = append(listOfEnv, env) @@ -335,9 +340,48 @@ func SubstituteSecrets(secrets []models.SingleEnvironmentVariable) []models.Sing expandedSecrets = append(expandedSecrets, models.SingleEnvironmentVariable{ Key: secret.Key, Value: expandedVariable, + Type: secret.Type, }) } return expandedSecrets } + +// if two secrets with the same name are found, the one that has type `personal` will be in the returned list +func OverrideWithPersonalSecrets(secrets []models.SingleEnvironmentVariable) []models.SingleEnvironmentVariable { + personalSecret := make(map[string]models.SingleEnvironmentVariable) + sharedSecret := make(map[string]models.SingleEnvironmentVariable) + secretsToReturn := []models.SingleEnvironmentVariable{} + + for _, secret := range secrets { + if secret.Type == PERSONAL_SECRET_TYPE_NAME { + personalSecret[secret.Key] = models.SingleEnvironmentVariable{ + Key: secret.Key, + Value: secret.Value, + Type: secret.Type, + } + } + + if secret.Type == SHARED_SECRET_TYPE_NAME { + sharedSecret[secret.Key] = models.SingleEnvironmentVariable{ + Key: secret.Key, + Value: secret.Value, + Type: secret.Type, + } + } + } + + for _, secret := range secrets { + personalValue, personalExists := personalSecret[secret.Key] + sharedValue, sharedExists := sharedSecret[secret.Key] + + if personalExists && sharedExists || personalExists && !sharedExists { + secretsToReturn = append(secretsToReturn, personalValue) + } else { + secretsToReturn = append(secretsToReturn, sharedValue) + } + } + + return secretsToReturn +} diff --git a/cli/packages/util/vault.go b/cli/packages/util/vault.go new file mode 100644 index 000000000..03d03b360 --- /dev/null +++ b/cli/packages/util/vault.go @@ -0,0 +1,66 @@ +package util + +import ( + "fmt" + "os" + + "github.com/99designs/keyring" + "golang.org/x/term" +) + +func GetCurrentVaultBackend() (keyring.BackendType, error) { + configFile, err := GetConfigFile() + if err != nil { + return "", fmt.Errorf("getCurrentVaultBackend: unable to get config file [err=%s]", err) + } + + if configFile.VaultBackendType == "" { + return keyring.AvailableBackends()[0], nil + } + + return configFile.VaultBackendType, nil +} + +func GetKeyRing() (keyring.Keyring, error) { + currentVaultBackend, err := GetCurrentVaultBackend() + if err != nil { + return nil, fmt.Errorf("GetKeyRing: unable to get the current vault backend, [err=%s]", err) + } + + keyringInstanceConfig := keyring.Config{ + FilePasswordFunc: fileKeyringPassphrasePrompt, + ServiceName: SERVICE_NAME, + LibSecretCollectionName: SERVICE_NAME, + KWalletAppID: SERVICE_NAME, + KWalletFolder: SERVICE_NAME, + KeychainTrustApplication: true, + WinCredPrefix: SERVICE_NAME, + FileDir: fmt.Sprintf("~/%s-file-vault", SERVICE_NAME), + KeychainAccessibleWhenUnlocked: true, + } + + // if the user explicitly sets a vault backend, then only use that + if currentVaultBackend != "" { + keyringInstanceConfig.AllowedBackends = []keyring.BackendType{keyring.BackendType(currentVaultBackend)} + } + + keyringInstance, err := keyring.Open(keyringInstanceConfig) + if err != nil { + return nil, fmt.Errorf("GetKeyRing: Unable to create instance of Keyring because of [err=%s]", err) + } + + return keyringInstance, nil +} + +func fileKeyringPassphrasePrompt(prompt string) (string, error) { + if password, ok := os.LookupEnv("INFISICAL_VAULT_FILE_PASSPHRASE"); ok { + return password, nil + } + + fmt.Fprintf(os.Stderr, "%s: ", prompt) + b, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return "", err + } + return string(b), nil +} diff --git a/cli/packages/visualize/visualize.go b/cli/packages/visualize/visualize.go new file mode 100644 index 000000000..85d946a32 --- /dev/null +++ b/cli/packages/visualize/visualize.go @@ -0,0 +1,35 @@ +package visualize + +import ( + "os" + + "github.com/jedib0t/go-pretty/table" +) + +// Given headers and rows, this function will print out a table +func Table(headers []string, rows [][]string) { + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.SetStyle(table.StyleLight) + + // t.SetTitle("Title") + t.Style().Options.DrawBorder = true + t.Style().Options.SeparateHeader = true + t.Style().Options.SeparateColumns = true + + tableHeaders := table.Row{} + for _, header := range headers { + tableHeaders = append(tableHeaders, header) + } + + t.AppendHeader(tableHeaders) + for _, row := range rows { + tableRow := table.Row{} + for _, val := range row { + tableRow = append(tableRow, val) + } + t.AppendRow(tableRow) + } + + t.Render() +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 15a200783..6cf75ad47 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -48,6 +48,8 @@ services: - ./frontend/public:/app/public - ./frontend/styles:/app/styles - ./frontend/components:/app/components + - ./frontend/locales:/app/locales + - ./frontend/next-i18next.config.js:/app/next-i18next.config.js env_file: .env environment: - NEXT_PUBLIC_ENV=development diff --git a/docs/cli/commands/commands.mdx b/docs/cli/commands/commands.mdx index acefdfa7e..7c1deeb1b 100644 --- a/docs/cli/commands/commands.mdx +++ b/docs/cli/commands/commands.mdx @@ -9,7 +9,7 @@ title: "Commands" | `login` | Used to authenticate and set the logged in user. | | `init` | Used to link a local project to the platform. | | `run` | Used to inject envars from the platform into an application process. | - +| `vault` | Used to manage where your login credentials are stored at rest | ## Global options | Option | Description | diff --git a/docs/cli/commands/login.mdx b/docs/cli/commands/login.mdx index 32c1bedba..de004c97f 100644 --- a/docs/cli/commands/login.mdx +++ b/docs/cli/commands/login.mdx @@ -7,7 +7,5 @@ infisical login ``` ## Description - -Verify a user and save credentials to the system keyring. - -To change the logged in user, run the command again to overwrite the previous login. +The CLI uses authentication to verify your identity. When you enter the correct email and password for your account, a token is generated and saved in your system Keyring to allow you to make future interactions with the CLI. +If you want to change where the login credentials are stored, visit the [vaults command](./vault) \ No newline at end of file diff --git a/docs/cli/commands/run.mdx b/docs/cli/commands/run.mdx index 2c65ef53e..4afd7586d 100644 --- a/docs/cli/commands/run.mdx +++ b/docs/cli/commands/run.mdx @@ -34,3 +34,4 @@ Inject environment variables from the platform into an application process. | `--projectId` | Used to link a local project to the platform (required only if injecting via the service token method) | None | | `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` | | `--command` | Pass secrets into chained commands (e.g., `"first-command && second-command; more-commands..."`) | None | +| `--secret-overriding`| Prioritizes personal secrets with the same name over shared secrets | `true` | diff --git a/docs/cli/commands/vault.mdx b/docs/cli/commands/vault.mdx new file mode 100644 index 000000000..f973cb727 --- /dev/null +++ b/docs/cli/commands/vault.mdx @@ -0,0 +1,50 @@ +--- +title: "infisical vault" +--- + + + + ```bash + infisical vault + + # Example output + The following vaults are available on your system: + - keychain + - pass + - file + + You are currently using [keychain] vault to store your login credentials + ``` + + + + ```bash + infisical vault set + + # Example + infisical vault set keychain + ``` + + + + +## Description + +To ensure secure storage of your login credentials when using the CLI, Infisical stores login credentials securely in a system vault or encrypted text file with a passphrase known only by the user. + + + By default, the most appropriate vault is chosen to store your login credentials. + For example, if you are on macOS, KeyChain will be automatically selected. + +- [macOS Keychain](https://support.apple.com/en-au/guide/keychain-access/welcome/mac) +- [Windows Credential Manager](https://support.microsoft.com/en-au/help/4026814/windows-accessing-credential-manager) +- Secret Service ([Gnome Keyring](https://wiki.gnome.org/Projects/GnomeKeyring), [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5)) +- [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5) +- [Pass](https://www.passwordstore.org/) +- [KeyCtl]() +- Encrypted file (JWT) + + +To avoid constantly entering your passphrase when using the `file` vault type, set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable with your password in your shell + + diff --git a/docs/cli/faq.mdx b/docs/cli/faq.mdx new file mode 100644 index 000000000..600386e81 --- /dev/null +++ b/docs/cli/faq.mdx @@ -0,0 +1,15 @@ +--- +title: "FAQ" +--- + +Frequently asked questions about the CLI can be found on this page. +If you can't find the answer you're looking for, please create an issue on our GitHub repository or join our Slack channel for additional support. + + +By default, the CLI will choose the most suitable store available on your system. +If you experience issues with the default store, you can switch to a different one. +If none of the available stores work for you, you can try using the `file` store type by running `infisical vault set file`, which should work in most cases. +If you are still experiencing trouble, please seek support. + +[Learn more about vault command](./commands/vault) + \ No newline at end of file diff --git a/docs/cli/overview.mdx b/docs/cli/overview.mdx index 144259023..0c998feb4 100644 --- a/docs/cli/overview.mdx +++ b/docs/cli/overview.mdx @@ -88,6 +88,14 @@ The Infisical CLI provides a way to inject environment variables from the platfo sudo apt-get update && sudo apt-get install -y infisical ``` + + + Use the `yay` package manager to install from the [Arch User Repository](https://aur.archlinux.org/packages/infisical-bin) + + ```bash + yay -S infisical-bin + ``` + diff --git a/docs/cli/usage.mdx b/docs/cli/usage.mdx index 4237f8154..fe08b60b3 100644 --- a/docs/cli/usage.mdx +++ b/docs/cli/usage.mdx @@ -4,10 +4,27 @@ title: "Usage" Prerequisite: [Install the CLI](/cli/overview) +## Authenticate + + + To use the Infisical CLI in your development environment, you can run the command below. + This will allow you to access the features and functionality provided by the CLI. + + ```bash + infisical login + ``` + + + + To use Infisical CLI in environments where you cannot run the `infisical login` command, you can authenticate via a + Infisical Token instead. Learn more about [Infisical Token](../getting-started/dashboard/token). + + + ## Initialize Infisical for your project ```bash -# move to your project +# navigate to your project cd /path/to/project # initialize infisical @@ -21,13 +38,7 @@ infisical init infisical run -- [your application start command] ``` -Options you can specify: - -| Option | Description | Default value | -| ------------- | ----------------------------------------------------------------------------------------------------------- | ------------- | -| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` | -| `--projectId` | Used to link a local project to the platform (required only if injecting via the service token method) | `None` | -| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` | +View all available options for `run` command [here](./commands/run) ## Examples: diff --git a/docs/contributing/FAQ.mdx b/docs/contributing/FAQ.mdx deleted file mode 100644 index 9f4f7aee9..000000000 --- a/docs/contributing/FAQ.mdx +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: 'Frequently Asked Questions' -description: 'Have any questions? [Join our Slack community](https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g).' ---- - -## Problem with SMTP - -If you opt for actual SMTP server (not the local MailHog), you have to have the right environment variables set. - -You can normally populate `SMTP_USERNAME` and `SMTP_PASSWORD` with your usual login and password (you could also create a 'burner' email). Sometimes, there still are problems. - -You can go to your Gmail account settings > security and enable โ€œless secure appsโ€. This would allow Infisical to use your Gmail to send emails. - -If it still doesn't work, [this](https://stackoverflow.com/questions/72547853/unable-to-send-email-in-c-sharp-less-secure-app-access-not-longer-available/72553362#72553362) should help. - -## `MONGO_URL` issues - -Your `MONGO_URL` should be something like `mongodb://root:example@mongo:27017/?authSource=admin`. If you want to change it (not recommended), you should make sure that you keep this URL in line with `MONGO_USERNAME=root` and `MONGO_PASSWORD=example`. diff --git a/docs/contributing/developing.mdx b/docs/contributing/developing.mdx index 812025dbc..423b83da2 100644 --- a/docs/contributing/developing.mdx +++ b/docs/contributing/developing.mdx @@ -16,53 +16,43 @@ cd infisical ## Set up environment variables -Start by creating a .env file at the root of the Infisical directory +Start by creating a .env file at the root of the Infisical directory. It's best to start with the provided [`.env.example`](https://github.com/Infisical/infisical/blob/main/.env.example) template containing the necessary envars to fill out your .env file โ€” you only have to modify the SMTP parameters. + + + The pre-populated environment variable values in the `.env.example` file are meant to be used in development only. + You'll want to fill in your own values in production, especially concerning encryption keys, secrets, and SMTP parameters. + + +Refer to the [environment variable list](https://infisical.com/docs/self-hosting/configuration/envars) for guidance on each envar. + +### Helpful tips for developing with Infisical: - Reference the [environment variable list](https://infisical.com/docs/self-hosting/configuration/envars) and provided [`.env.example`](https://raw.githubusercontent.com/Infisical/infisical/main/.env.example) template to fill out your .env file. +Use the `ENCRYPTION_KEY`, JWT-secret envars, `MONGO_URL`, `MONGO_USERNAME`, `MONGO_PASSWORD` provided in the `.env.example` file. + +If setting your own values: + +- `ENCRYPTION_KEY` should be a [32-byte random hex](https://www.browserling.com/tools/random-hex) +- `MONGO_URL` should take the form: `mongodb://[MONGO_USERNAME]:[MONGO_PASSWORD]@mongo:27017/?authSource=admin`. -### Keys + +Bring and configure your own SMTP server by following our [email configuration guide](https://infisical.com/docs/self-hosting/configuration/email) (we recommend using either SendGrid or Mailgun). -`ENCRYPTION_KEY`, `JWT_SIGNUP_SECRET`, `JWT_REFRESH_SECRET`, `JWT_AUTH_SECRET`, `JWT_SERVICE_SECRET` values can be generated with this [32-byte random hex generator](https://www.browserling.com/tools/random-hex). - -### Database - -Use to the following `MONGO_URL`, `MONGO_USERNAME`, `MONGO_PASSWORD`, `SITE_URL` values: - -``` -MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin -MONGO_USERNAME=root -MONGO_PASSWORD=example - -SITE_URL=http://localhost:8080 -``` - - - If you decide to use your own `MONGO_USERNAME` and `MONGO_PASSWORD`, you'll have to modify `MONGO_URL` to take the form: `mongodb://[MONGO_USERNAME]:[MONGO_PASSWORD]@mongo:27017/?authSource=admin`. - - -### Mailing - -Option 1: Bring your own SMTP server and credentials by filling in `SMTP_HOST`, `SMTP_FROM_ADDRESS`, `SMTP_FROM_NAME`, `SMTP_USERNAME`, and `SMTP_PASSWORD`. - - `SMTP_HOST` is set to `smtp.gmail.com` by default. For `SMTP_USERNAME` and `SMTP_PASSWORD`, you'll need an email with 2-step-verification and an [app password](https://support.google.com/mail/answer/185833?hl=en) for it. - - - -Option 2: Use the provided (Mailhog) SMTP server and browse emails sent by the backend on `http://localhost:8025`. To use this option, set the following `SMTP_HOST`, `SMTP_PORT`, `SMTP_FROM_NAME`, `SMTP_USERNAME`, `SMTP_PASSWORD` values: +Alternatively, you can use the provided development (Mailhog) SMTP server to send and browse emails sent by the backend on http://localhost:8025; to use this option, set the following `SMTP_HOST`, `SMTP_PORT`, `SMTP_FROM_NAME`, `SMTP_USERNAME`, `SMTP_PASSWORD` below. + ``` SMTP_HOST=smtp-server SMTP_PORT=1025 SMTP_FROM_ADDRESS=team@infisical.com -SMTP_FROM_NAME=[whatever you like] +SMTP_FROM_NAME=Infisical SMTP_USERNAME=team@infisical.com SMTP_PASSWORD= ``` - Make sure to leave the `SMTP_PASSWORD` blank so the backend can connect to MailHog. + If using Mailhog, make sure to leave the `SMTP_PASSWORD` blank so the backend can connect to MailHog. ## Docker for development diff --git a/docs/getting-started/dashboard/token.mdx b/docs/getting-started/dashboard/token.mdx index 9b3ddf79f..455a57929 100644 --- a/docs/getting-started/dashboard/token.mdx +++ b/docs/getting-started/dashboard/token.mdx @@ -11,6 +11,12 @@ To generate the the token, head over to your project settings as shown below. ![token add](../../images/project-token-add.png) +## Feeding Infisical Token to the CLI + +The Infisical CLI checks for the presence of an environment variable called `INFISICAL_TOKEN`. +If it detects this variable in the terminal where it is being run, it will use it to authenticate and retrieve the environment variables that the token is authorized to access. +This allows you to use the CLI in environments where you are unable to run the `infisical login` command. + The token grants read-only access to a particular environment and project for a specified amount of time. Once the token is expired, the CLI using it will no longer be able to make diff --git a/docs/images/email-mailhog-credentials.png b/docs/images/email-mailhog-credentials.png new file mode 100644 index 000000000..8d5a11295 Binary files /dev/null and b/docs/images/email-mailhog-credentials.png differ diff --git a/docs/images/email-sendgrid-create-key.png b/docs/images/email-sendgrid-create-key.png new file mode 100644 index 000000000..1caa977a8 Binary files /dev/null and b/docs/images/email-sendgrid-create-key.png differ diff --git a/docs/images/email-sendgrid-restrictions.png b/docs/images/email-sendgrid-restrictions.png new file mode 100644 index 000000000..a70891a60 Binary files /dev/null and b/docs/images/email-sendgrid-restrictions.png differ diff --git a/docs/mint.json b/docs/mint.json index 92a952767..cbac56d5a 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -94,26 +94,31 @@ "cli/commands/login", "cli/commands/init", "cli/commands/run", - "cli/commands/export" + "cli/commands/export", + "cli/commands/vault" ] - } + }, + "cli/faq" ] }, { "group": "Self-hosting", "pages": [ - "self-hosting/overview", - { - "group": "Deployments options", - "pages": [ - "self-hosting/deployments/linux", - "self-hosting/deployments/kubernetes" - ] - }, - { - "group": "Configuration", - "pages": ["self-hosting/configuration/envars"] - } + "self-hosting/overview" + ] + }, + { + "group": "Deployment options", + "pages": [ + "self-hosting/deployments/linux", + "self-hosting/deployments/kubernetes" + ] + }, + { + "group": "Configuration", + "pages": [ + "self-hosting/configuration/envars", + "self-hosting/configuration/email" ] }, { @@ -177,8 +182,7 @@ "pages": [ "contributing/overview", "contributing/code-of-conduct", - "contributing/developing", - "contributing/FAQ" + "contributing/developing" ] } ], diff --git a/docs/self-hosting/configuration/email.mdx b/docs/self-hosting/configuration/email.mdx new file mode 100644 index 000000000..b1e911fb6 --- /dev/null +++ b/docs/self-hosting/configuration/email.mdx @@ -0,0 +1,75 @@ +--- +title: "Email" +description: "" +--- + +Infisical requires you to configure your own SMTP server for certain functionality like: + +- Sending email confirmation links to sign up. +- Sending invite links for projects. +- Sending alerts. + +We strongly recommend using an email service to act as your email server and provide examples for common providers. + +## General configuration + +By default, you need to configure the following SMTP [environment variables](https://infisical.com/docs/self-hosting/configuration/envars): + +- `SMTP_HOST`: Hostname to connect to for establishing SMTP connections. +- `SMTP_USERNAME`: Credential to connect to host (e.g. team@infisical.com) +- `SMTP_PASSWORD`: Credential to connect to host. +- `SMTP_PORT`: Port to connect to for establishing SMTP connections. +- `SMTP_SECURE`: If `true`, the connection will use TLS when connecting to server with special configs for SendGrid and Mailgun. If `false` (the default) then TLS is used if server supports the STARTTLS extension. +- `SMTP_FROM_ADDRESS`: Email address to be used for sending emails (e.g. team@infisical.com). +- `SMTP_FROM_NAME`: Name label to be used in `From` field (e.g. Team). + +Below you will find details on how to configure common email providers (not in any particular order). + +## Twilio SendGrid + +1. Create an account and configure [SendGrid](https://sendgrid.com) to send emails. +2. Create a SendGrid API Key under Settings > [API Keys](https://app.sendgrid.com/settings/api_keys) +3. Set a name for your API Key, we recommend using "Infisical," and select the "Restricted Key" option. You will need to enable the "Mail Send" permission as shown below: + +![creating sendgrid api key](../../images/email-sendgrid-create-key.png) + +![setting sendgrid api key restriction](../../images/email-sendgrid-restrictions.png) + +4. With the API Key, you can now set your SMTP environment variables: + +``` +SMTP_HOST=smtp.sendgrid.net +SMTP_USERNAME=apikey +SMTP_PASSWORD=SG.rqFsfjxYPiqE1lqZTgD_lz7x8IVLx # your SendGrid API Key from step above +SMTP_PORT=587 +SMTP_SECURE=true +SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails +SMTP_FROM_NAME=Infisical +``` + + + Remember that you will need to restart Infisical for this to work properly. + + +## Mailgun + +1. Create an account and configure [Mailgun](https://www.mailgun.com) to send emails. +2. Obtain your Mailgun credentials in Sending > Overview > SMTP + +![obtain mailhog api key estriction](../../images/email-mailhog-credentials.png) + +3. With your Mailgun credentials, you can now set up your SMTP environment variables: + +``` +SMTP_HOST=smtp.mailgun.org # obtained from credentials page +SMTP_USERNAME=postmaster@example.mailgun.org # obtained from credentials page +SMTP_PASSWORD=password # obtained from credentials page +SMTP_PORT=587 +SMTP_SECURE=true +SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails +SMTP_FROM_NAME=Infisical +``` + + + Remember that you will need to restart Infisical for this to work properly. + \ No newline at end of file diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 8bbb507d8..f598336b6 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -3,9 +3,7 @@ title: "Environment Variables" description: "" --- -## The .env file - -Configuring Infisical requires setting some environment variables. There is a file called `.env.example` at the root directory of our main repo that you can use to create a `.env` before you start the server. +Configuring Infisical requires setting some environment variables. There is a file called [`.env.example`](https://github.com/Infisical/infisical/blob/main/.env.example) at the root directory of our main repo that you can use to create a `.env` file before you start the server. | Variable | Description | Default Value | | ---------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------- | @@ -22,14 +20,15 @@ Configuring Infisical requires setting some environment variables. There is a fi | `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` | -| `SMTP_HOST` | Hostname to connect to for establishing SMTP connections | `smtp.gmail.com` | -| `SMTP_SECURE` | Use TLS when connecting to host. If false, TLS will be used if STARTTLS is supported | `false` | -| `SMTP_PORT` | Port to connect to for establishing SMTP connections | `587` | -| `SMTP_FROM_ADDRESS` | โ—๏ธ Email address to be used for sending emails (e.g. `team@infisical.com`) | `None` | -| `SMTP_FROM_NAME` | Name label to be used in From field (e.g. `Team`) | `Infisical` | +| `SMTP_HOST` | โ—๏ธ Hostname to connect to for establishing SMTP connections | `None` | | `SMTP_USERNAME` | โ—๏ธ Credential to connect to host (e.g. `team@infisical.com`) | `None` | | `SMTP_PASSWORD` | โ—๏ธ Credential to connect to host | `None` | +| `SMTP_PORT` | Port to connect to for establishing SMTP connections | `587` | +| `SMTP_SECURE` | If true, use TLS when connecting to host. If false, TLS will be used if STARTTLS is supported | `false` | +| `SMTP_FROM_ADDRESS` | โ—๏ธ Email address to be used for sending emails (e.g. `team@infisical.com`) | `None` | +| `SMTP_FROM_NAME` | Name label to be used in From field (e.g. `Team`) | `Infisical` | | `TELEMETRY_ENABLED` | `true` or `false`. [More](../overview). | `true` | +| `LICENSE_KEY` | License key if using Infisical Enterprise Edition | `true` | | `CLIENT_ID_HEROKU` | OAuth2 client ID for Heroku integration | `None` | | `CLIENT_ID_VERCEL` | OAuth2 client ID for Vercel integration | `None` | | `CLIENT_ID_NETLIFY` | OAuth2 client ID for Netlify integration | `None` | diff --git a/frontend/.gitignore b/frontend/.gitignore index 83e774c8c..5edd5a7fa 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -32,3 +32,5 @@ yarn-error.log* .env.production.local .vercel .env.infisical + +.vscode \ No newline at end of file diff --git a/frontend/components/RouteGuard.js b/frontend/components/RouteGuard.js index d08972b99..c5f6feb35 100644 --- a/frontend/components/RouteGuard.js +++ b/frontend/components/RouteGuard.js @@ -48,6 +48,7 @@ export default function RouteGuard({ children }) { // Check if the user is authenticated const response = await checkAuth(); // #TODO: figure our why sometimes it doesn't output a response + // ANS(akhilmhdh): Because inside the security client the await token() doesn't have try/catch if (!publicPaths.includes(path)) { try { if (response.status !== 200) { diff --git a/frontend/components/basic/InputField.tsx b/frontend/components/basic/InputField.tsx index 46b42c15c..241410959 100644 --- a/frontend/components/basic/InputField.tsx +++ b/frontend/components/basic/InputField.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { memo,useState } from 'react'; import { faCircle, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -96,6 +96,7 @@ const InputField = ( /> {props.label?.includes('Password') && ( - + + router.push( + "/settings/org/" + router.query.id + ) + } + />, + // eslint-disable-next-line react/jsx-key + - Click to Copy + {t("common.click-to-copy")} diff --git a/frontend/components/basic/popups/BottomRightPopup.js b/frontend/components/basic/popups/BottomRightPopup.tsx similarity index 71% rename from frontend/components/basic/popups/BottomRightPopup.js rename to frontend/components/basic/popups/BottomRightPopup.tsx index 85aaba725..e6ea60f92 100644 --- a/frontend/components/basic/popups/BottomRightPopup.js +++ b/frontend/components/basic/popups/BottomRightPopup.tsx @@ -3,6 +3,16 @@ import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +interface PopupProps { + buttonText: string; + buttonLink: string; + titleText: string; + emoji: string; + textLine1: string; + textLine2: string; + setCheckDocsPopUpVisible: (value: boolean) => void; +} + /** * This is the notification that pops up at the bottom right when a user performs a certain action * @param {object} org @@ -23,16 +33,16 @@ export default function BottonRightPopup({ textLine1, textLine2, setCheckDocsPopUpVisible, -}) { +}: PopupProps): JSX.Element { return (