mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 17:27:40 +00:00
Merge remote-tracking branch 'upstream/main' into feat/39
This commit is contained in:
+22
-17
@@ -1,21 +1,19 @@
|
|||||||
# Keys
|
# Keys
|
||||||
# Required keys for platform encryption/decryption ops
|
# Required key for platform encryption/decryption ops
|
||||||
PRIVATE_KEY=replace_with_nacl_sk
|
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
||||||
PUBLIC_KEY=replace_with_nacl_pk
|
|
||||||
ENCRYPTION_KEY=replace_with_lengthy_secure_hex
|
|
||||||
|
|
||||||
# JWT
|
# JWT
|
||||||
# Required secrets to sign JWT tokens
|
# Required secrets to sign JWT tokens
|
||||||
JWT_SIGNUP_SECRET=replace_with_lengthy_secure_hex
|
JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a
|
||||||
JWT_REFRESH_SECRET=replace_with_lengthy_secure_hex
|
JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff
|
||||||
JWT_AUTH_SECRET=replace_with_lengthy_secure_hex
|
JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
|
||||||
|
JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
|
||||||
|
|
||||||
# JWT lifetime
|
# JWT lifetime
|
||||||
# Optional lifetimes for JWT tokens expressed in seconds or a string
|
# Optional lifetimes for JWT tokens expressed in seconds or a string
|
||||||
# describing a time span (e.g. 60, "2 days", "10h", "7d")
|
# describing a time span (e.g. 60, "2 days", "10h", "7d")
|
||||||
JWT_AUTH_LIFETIME=
|
JWT_AUTH_LIFETIME=
|
||||||
JWT_REFRESH_LIFETIME=
|
JWT_REFRESH_LIFETIME=
|
||||||
JWT_SERVICE_SECRET=
|
|
||||||
JWT_SIGNUP_LIFETIME=
|
JWT_SIGNUP_LIFETIME=
|
||||||
|
|
||||||
# Optional lifetimes for OTP expressed in seconds
|
# Optional lifetimes for OTP expressed in seconds
|
||||||
@@ -33,21 +31,28 @@ MONGO_PASSWORD=example
|
|||||||
|
|
||||||
# Website URL
|
# Website URL
|
||||||
# Required
|
# Required
|
||||||
|
|
||||||
SITE_URL=http://localhost:8080
|
SITE_URL=http://localhost:8080
|
||||||
|
|
||||||
# Mail/SMTP
|
# Mail/SMTP
|
||||||
# Required to send emails
|
SMTP_HOST= # required
|
||||||
# By default, SMTP_HOST is set to smtp.gmail.com
|
SMTP_USERNAME= # required
|
||||||
SMTP_HOST=smtp.gmail.com
|
SMTP_PASSWORD= # required
|
||||||
SMTP_NAME=Team
|
SMTP_PORT=587
|
||||||
SMTP_USERNAME=[email protected]
|
SMTP_SECURE=false
|
||||||
SMTP_PASSWORD=
|
SMTP_FROM_ADDRESS= # required
|
||||||
|
SMTP_FROM_NAME=Infisical
|
||||||
|
|
||||||
# Integration
|
# Integration
|
||||||
# Optional only if integration is used
|
# Optional only if integration is used
|
||||||
OAUTH_CLIENT_SECRET_HEROKU=
|
CLIENT_ID_HEROKU=
|
||||||
OAUTH_TOKEN_URL_HEROKU=
|
CLIENT_ID_VERCEL=
|
||||||
|
CLIENT_ID_NETLIFY=
|
||||||
|
CLIENT_ID_GITHUB=
|
||||||
|
CLIENT_SECRET_HEROKU=
|
||||||
|
CLIENT_SECRET_VERCEL=
|
||||||
|
CLIENT_SECRET_NETLIFY=
|
||||||
|
CLIENT_SECRET_GITHUB=
|
||||||
|
CLIENT_SLUG_VERCEL=
|
||||||
|
|
||||||
# Sentry (optional) for monitoring errors
|
# Sentry (optional) for monitoring errors
|
||||||
SENTRY_DSN=
|
SENTRY_DSN=
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
built
|
||||||
|
healthcheck.js
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 106 KiB |
@@ -0,0 +1,30 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
container_name: infisical-backend-test
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- mongo
|
||||||
|
image: infisical/backend:test
|
||||||
|
command: npm run start
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- MONGO_URL=mongodb://test:example@mongo:27017/?authSource=admin
|
||||||
|
- MONGO_USERNAME=test
|
||||||
|
- MONGO_PASSWORD=example
|
||||||
|
networks:
|
||||||
|
- infisical-test
|
||||||
|
|
||||||
|
mongo:
|
||||||
|
container_name: infisical-mongo-test
|
||||||
|
image: mongo
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- MONGO_INITDB_ROOT_USERNAME=test
|
||||||
|
- MONGO_INITDB_ROOT_PASSWORD=example
|
||||||
|
networks:
|
||||||
|
- infisical-test
|
||||||
|
|
||||||
|
networks:
|
||||||
|
infisical-test:
|
||||||
Executable
+26
@@ -0,0 +1,26 @@
|
|||||||
|
# Name of the target container to check
|
||||||
|
container_name="$1"
|
||||||
|
# Timeout in seconds. Default: 60
|
||||||
|
timeout=$((${2:-60}));
|
||||||
|
|
||||||
|
if [ -z $container_name ]; then
|
||||||
|
echo "No container name specified";
|
||||||
|
exit 1;
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Container: $container_name";
|
||||||
|
echo "Timeout: $timeout sec";
|
||||||
|
|
||||||
|
try=0;
|
||||||
|
is_healthy="false";
|
||||||
|
while [ $is_healthy != "true" ];
|
||||||
|
do
|
||||||
|
try=$(($try + 1));
|
||||||
|
printf "■";
|
||||||
|
is_healthy=$(docker inspect --format='{{json .State.Health}}' $container_name | jq '.Status == "healthy"');
|
||||||
|
sleep 1;
|
||||||
|
if [[ $try -eq $timeout ]]; then
|
||||||
|
echo " Container was not ready within timeout";
|
||||||
|
exit 1;
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
name: "Backend Test Report"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Check Backend Pull Request"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
be-report:
|
||||||
|
name: Backend test report
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: 📁 Download test results
|
||||||
|
id: download-artifact
|
||||||
|
uses: dawidd6/action-download-artifact@v2
|
||||||
|
with:
|
||||||
|
name: be-test-results
|
||||||
|
path: backend
|
||||||
|
workflow: check-be-pull-request.yml
|
||||||
|
workflow_conclusion: success
|
||||||
|
- name: 📋 Publish test results
|
||||||
|
uses: dorny/test-reporter@v1
|
||||||
|
with:
|
||||||
|
name: Test Results
|
||||||
|
path: reports/jest-*.xml
|
||||||
|
reporter: jest-junit
|
||||||
|
working-directory: backend
|
||||||
|
- name: 📋 Publish coverage
|
||||||
|
uses: ArtiomTr/jest-coverage-report-action@v2
|
||||||
|
id: coverage
|
||||||
|
with:
|
||||||
|
output: comment, report-markdown
|
||||||
|
coverage-file: coverage/report.json
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
working-directory: backend
|
||||||
|
- uses: marocchino/sticky-pull-request-comment@v2
|
||||||
|
with:
|
||||||
|
message: ${{ steps.coverage.outputs.report }}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
name: "Check Backend Pull Request"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize]
|
||||||
|
paths:
|
||||||
|
- "backend/**"
|
||||||
|
- "!backend/README.md"
|
||||||
|
- "!backend/.*"
|
||||||
|
- "backend/.eslintrc.js"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-be-pr:
|
||||||
|
name: Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: ☁️ Checkout source
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: 🔧 Setup Node 16
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "16"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: backend/package-lock.json
|
||||||
|
- name: 📦 Install dependencies
|
||||||
|
run: npm ci --only-production --ignore-scripts
|
||||||
|
working-directory: backend
|
||||||
|
- name: 🧪 Run tests
|
||||||
|
run: npm run test:ci
|
||||||
|
working-directory: backend
|
||||||
|
- name: 📁 Upload test results
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: be-test-results
|
||||||
|
path: |
|
||||||
|
./backend/reports
|
||||||
|
./backend/coverage
|
||||||
|
- name: 🏗️ Run build
|
||||||
|
run: npm run build
|
||||||
|
working-directory: backend
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
name: Check Frontend Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [ opened, synchronize ]
|
||||||
|
paths:
|
||||||
|
- 'frontend/**'
|
||||||
|
- '!frontend/README.md'
|
||||||
|
- '!frontend/.*'
|
||||||
|
- 'frontend/.eslintrc.js'
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
check-fe-pr:
|
||||||
|
name: Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: ☁️ Checkout source
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
-
|
||||||
|
name: 🔧 Setup Node 16
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
-
|
||||||
|
name: 📦 Install dependencies
|
||||||
|
run: npm ci --only-production --ignore-scripts
|
||||||
|
working-directory: frontend
|
||||||
|
# -
|
||||||
|
# name: 🧪 Run tests
|
||||||
|
# run: npm run test:ci
|
||||||
|
# working-directory: frontend
|
||||||
|
-
|
||||||
|
name: 🏗️ Run build
|
||||||
|
run: npm run build
|
||||||
|
working-directory: frontend
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
name: Close inactive issues
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "30 1 * * *"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
close-issues:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/stale@v4
|
|
||||||
with:
|
|
||||||
days-before-issue-stale: 30
|
|
||||||
days-before-issue-close: 14
|
|
||||||
stale-issue-label: "stale"
|
|
||||||
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
|
|
||||||
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
|
|
||||||
days-before-pr-stale: -1
|
|
||||||
days-before-pr-close: -1
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
@@ -3,35 +3,84 @@ name: Push to Docker Hub
|
|||||||
on: [workflow_dispatch]
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
backend-image:
|
||||||
|
name: Build backend image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: ☁️ Checkout source
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
-
|
- name: 🔧 Set up QEMU
|
||||||
name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
-
|
- name: 🔧 Set up Docker Buildx
|
||||||
name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
-
|
- name: 🐋 Login to Docker Hub
|
||||||
name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
-
|
- name: 📦 Build backend and export to Docker
|
||||||
name: Build and push backend
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
load: true
|
||||||
|
context: backend
|
||||||
|
tags: infisical/backend:test
|
||||||
|
- name: ⏻ Spawn backend container and dependencies
|
||||||
|
run: |
|
||||||
|
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
|
||||||
|
- name: 🧪 Test backend image
|
||||||
|
run: |
|
||||||
|
./.github/resources/healthcheck.sh infisical-backend-test
|
||||||
|
- name: ⏻ Shut down backend container and dependencies
|
||||||
|
run: |
|
||||||
|
docker compose -f .github/resources/docker-compose.be-test.yml down
|
||||||
|
- name: 🏗️ Build backend and push
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
context: backend
|
context: backend
|
||||||
tags: infisical/backend:test
|
tags: infisical/backend:latest
|
||||||
-
|
platforms: linux/amd64,linux/arm64
|
||||||
name: Build and push frontend
|
|
||||||
|
frontend-image:
|
||||||
|
name: Build frontend image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: ☁️ Checkout source
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: 🔧 Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
- name: 🔧 Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: 🐋 Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: 📦 Build frontend and export to Docker
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
load: true
|
||||||
|
context: frontend
|
||||||
|
tags: infisical/frontend:test
|
||||||
|
build-args: |
|
||||||
|
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||||
|
- name: ⏻ Spawn frontend container
|
||||||
|
run: |
|
||||||
|
docker run -d --rm --name infisical-frontend-test infisical/frontend:test
|
||||||
|
- name: 🧪 Test frontend image
|
||||||
|
run: |
|
||||||
|
./.github/resources/healthcheck.sh infisical-frontend-test
|
||||||
|
- name: ⏻ Shut down frontend container
|
||||||
|
run: |
|
||||||
|
docker stop infisical-frontend-test
|
||||||
|
- name: 🏗️ Build frontend and push
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
file: frontend/Dockerfile.dev
|
|
||||||
context: frontend
|
context: frontend
|
||||||
tags: infisical/frontend:test
|
tags: infisical/frontend:latest
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
build-args: |
|
||||||
|
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
goreleaser:
|
goreleaser:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
@@ -24,6 +24,15 @@ jobs:
|
|||||||
go-version: '>=1.19.3'
|
go-version: '>=1.19.3'
|
||||||
cache: true
|
cache: true
|
||||||
cache-dependency-path: cli/go.sum
|
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
|
- uses: goreleaser/goreleaser-action@v2
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser
|
distribution: goreleaser
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: Release Docker image for K8 operator
|
||||||
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: 🔧 Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: 🔧 Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: 🐋 Login to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: k8-operator
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: infisical/kubernetes-operator:latest
|
||||||
+3
-1
@@ -25,7 +25,9 @@ node_modules
|
|||||||
.env
|
.env
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
coverage
|
||||||
|
reports
|
||||||
|
junit.xml
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
|
|||||||
+16
-6
@@ -7,12 +7,23 @@
|
|||||||
# # you may remove this if you don't need go generate
|
# # you may remove this if you don't need go generate
|
||||||
# - cd cli && go generate ./...
|
# - cd cli && go generate ./...
|
||||||
builds:
|
builds:
|
||||||
- env:
|
- id: darwin-build
|
||||||
- CGO_ENABLED=0
|
|
||||||
binary: infisical
|
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:
|
goos:
|
||||||
- darwin
|
- darwin
|
||||||
|
ignore:
|
||||||
|
- goos: darwin
|
||||||
|
goarch: "386"
|
||||||
|
dir: ./cli
|
||||||
|
- id: all-other-builds
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
binary: infisical
|
||||||
|
goos:
|
||||||
- freebsd
|
- freebsd
|
||||||
- linux
|
- linux
|
||||||
- netbsd
|
- netbsd
|
||||||
@@ -27,8 +38,6 @@ builds:
|
|||||||
- 6
|
- 6
|
||||||
- 7
|
- 7
|
||||||
ignore:
|
ignore:
|
||||||
- goos: darwin
|
|
||||||
goarch: "386"
|
|
||||||
- goos: windows
|
- goos: windows
|
||||||
goarch: "386"
|
goarch: "386"
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
@@ -71,7 +80,7 @@ nfpms:
|
|||||||
- id: infisical
|
- id: infisical
|
||||||
package_name: infisical
|
package_name: infisical
|
||||||
builds:
|
builds:
|
||||||
- infisical
|
- all-other-builds
|
||||||
vendor: Infisical, Inc
|
vendor: Infisical, Inc
|
||||||
homepage: https://infisical.com/
|
homepage: https://infisical.com/
|
||||||
maintainer: Infisical, Inc
|
maintainer: Infisical, Inc
|
||||||
@@ -81,6 +90,7 @@ nfpms:
|
|||||||
- rpm
|
- rpm
|
||||||
- deb
|
- deb
|
||||||
- apk
|
- apk
|
||||||
|
- archlinux
|
||||||
bindir: /usr/bin
|
bindir: /usr/bin
|
||||||
scoop:
|
scoop:
|
||||||
bucket:
|
bucket:
|
||||||
|
|||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npx lint-staged
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
<img width="300" src="/img/logoname-black.svg#gh-light-mode-only" alt="ifnisical">
|
<img width="300" src="/img/logoname-black.svg#gh-light-mode-only" alt="infisical">
|
||||||
<img width="300" src="/img/logoname-white.svg#gh-dark-mode-only" alt="infisical">
|
<img width="300" src="/img/logoname-white.svg#gh-dark-mode-only" alt="infisical">
|
||||||
</h1>
|
</h1>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -27,6 +27,9 @@
|
|||||||
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">
|
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">
|
||||||
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
|
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://twitter.com/infisical">
|
||||||
|
<img src="https://img.shields.io/twitter/follow/infisical?label=Follow" alt="Infisical Twitter" />
|
||||||
|
</a>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<img src="/img/infisical_github_repo.png" width="100%" alt="Dashboard" />
|
<img src="/img/infisical_github_repo.png" width="100%" alt="Dashboard" />
|
||||||
@@ -52,9 +55,15 @@ And more.
|
|||||||
|
|
||||||
To quickly get started, visit our [get started guide](https://infisical.com/docs/getting-started/introduction).
|
To quickly get started, visit our [get started guide](https://infisical.com/docs/getting-started/introduction).
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://infisical.com/docs/self-hosting/overview" target="_blank"><img src="https://user-images.githubusercontent.com/78047717/206356882-2b773eed-b0da-4725-ae2f-83e3cd7f2713.png" height=120 /> </a>
|
||||||
|
<a href="https://www.youtube.com/watch?v=JS3OKYU2078" target="_blank"><img src="https://user-images.githubusercontent.com/78047717/206356600-8833b128-6cae-408c-a703-07b2fc6aff4b.png" height=120 /> </a>
|
||||||
|
<a href="https://app.infisical.com/signup" target="_blank"><img src="https://user-images.githubusercontent.com/78047717/206355970-f4c09062-b88f-452a-94e0-9c61a0651170.png" height=120></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
## 🔥 What's cool about this?
|
## 🔥 What's cool about this?
|
||||||
|
|
||||||
Infisical makes secret management simple and end-to-end encrypted by default. We're on a mission to make it more accessible to all developers, <i>not just security teams</i>.
|
Infisical makes secret management simple and end-to-end encrypted by default. We're on a mission to make it more accessible to all developers, <i>not just security teams</i>.
|
||||||
|
|
||||||
According to a [report](https://www.ekransystem.com/en/blog/secrets-management) in 2019, only 10% of organizations use secret management solutions despite all using digital secrets to some extent.
|
According to a [report](https://www.ekransystem.com/en/blog/secrets-management) in 2019, only 10% of organizations use secret management solutions despite all using digital secrets to some extent.
|
||||||
|
|
||||||
@@ -66,14 +75,17 @@ We are currently working hard to make Infisical more extensive. Need any integra
|
|||||||
|
|
||||||
Whether it's big or small, we love contributions ❤️ Check out our guide to see how to [get started](https://infisical.com/docs/contributing/overview).
|
Whether it's big or small, we love contributions ❤️ Check out our guide to see how to [get started](https://infisical.com/docs/contributing/overview).
|
||||||
|
|
||||||
Not sure where to get started? [Book a free, non-pressure pairing sessions with one of our teammates](mailto:[email protected]?subject=Pairing%20session&body=I'd%20like%20to%20do%20a%20pairing%20session!)!
|
Not sure where to get started? You can:
|
||||||
|
|
||||||
|
- [Book a free, non-pressure pairing sessions with one of our teammates](mailto:[email protected]?subject=Pairing%20session&body=I'd%20like%20to%20do%20a%20pairing%20session!)!
|
||||||
|
- Join our <a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">Slack</a>, and ask us any questions there.
|
||||||
|
|
||||||
## 💚 Community & Support
|
## 💚 Community & Support
|
||||||
|
|
||||||
- [Slack](https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g) (For live discussion with the community and the Infisical team)
|
- [Slack](https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g) (For live discussion with the community and the Infisical team)
|
||||||
- [GitHub Discussions](https://github.com/Infisical/infisical/discussions) (For help with building and deeper conversations about features)
|
- [GitHub Discussions](https://github.com/Infisical/infisical/discussions) (For help with building and deeper conversations about features)
|
||||||
- [GitHub Issues](https://github.com/Infisical/infisical-cli/issues) (For any bugs and errors you encounter using Infisical)
|
- [GitHub Issues](https://github.com/Infisical/infisical-cli/issues) (For any bugs and errors you encounter using Infisical)
|
||||||
- [Twitter](https://twitter.com/infisical) (Get news fast)
|
- [Twitter](https://twitter.com/infisical) (Get news fast)
|
||||||
|
|
||||||
## 🐥 Status
|
## 🐥 Status
|
||||||
|
|
||||||
@@ -83,12 +95,6 @@ Not sure where to get started? [Book a free, non-pressure pairing sessions with
|
|||||||
|
|
||||||
We're currently in Public Alpha.
|
We're currently in Public Alpha.
|
||||||
|
|
||||||
## 🚨 Stay Up-to-Date
|
|
||||||
|
|
||||||
Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of new features are coming very quickly. Watch **releases** of this repository to be notified about future updates:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 🔌 Integrations
|
## 🔌 Integrations
|
||||||
|
|
||||||
We're currently setting the foundation and building [integrations](https://infisical.com/docs/integrations/overview) so secrets can be synced everywhere. Any help is welcome! :)
|
We're currently setting the foundation and building [integrations](https://infisical.com/docs/integrations/overview) so secrets can be synced everywhere. Any help is welcome! :)
|
||||||
@@ -122,10 +128,14 @@ We're currently setting the foundation and building [integrations](https://infis
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" valign="middle">
|
<td align="left" valign="middle">
|
||||||
🔜 Vercel (https://github.com/Infisical/infisical/issues/60)
|
<a href="https://infisical.com/docs/integrations/cloud/vercel?ref=github.com">
|
||||||
|
✔️ Vercel
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="left" valign="middle">
|
<td align="left" valign="middle">
|
||||||
🔜 GitLab CI/CD
|
<a href="https://infisical.com/docs/integrations/platforms/kubernetes?ref=github.com">
|
||||||
|
✔️ Kubernetes
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="left" valign="middle">
|
<td align="left" valign="middle">
|
||||||
🔜 Fly.io
|
🔜 Fly.io
|
||||||
@@ -136,7 +146,9 @@ We're currently setting the foundation and building [integrations](https://infis
|
|||||||
🔜 AWS
|
🔜 AWS
|
||||||
</td>
|
</td>
|
||||||
<td align="left" valign="middle">
|
<td align="left" valign="middle">
|
||||||
🔜 GitHub Actions (https://github.com/Infisical/infisical/issues/54)
|
<a href="https://infisical.com/docs/integrations/cicd/githubactions">
|
||||||
|
✔️ GitHub Actions
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="left" valign="middle">
|
<td align="left" valign="middle">
|
||||||
🔜 Railway
|
🔜 Railway
|
||||||
@@ -147,10 +159,10 @@ We're currently setting the foundation and building [integrations](https://infis
|
|||||||
🔜 GCP
|
🔜 GCP
|
||||||
</td>
|
</td>
|
||||||
<td align="left" valign="middle">
|
<td align="left" valign="middle">
|
||||||
🔜 Kubernetes
|
🔜 GitLab CI/CD (https://github.com/Infisical/infisical/issues/134)
|
||||||
</td>
|
</td>
|
||||||
<td align="left" valign="middle">
|
<td align="left" valign="middle">
|
||||||
🔜 CircleCI
|
🔜 CircleCI (https://github.com/Infisical/infisical/issues/91)
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -169,7 +181,23 @@ We're currently setting the foundation and building [integrations](https://infis
|
|||||||
🔜 TravisCI
|
🔜 TravisCI
|
||||||
</td>
|
</td>
|
||||||
<td align="left" valign="middle">
|
<td align="left" valign="middle">
|
||||||
🔜 Netlify (https://github.com/Infisical/infisical/issues/55)
|
<a href="https://infisical.com/docs/integrations/cloud/netlify">
|
||||||
|
✔️ Netlify
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="left" valign="middle">
|
||||||
|
🔜 Railway
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left" valign="middle">
|
||||||
|
🔜 Bitbucket
|
||||||
|
</td>
|
||||||
|
<td align="left" valign="middle">
|
||||||
|
🔜 Supabase
|
||||||
|
</td>
|
||||||
|
<td align="left" valign="middle">
|
||||||
|
🔜 Render (https://github.com/Infisical/infisical/issues/132)
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -178,7 +206,6 @@ We're currently setting the foundation and building [integrations](https://infis
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -253,6 +280,18 @@ We're currently setting the foundation and building [integrations](https://infis
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left" valign="middle">
|
||||||
|
<a href="https://infisical.com/docs/integrations/frameworks/fiber?ref=github.com">
|
||||||
|
✔️ Fiber
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="left" valign="middle">
|
||||||
|
<a href="https://infisical.com/docs/integrations/frameworks/nuxt?ref=github.com">
|
||||||
|
✔️ Nuxt
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -260,7 +299,6 @@ We're currently setting the foundation and building [integrations](https://infis
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
## 🏘 Open-source vs. paid
|
## 🏘 Open-source vs. paid
|
||||||
|
|
||||||
This repo is entirely MIT licensed, with the exception of the `ee` directory which will contain premium enterprise features requiring a Infisical license in the future. We're currently focused on developing non-enterprise offerings first that should suit most use-cases.
|
This repo is entirely MIT licensed, with the exception of the `ee` directory which will contain premium enterprise features requiring a Infisical license in the future. We're currently focused on developing non-enterprise offerings first that should suit most use-cases.
|
||||||
@@ -269,6 +307,12 @@ This repo is entirely MIT licensed, with the exception of the `ee` directory whi
|
|||||||
|
|
||||||
Looking to report a security vulnerability? Please don't post about it in GitHub issue. Instead, refer to our [SECURITY.md](./SECURITY.md) file.
|
Looking to report a security vulnerability? Please don't post about it in GitHub issue. Instead, refer to our [SECURITY.md](./SECURITY.md) file.
|
||||||
|
|
||||||
|
## 🚨 Stay Up-to-Date
|
||||||
|
|
||||||
|
Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of new features are coming very quickly. Watch **releases** of this repository to be notified about future updates:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 🦸 Contributors
|
## 🦸 Contributors
|
||||||
|
|
||||||
[//]: contributor-faces
|
[//]: contributor-faces
|
||||||
@@ -277,4 +321,4 @@ Looking to report a security vulnerability? Please don't post about it in GitHub
|
|||||||
<!-- prettier-ignore-start -->
|
<!-- prettier-ignore-start -->
|
||||||
<!-- markdownlint-disable -->
|
<!-- markdownlint-disable -->
|
||||||
|
|
||||||
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a>
|
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
|
||||||
|
|||||||
+11
-17
@@ -1,18 +1,12 @@
|
|||||||
{
|
{
|
||||||
"root": true,
|
"parser": "@typescript-eslint/parser",
|
||||||
"parser": "@typescript-eslint/parser",
|
"plugins": ["@typescript-eslint"],
|
||||||
"plugins": [
|
"extends": [
|
||||||
"@typescript-eslint",
|
"eslint:recommended",
|
||||||
"prettier"
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
],
|
"plugin:@typescript-eslint/recommended"
|
||||||
"extends": [
|
],
|
||||||
"eslint:recommended",
|
"rules": {
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
"no-console": 2
|
||||||
"plugin:@typescript-eslint/recommended",
|
}
|
||||||
"prettier"
|
}
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"no-console": 2,
|
|
||||||
"prettier/prettier": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": true,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"singleQuote": true,
|
|
||||||
"printWidth": 80,
|
|
||||||
"useTabs": true
|
|
||||||
}
|
|
||||||
+6
-3
@@ -2,11 +2,14 @@ FROM node:16-bullseye-slim
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package*.json .
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
RUN npm install
|
RUN npm ci --only-production --ignore-scripts
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
CMD ["npm", "run", "start"]
|
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
||||||
|
CMD node healthcheck.js
|
||||||
|
|
||||||
|
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { server } from '../src/app';
|
||||||
|
import { describe, expect, it, beforeAll, afterAll } from '@jest/globals';
|
||||||
|
import supertest from 'supertest';
|
||||||
|
import { setUpHealthEndpoint } from '../src/services/health';
|
||||||
|
|
||||||
|
const requestWithSupertest = supertest(server);
|
||||||
|
describe('Healthcheck endpoint', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
setUpHealthEndpoint(server);
|
||||||
|
});
|
||||||
|
afterAll(async () => {
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /healthcheck should return OK', async () => {
|
||||||
|
const res = await requestWithSupertest.get('/healthcheck');
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
Vendored
+8
-4
@@ -14,12 +14,16 @@ declare global {
|
|||||||
JWT_SIGNUP_SECRET: string;
|
JWT_SIGNUP_SECRET: string;
|
||||||
MONGO_URL: string;
|
MONGO_URL: string;
|
||||||
NODE_ENV: 'development' | 'staging' | 'testing' | 'production';
|
NODE_ENV: 'development' | 'staging' | 'testing' | 'production';
|
||||||
OAUTH_CLIENT_SECRET_HEROKU: string;
|
VERBOSE_ERROR_OUTPUT: string;
|
||||||
OAUTH_TOKEN_URL_HEROKU: string;
|
LOKI_HOST: string;
|
||||||
|
CLIENT_ID_HEROKU: string;
|
||||||
|
CLIENT_ID_VERCEL: string;
|
||||||
|
CLIENT_ID_NETLIFY: string;
|
||||||
|
CLIENT_SECRET_HEROKU: string;
|
||||||
|
CLIENT_SECRET_VERCEL: string;
|
||||||
|
CLIENT_SECRET_NETLIFY: string;
|
||||||
POSTHOG_HOST: string;
|
POSTHOG_HOST: string;
|
||||||
POSTHOG_PROJECT_API_KEY: string;
|
POSTHOG_PROJECT_API_KEY: string;
|
||||||
PRIVATE_KEY: string;
|
|
||||||
PUBLIC_KEY: string;
|
|
||||||
SENTRY_DSN: string;
|
SENTRY_DSN: string;
|
||||||
SITE_URL: string;
|
SITE_URL: string;
|
||||||
SMTP_HOST: string;
|
SMTP_HOST: string;
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
const http = require('http');
|
||||||
|
const PORT = process.env.PORT || 4000;
|
||||||
|
const options = {
|
||||||
|
host: 'localhost',
|
||||||
|
port: PORT,
|
||||||
|
timeout: 2000,
|
||||||
|
path: '/healthcheck'
|
||||||
|
};
|
||||||
|
|
||||||
|
const healthCheck = http.request(options, (res) => {
|
||||||
|
console.log(`HEALTHCHECK STATUS: ${res.statusCode}`);
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
healthCheck.on('error', function (err) {
|
||||||
|
console.error(`HEALTH CHECK ERROR: ${err}`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
healthCheck.end();
|
||||||
Generated
+1715
-405
File diff suppressed because it is too large
Load Diff
+47
-13
@@ -1,8 +1,11 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@godaddy/terminus": "^4.11.2",
|
||||||
|
"@octokit/rest": "^19.0.5",
|
||||||
"@sentry/node": "^7.14.0",
|
"@sentry/node": "^7.14.0",
|
||||||
"@sentry/tracing": "^7.14.0",
|
"@sentry/tracing": "^7.19.0",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
|
"@types/libsodium-wrappers": "^0.7.10",
|
||||||
"axios": "^1.1.3",
|
"axios": "^1.1.3",
|
||||||
"bigint-conversion": "^2.2.2",
|
"bigint-conversion": "^2.2.2",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
@@ -10,32 +13,40 @@
|
|||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
"express": "^4.18.1",
|
"express": "^4.18.1",
|
||||||
"express-rate-limit": "^6.5.1",
|
"express-rate-limit": "^6.7.0",
|
||||||
"express-validator": "^6.14.2",
|
"express-validator": "^6.14.2",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"helmet": "^5.1.1",
|
"helmet": "^5.1.1",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"jsrp": "^0.2.4",
|
"jsrp": "^0.2.4",
|
||||||
"mongoose": "^6.7.1",
|
"libsodium-wrappers": "^0.7.10",
|
||||||
|
"mongoose": "^6.7.2",
|
||||||
"nodemailer": "^6.8.0",
|
"nodemailer": "^6.8.0",
|
||||||
"posthog-node": "^2.1.0",
|
"posthog-node": "^2.2.2",
|
||||||
"query-string": "^7.1.1",
|
"query-string": "^7.1.3",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"stripe": "^10.7.0",
|
"stripe": "^10.7.0",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
"tweetnacl-util": "^0.15.1",
|
"tweetnacl-util": "^0.15.1",
|
||||||
"typescript": "^4.8.4"
|
"typescript": "^4.9.3",
|
||||||
|
"winston": "^3.8.2",
|
||||||
|
"winston-loki": "^6.0.6"
|
||||||
},
|
},
|
||||||
"name": "infisical-api",
|
"name": "infisical-api",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"prepare": "cd .. && npm install",
|
||||||
"start": "npm run build && node build/index.js",
|
"start": "npm run build && node build/index.js",
|
||||||
"dev": "nodemon",
|
"dev": "nodemon",
|
||||||
"build": "rimraf ./build && tsc && cp -R ./src/templates ./src/json ./build",
|
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build",
|
||||||
"lint": "eslint . --ext .ts",
|
"lint": "eslint . --ext .ts",
|
||||||
"lint-and-fix": "eslint . --ext .ts --fix",
|
"lint-and-fix": "eslint . --ext .ts --fix",
|
||||||
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write"
|
"lint-staged": "lint-staged",
|
||||||
|
"pretest": "docker compose -f test-resources/docker-compose.test.yml up -d",
|
||||||
|
"test": "cross-env NODE_ENV=test jest --testTimeout=10000 --detectOpenHandles",
|
||||||
|
"test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json",
|
||||||
|
"posttest": "docker compose -f test-resources/docker-compose.test.yml down"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -49,26 +60,49 @@
|
|||||||
"homepage": "https://github.com/Infisical/infisical-api#readme",
|
"homepage": "https://github.com/Infisical/infisical-api#readme",
|
||||||
"description": "",
|
"description": "",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@jest/globals": "^29.3.1",
|
||||||
"@posthog/plugin-scaffold": "^1.3.4",
|
"@posthog/plugin-scaffold": "^1.3.4",
|
||||||
"@types/cookie-parser": "^1.4.3",
|
"@types/cookie-parser": "^1.4.3",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
"@types/express": "^4.17.14",
|
"@types/express": "^4.17.14",
|
||||||
|
"@types/jest": "^29.2.4",
|
||||||
"@types/jsonwebtoken": "^8.5.9",
|
"@types/jsonwebtoken": "^8.5.9",
|
||||||
"@types/node": "^18.11.3",
|
"@types/node": "^18.11.3",
|
||||||
"@types/nodemailer": "^6.4.6",
|
"@types/nodemailer": "^6.4.6",
|
||||||
|
"@types/supertest": "^2.0.12",
|
||||||
"@types/swagger-jsdoc": "^6.0.1",
|
"@types/swagger-jsdoc": "^6.0.1",
|
||||||
"@types/swagger-ui-express": "^4.1.3",
|
"@types/swagger-ui-express": "^4.1.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||||
"@typescript-eslint/parser": "^5.40.1",
|
"@typescript-eslint/parser": "^5.40.1",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.26.0",
|
"eslint": "^8.26.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
|
||||||
"husky": "^8.0.1",
|
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
"jest": "^29.3.1",
|
"jest": "^29.3.1",
|
||||||
|
"jest-junit": "^15.0.0",
|
||||||
"nodemon": "^2.0.19",
|
"nodemon": "^2.0.19",
|
||||||
"npm": "^8.19.3",
|
"npm": "^8.19.3",
|
||||||
"prettier": "^2.7.1",
|
"supertest": "^6.3.3",
|
||||||
|
"ts-jest": "^29.0.3",
|
||||||
"ts-node": "^10.9.1"
|
"ts-node": "^10.9.1"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"preset": "ts-jest",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"src/*.{js,ts}",
|
||||||
|
"!**/node_modules/**"
|
||||||
|
],
|
||||||
|
"setupFiles": [
|
||||||
|
"<rootDir>/test-resources/env-vars.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"jest-junit": {
|
||||||
|
"outputDirectory": "reports",
|
||||||
|
"outputName": "jest-junit.xml",
|
||||||
|
"ancestorSeparator": " › ",
|
||||||
|
"uniqueOutputName": "false",
|
||||||
|
"suiteNameTemplate": "{filepath}",
|
||||||
|
"classNameTemplate": "{classname}",
|
||||||
|
"titleTemplate": "{title}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
|
||||||
|
import { patchRouterParam } from './utils/patchAsyncRoutes';
|
||||||
|
import express from 'express';
|
||||||
|
import helmet from 'helmet';
|
||||||
|
import cors from 'cors';
|
||||||
|
import cookieParser from 'cookie-parser';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
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';
|
||||||
|
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();
|
||||||
|
|
||||||
|
app.enable('trust proxy');
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(
|
||||||
|
cors({
|
||||||
|
credentials: true,
|
||||||
|
origin: SITE_URL
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (NODE_ENV === 'production') {
|
||||||
|
// enable app-wide rate-limiting + helmet security
|
||||||
|
// in production
|
||||||
|
app.disable('x-powered-by');
|
||||||
|
app.use(apiLimiter);
|
||||||
|
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);
|
||||||
|
|
||||||
|
|
||||||
|
//* 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, () => {
|
||||||
|
getLogger("backend-main").info(`Server started listening at port ${PORT}`)
|
||||||
|
});
|
||||||
+61
-39
@@ -10,56 +10,78 @@ const JWT_SIGNUP_LIFETIME = process.env.JWT_SIGNUP_LIFETIME! || '15m';
|
|||||||
const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!;
|
const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!;
|
||||||
const MONGO_URL = process.env.MONGO_URL!;
|
const MONGO_URL = process.env.MONGO_URL!;
|
||||||
const NODE_ENV = process.env.NODE_ENV! || 'production';
|
const NODE_ENV = process.env.NODE_ENV! || 'production';
|
||||||
const OAUTH_CLIENT_SECRET_HEROKU = process.env.OAUTH_CLIENT_SECRET_HEROKU!;
|
const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true;
|
||||||
const OAUTH_TOKEN_URL_HEROKU = process.env.OAUTH_TOKEN_URL_HEROKU!;
|
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!;
|
||||||
|
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!;
|
||||||
|
const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!;
|
||||||
|
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
|
||||||
|
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
|
||||||
|
const CLIENT_SECRET_GITHUB = process.env.CLIENT_SECRET_GITHUB!;
|
||||||
|
const CLIENT_SLUG_VERCEL= process.env.CLIENT_SLUG_VERCEL!;
|
||||||
const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com';
|
const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com';
|
||||||
const POSTHOG_PROJECT_API_KEY = process.env.POSTHOG_PROJECT_API_KEY! || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE';
|
const POSTHOG_PROJECT_API_KEY =
|
||||||
const PRIVATE_KEY = process.env.PRIVATE_KEY!;
|
process.env.POSTHOG_PROJECT_API_KEY! ||
|
||||||
const PUBLIC_KEY = process.env.PUBLIC_KEY!;
|
'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE';
|
||||||
const SENTRY_DSN = process.env.SENTRY_DSN!;
|
const SENTRY_DSN = process.env.SENTRY_DSN!;
|
||||||
const SITE_URL = process.env.SITE_URL!;
|
const SITE_URL = process.env.SITE_URL!;
|
||||||
const SMTP_HOST = process.env.SMTP_HOST! || 'smtp.gmail.com';
|
const SMTP_HOST = process.env.SMTP_HOST!;
|
||||||
const SMTP_NAME = process.env.SMTP_NAME!;
|
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_USERNAME = process.env.SMTP_USERNAME!;
|
||||||
const SMTP_PASSWORD = process.env.SMTP_PASSWORD!;
|
const SMTP_PASSWORD = process.env.SMTP_PASSWORD!;
|
||||||
|
const SMTP_FROM_ADDRESS = process.env.SMTP_FROM_ADDRESS!;
|
||||||
|
const SMTP_FROM_NAME = process.env.SMTP_FROM_NAME! || 'Infisical';
|
||||||
const STRIPE_PRODUCT_CARD_AUTH = process.env.STRIPE_PRODUCT_CARD_AUTH!;
|
const STRIPE_PRODUCT_CARD_AUTH = process.env.STRIPE_PRODUCT_CARD_AUTH!;
|
||||||
const STRIPE_PRODUCT_PRO = process.env.STRIPE_PRODUCT_PRO!;
|
const STRIPE_PRODUCT_PRO = process.env.STRIPE_PRODUCT_PRO!;
|
||||||
const STRIPE_PRODUCT_STARTER = process.env.STRIPE_PRODUCT_STARTER!;
|
const STRIPE_PRODUCT_STARTER = process.env.STRIPE_PRODUCT_STARTER!;
|
||||||
const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY!;
|
const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY!;
|
||||||
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY!;
|
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY!;
|
||||||
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!;
|
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!;
|
||||||
const TELEMETRY_ENABLED = (process.env.TELEMETRY_ENABLED! !== 'false') && true;
|
const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED! !== 'false' && true;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
PORT,
|
PORT,
|
||||||
EMAIL_TOKEN_LIFETIME,
|
EMAIL_TOKEN_LIFETIME,
|
||||||
ENCRYPTION_KEY,
|
ENCRYPTION_KEY,
|
||||||
JWT_AUTH_LIFETIME,
|
JWT_AUTH_LIFETIME,
|
||||||
JWT_AUTH_SECRET,
|
JWT_AUTH_SECRET,
|
||||||
JWT_REFRESH_LIFETIME,
|
JWT_REFRESH_LIFETIME,
|
||||||
JWT_REFRESH_SECRET,
|
JWT_REFRESH_SECRET,
|
||||||
JWT_SERVICE_SECRET,
|
JWT_SERVICE_SECRET,
|
||||||
JWT_SIGNUP_LIFETIME,
|
JWT_SIGNUP_LIFETIME,
|
||||||
JWT_SIGNUP_SECRET,
|
JWT_SIGNUP_SECRET,
|
||||||
MONGO_URL,
|
MONGO_URL,
|
||||||
NODE_ENV,
|
NODE_ENV,
|
||||||
OAUTH_CLIENT_SECRET_HEROKU,
|
VERBOSE_ERROR_OUTPUT,
|
||||||
OAUTH_TOKEN_URL_HEROKU,
|
LOKI_HOST,
|
||||||
POSTHOG_HOST,
|
CLIENT_ID_HEROKU,
|
||||||
POSTHOG_PROJECT_API_KEY,
|
CLIENT_ID_VERCEL,
|
||||||
PRIVATE_KEY,
|
CLIENT_ID_NETLIFY,
|
||||||
PUBLIC_KEY,
|
CLIENT_ID_GITHUB,
|
||||||
SENTRY_DSN,
|
CLIENT_SECRET_HEROKU,
|
||||||
SITE_URL,
|
CLIENT_SECRET_VERCEL,
|
||||||
SMTP_HOST,
|
CLIENT_SECRET_NETLIFY,
|
||||||
SMTP_NAME,
|
CLIENT_SECRET_GITHUB,
|
||||||
SMTP_USERNAME,
|
CLIENT_SLUG_VERCEL,
|
||||||
SMTP_PASSWORD,
|
POSTHOG_HOST,
|
||||||
STRIPE_PRODUCT_CARD_AUTH,
|
POSTHOG_PROJECT_API_KEY,
|
||||||
STRIPE_PRODUCT_PRO,
|
SENTRY_DSN,
|
||||||
STRIPE_PRODUCT_STARTER,
|
SITE_URL,
|
||||||
STRIPE_PUBLISHABLE_KEY,
|
SMTP_HOST,
|
||||||
STRIPE_SECRET_KEY,
|
SMTP_PORT,
|
||||||
STRIPE_WEBHOOK_SECRET,
|
SMTP_SECURE,
|
||||||
TELEMETRY_ENABLED
|
SMTP_USERNAME,
|
||||||
|
SMTP_PASSWORD,
|
||||||
|
SMTP_FROM_ADDRESS,
|
||||||
|
SMTP_FROM_NAME,
|
||||||
|
STRIPE_PRODUCT_CARD_AUTH,
|
||||||
|
STRIPE_PRODUCT_PRO,
|
||||||
|
STRIPE_PRODUCT_STARTER,
|
||||||
|
STRIPE_PUBLISHABLE_KEY,
|
||||||
|
STRIPE_SECRET_KEY,
|
||||||
|
STRIPE_WEBHOOK_SECRET,
|
||||||
|
TELEMETRY_ENABLED
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
@@ -5,17 +6,17 @@ import * as bigintConversion from 'bigint-conversion';
|
|||||||
const jsrp = require('jsrp');
|
const jsrp = require('jsrp');
|
||||||
import { User } from '../models';
|
import { User } from '../models';
|
||||||
import { createToken, issueTokens, clearTokens } from '../helpers/auth';
|
import { createToken, issueTokens, clearTokens } from '../helpers/auth';
|
||||||
import {
|
import {
|
||||||
NODE_ENV,
|
NODE_ENV,
|
||||||
JWT_AUTH_LIFETIME,
|
JWT_AUTH_LIFETIME,
|
||||||
JWT_AUTH_SECRET,
|
JWT_AUTH_SECRET,
|
||||||
JWT_REFRESH_SECRET
|
JWT_REFRESH_SECRET
|
||||||
} from '../config';
|
} from '../config';
|
||||||
|
|
||||||
declare module 'jsonwebtoken' {
|
declare module 'jsonwebtoken' {
|
||||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||||
userId: string;
|
userId: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientPublicKeys: any = {};
|
const clientPublicKeys: any = {};
|
||||||
@@ -27,47 +28,45 @@ const clientPublicKeys: any = {};
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const login1 = async (req: Request, res: Response) => {
|
export const login1 = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
email,
|
email,
|
||||||
clientPublicKey
|
clientPublicKey
|
||||||
}: { email: string; clientPublicKey: string } = req.body;
|
}: { email: string; clientPublicKey: string } = req.body;
|
||||||
|
|
||||||
const user = await User.findOne({
|
|
||||||
email
|
|
||||||
}).select('+salt +verifier');
|
|
||||||
|
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
const user = await User.findOne({
|
||||||
|
email
|
||||||
|
}).select('+salt +verifier');
|
||||||
|
|
||||||
const server = new jsrp.server();
|
if (!user) throw new Error('Failed to find user');
|
||||||
server.init(
|
|
||||||
{
|
|
||||||
salt: user.salt,
|
|
||||||
verifier: user.verifier
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
// generate server-side public key
|
|
||||||
const serverPublicKey = server.getPublicKey();
|
|
||||||
clientPublicKeys[email] = {
|
|
||||||
clientPublicKey,
|
|
||||||
serverBInt: bigintConversion.bigintToBuf(server.bInt)
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return res.status(200).send({
|
const server = new jsrp.server();
|
||||||
serverPublicKey,
|
server.init(
|
||||||
salt: user.salt
|
{
|
||||||
});
|
salt: user.salt,
|
||||||
}
|
verifier: user.verifier
|
||||||
);
|
},
|
||||||
} catch (err) {
|
() => {
|
||||||
Sentry.setUser(null);
|
// generate server-side public key
|
||||||
Sentry.captureException(err);
|
const serverPublicKey = server.getPublicKey();
|
||||||
return res.status(400).send({
|
clientPublicKeys[email] = {
|
||||||
message: 'Failed to start authentication process'
|
clientPublicKey,
|
||||||
});
|
serverBInt: bigintConversion.bigintToBuf(server.bInt)
|
||||||
}
|
};
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
serverPublicKey,
|
||||||
|
salt: user.salt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to start authentication process'
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,59 +77,59 @@ export const login1 = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const login2 = async (req: Request, res: Response) => {
|
export const login2 = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { email, clientProof } = req.body;
|
const { email, clientProof } = req.body;
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
email
|
email
|
||||||
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
|
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
const server = new jsrp.server();
|
const server = new jsrp.server();
|
||||||
server.init(
|
server.init(
|
||||||
{
|
{
|
||||||
salt: user.salt,
|
salt: user.salt,
|
||||||
verifier: user.verifier,
|
verifier: user.verifier,
|
||||||
b: clientPublicKeys[email].serverBInt
|
b: clientPublicKeys[email].serverBInt
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
server.setClientPublicKey(clientPublicKeys[email].clientPublicKey);
|
server.setClientPublicKey(clientPublicKeys[email].clientPublicKey);
|
||||||
|
|
||||||
// compare server and client shared keys
|
// compare server and client shared keys
|
||||||
if (server.checkClientProof(clientProof)) {
|
if (server.checkClientProof(clientProof)) {
|
||||||
// issue tokens
|
// issue tokens
|
||||||
const tokens = await issueTokens({ userId: user._id.toString() });
|
const tokens = await issueTokens({ userId: user._id.toString() });
|
||||||
|
|
||||||
// store (refresh) token in httpOnly cookie
|
|
||||||
res.cookie('jid', tokens.refreshToken, {
|
|
||||||
httpOnly: true,
|
|
||||||
path: '/token',
|
|
||||||
sameSite: "strict",
|
|
||||||
secure: NODE_ENV === 'production' ? true : false
|
|
||||||
});
|
|
||||||
|
|
||||||
// return (access) token in response
|
// store (refresh) token in httpOnly cookie
|
||||||
return res.status(200).send({
|
res.cookie('jid', tokens.refreshToken, {
|
||||||
token: tokens.token,
|
httpOnly: true,
|
||||||
publicKey: user.publicKey,
|
path: '/',
|
||||||
encryptedPrivateKey: user.encryptedPrivateKey,
|
sameSite: 'strict',
|
||||||
iv: user.iv,
|
secure: NODE_ENV === 'production' ? true : false
|
||||||
tag: user.tag
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(400).send({
|
// return (access) token in response
|
||||||
message: 'Failed to authenticate. Try again?'
|
return res.status(200).send({
|
||||||
});
|
token: tokens.token,
|
||||||
}
|
publicKey: user.publicKey,
|
||||||
);
|
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||||
} catch (err) {
|
iv: user.iv,
|
||||||
Sentry.setUser(null);
|
tag: user.tag
|
||||||
Sentry.captureException(err);
|
});
|
||||||
return res.status(400).send({
|
}
|
||||||
message: 'Failed to authenticate. Try again?'
|
|
||||||
});
|
return res.status(400).send({
|
||||||
}
|
message: 'Failed to authenticate. Try again?'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to authenticate. Try again?'
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -140,29 +139,29 @@ export const login2 = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const logout = async (req: Request, res: Response) => {
|
export const logout = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
await clearTokens({
|
await clearTokens({
|
||||||
userId: req.user._id.toString()
|
userId: req.user._id.toString()
|
||||||
});
|
});
|
||||||
|
|
||||||
// clear httpOnly cookie
|
|
||||||
res.cookie('jid', '', {
|
|
||||||
httpOnly: true,
|
|
||||||
path: '/token',
|
|
||||||
sameSite: "strict",
|
|
||||||
secure: NODE_ENV === 'production' ? true : false
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to logout'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
// clear httpOnly cookie
|
||||||
message: 'Successfully logged out.'
|
res.cookie('jid', '', {
|
||||||
});
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
sameSite: 'strict',
|
||||||
|
secure: NODE_ENV === 'production' ? true : false
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to logout'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully logged out.'
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,9 +171,9 @@ export const logout = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const checkAuth = async (req: Request, res: Response) =>
|
export const checkAuth = async (req: Request, res: Response) =>
|
||||||
res.status(200).send({
|
res.status(200).send({
|
||||||
message: 'Authenticated'
|
message: 'Authenticated'
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return new token by redeeming refresh token
|
* Return new token by redeeming refresh token
|
||||||
@@ -183,42 +182,41 @@ export const checkAuth = async (req: Request, res: Response) =>
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getNewToken = async (req: Request, res: Response) => {
|
export const getNewToken = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const refreshToken = req.cookies.jid;
|
const refreshToken = req.cookies.jid;
|
||||||
|
|
||||||
if (!refreshToken) {
|
|
||||||
throw new Error('Failed to find token in request cookies');
|
|
||||||
}
|
|
||||||
|
|
||||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
|
||||||
jwt.verify(refreshToken, JWT_REFRESH_SECRET)
|
|
||||||
);
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
|
||||||
_id: decodedToken.userId
|
|
||||||
}).select('+publicKey');
|
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to authenticate unfound user');
|
if (!refreshToken) {
|
||||||
if (!user?.publicKey)
|
throw new Error('Failed to find token in request cookies');
|
||||||
throw new Error('Failed to authenticate not fully set up account');
|
}
|
||||||
|
|
||||||
const token = createToken({
|
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||||
payload: {
|
jwt.verify(refreshToken, JWT_REFRESH_SECRET)
|
||||||
userId: decodedToken.userId
|
);
|
||||||
},
|
|
||||||
expiresIn: JWT_AUTH_LIFETIME,
|
const user = await User.findOne({
|
||||||
secret: JWT_AUTH_SECRET
|
_id: decodedToken.userId
|
||||||
});
|
}).select('+publicKey');
|
||||||
|
|
||||||
return res.status(200).send({
|
if (!user) throw new Error('Failed to authenticate unfound user');
|
||||||
token
|
if (!user?.publicKey)
|
||||||
});
|
throw new Error('Failed to authenticate not fully set up account');
|
||||||
|
|
||||||
} catch (err) {
|
const token = createToken({
|
||||||
Sentry.setUser(null);
|
payload: {
|
||||||
Sentry.captureException(err);
|
userId: decodedToken.userId
|
||||||
return res.status(400).send({
|
},
|
||||||
message: 'Invalid request'
|
expiresIn: JWT_AUTH_LIFETIME,
|
||||||
});
|
secret: JWT_AUTH_SECRET
|
||||||
}
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
token
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Invalid request'
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Bot, BotKey } from '../models';
|
||||||
|
import { createBot } from '../helpers/bot';
|
||||||
|
|
||||||
|
interface BotKey {
|
||||||
|
encryptedKey: string;
|
||||||
|
nonce: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return bot for workspace with id [workspaceId]. If a workspace bot doesn't exist,
|
||||||
|
* then create and return a new bot.
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
||||||
|
let bot;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
bot = await Bot.findOne({
|
||||||
|
workspace: workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bot) {
|
||||||
|
// case: bot doesn't exist for workspace with id [workspaceId]
|
||||||
|
// -> create a new bot and return it
|
||||||
|
bot = await createBot({
|
||||||
|
name: 'Infisical Bot',
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get bot for workspace'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
bot
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return bot with id [req.bot._id] with active state set to [isActive].
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const setBotActiveState = async (req: Request, res: Response) => {
|
||||||
|
let bot;
|
||||||
|
try {
|
||||||
|
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
// bot state set to active -> share workspace key with bot
|
||||||
|
if (!botKey?.encryptedKey || !botKey?.nonce) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to set bot state to active - missing bot key'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await BotKey.findOneAndUpdate({
|
||||||
|
workspace: req.bot.workspace
|
||||||
|
}, {
|
||||||
|
encryptedKey: botKey.encryptedKey,
|
||||||
|
nonce: botKey.nonce,
|
||||||
|
sender: req.user._id,
|
||||||
|
bot: req.bot._id,
|
||||||
|
workspace: req.bot.workspace
|
||||||
|
}, {
|
||||||
|
upsert: true,
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// case: bot state set to inactive -> delete bot's workspace key
|
||||||
|
await BotKey.deleteOne({
|
||||||
|
bot: req.bot._id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bot = await Bot.findOneAndUpdate({
|
||||||
|
_id: req.bot._id
|
||||||
|
}, {
|
||||||
|
isActive
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bot) throw new Error('Failed to update bot active state');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to update bot active state'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
bot
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as authController from './authController';
|
import * as authController from './authController';
|
||||||
|
import * as botController from './botController';
|
||||||
import * as integrationAuthController from './integrationAuthController';
|
import * as integrationAuthController from './integrationAuthController';
|
||||||
import * as integrationController from './integrationController';
|
import * as integrationController from './integrationController';
|
||||||
import * as keyController from './keyController';
|
import * as keyController from './keyController';
|
||||||
@@ -16,6 +17,7 @@ import * as workspaceController from './workspaceController';
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
authController,
|
authController,
|
||||||
|
botController,
|
||||||
integrationAuthController,
|
integrationAuthController,
|
||||||
integrationController,
|
integrationController,
|
||||||
keyController,
|
keyController,
|
||||||
|
|||||||
@@ -3,69 +3,45 @@ import * as Sentry from '@sentry/node';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { IntegrationAuth, Integration } from '../models';
|
import { IntegrationAuth, Integration } from '../models';
|
||||||
import { processOAuthTokenRes } from '../helpers/integrationAuth';
|
import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../variables';
|
||||||
import { INTEGRATION_SET, ENV_DEV } from '../variables';
|
import { IntegrationService } from '../services';
|
||||||
import { OAUTH_CLIENT_SECRET_HEROKU, OAUTH_TOKEN_URL_HEROKU } from '../config';
|
import { getApps, revokeAccess } from '../integrations';
|
||||||
|
|
||||||
|
export const getIntegrationOptions = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
return res.status(200).send({
|
||||||
|
integrationOptions: INTEGRATION_OPTIONS
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId]
|
* Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId]
|
||||||
* Note: integration [integration] must be set up compatible/designed for OAuth2
|
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const integrationAuthOauthExchange = async (
|
export const oAuthExchange = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
let clientSecret;
|
|
||||||
|
|
||||||
const { workspaceId, code, integration } = req.body;
|
const { workspaceId, code, integration } = req.body;
|
||||||
|
|
||||||
if (!INTEGRATION_SET.has(integration))
|
if (!INTEGRATION_SET.has(integration))
|
||||||
throw new Error('Failed to validate integration');
|
throw new Error('Failed to validate integration');
|
||||||
|
|
||||||
// use correct client secret
|
await IntegrationService.handleOAuthExchange({
|
||||||
switch (integration) {
|
|
||||||
case 'heroku':
|
|
||||||
clientSecret = OAUTH_CLIENT_SECRET_HEROKU;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: unfinished - make compatible with other integration types
|
|
||||||
const res = await axios.post(
|
|
||||||
OAUTH_TOKEN_URL_HEROKU!,
|
|
||||||
new URLSearchParams({
|
|
||||||
grant_type: 'authorization_code',
|
|
||||||
code: code,
|
|
||||||
client_secret: clientSecret
|
|
||||||
} as any)
|
|
||||||
);
|
|
||||||
|
|
||||||
const integrationAuth = await processOAuthTokenRes({
|
|
||||||
workspaceId,
|
workspaceId,
|
||||||
integration,
|
integration,
|
||||||
res
|
code
|
||||||
});
|
});
|
||||||
|
|
||||||
// create or replace integration
|
|
||||||
const integrationObj = await Integration.findOneAndUpdate(
|
|
||||||
{ workspace: workspaceId, integration },
|
|
||||||
{
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: ENV_DEV,
|
|
||||||
isActive: false,
|
|
||||||
app: null,
|
|
||||||
integration,
|
|
||||||
integrationAuth: integrationAuth._id
|
|
||||||
},
|
|
||||||
{ upsert: true, new: true }
|
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get OAuth2 token'
|
message: 'Failed to get OAuth2 code-token exchange'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,26 +51,25 @@ export const integrationAuthOauthExchange = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return list of applications allowed for integration with id [integrationAuthId]
|
* Return list of applications allowed for integration with integration authorization id [integrationAuthId]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
||||||
// TODO: unfinished - make compatible with other integration types
|
|
||||||
let apps;
|
let apps;
|
||||||
try {
|
try {
|
||||||
const res = await axios.get('https://api.heroku.com/apps', {
|
apps = await getApps({
|
||||||
headers: {
|
integrationAuth: req.integrationAuth,
|
||||||
Accept: 'application/vnd.heroku+json; version=3',
|
accessToken: req.accessToken
|
||||||
Authorization: 'Bearer ' + req.accessToken
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
apps = res.data.map((a: any) => ({
|
Sentry.setUser(null);
|
||||||
name: a.name
|
Sentry.captureException(err);
|
||||||
}));
|
return res.status(400).send({
|
||||||
} catch (err) {}
|
message: 'Failed to get integration authorization applications'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
apps
|
apps
|
||||||
@@ -108,46 +83,22 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
||||||
// TODO: unfinished - disable application via Heroku API and make compatible with other integration types
|
|
||||||
try {
|
try {
|
||||||
const { integrationAuthId } = req.params;
|
const { integrationAuthId } = req.params;
|
||||||
|
|
||||||
// TODO: disable application via Heroku API; figure out what authorization id is
|
await revokeAccess({
|
||||||
|
integrationAuth: req.integrationAuth,
|
||||||
const integrations = JSON.parse(
|
accessToken: req.accessToken
|
||||||
readFileSync('./src/json/integrations.json').toString()
|
|
||||||
);
|
|
||||||
|
|
||||||
let authorizationId;
|
|
||||||
switch (req.integrationAuth.integration) {
|
|
||||||
case 'heroku':
|
|
||||||
authorizationId = integrations.heroku.clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// not sure what authorizationId is?
|
|
||||||
// // revoke authorization
|
|
||||||
// const res2 = await axios.delete(
|
|
||||||
// `https://api.heroku.com/oauth/authorizations/${authorizationId}`,
|
|
||||||
// {
|
|
||||||
// headers: {
|
|
||||||
// 'Accept': 'application/vnd.heroku+json; version=3',
|
|
||||||
// 'Authorization': 'Bearer ' + req.accessToken
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
|
||||||
_id: integrationAuthId
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (deletedIntegrationAuth) {
|
|
||||||
await Integration.deleteMany({
|
|
||||||
integrationAuth: deletedIntegrationAuth._id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to delete integration authorization'
|
message: 'Failed to delete integration authorization'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully deleted integration authorization'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import axios from 'axios';
|
import { Integration, Bot, BotKey } from '../models';
|
||||||
import { Integration } from '../models';
|
import { EventService } from '../services';
|
||||||
import { decryptAsymmetric } from '../utils/crypto';
|
import { eventPushSecrets } from '../events';
|
||||||
import { decryptSecrets } from '../helpers/secret';
|
|
||||||
import { PRIVATE_KEY } from '../config';
|
|
||||||
|
|
||||||
interface Key {
|
interface Key {
|
||||||
encryptedKey: string;
|
encryptedKey: string;
|
||||||
@@ -24,104 +22,58 @@ interface PushSecret {
|
|||||||
type: 'shared' | 'personal';
|
type: 'shared' | 'personal';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return list of all available integrations on Infisical
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getIntegrations = async (req: Request, res: Response) => {
|
|
||||||
let integrations;
|
|
||||||
try {
|
|
||||||
integrations = JSON.parse(
|
|
||||||
readFileSync('./src/json/integrations.json').toString()
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get integrations'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
integrations
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync secrets [secrets] to integration with id [integrationId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const syncIntegration = async (req: Request, res: Response) => {
|
|
||||||
// TODO: unfinished - make more versatile to accomodate for other integrations
|
|
||||||
try {
|
|
||||||
const { key, secrets }: { key: Key; secrets: PushSecret[] } = req.body;
|
|
||||||
const symmetricKey = decryptAsymmetric({
|
|
||||||
ciphertext: key.encryptedKey,
|
|
||||||
nonce: key.nonce,
|
|
||||||
publicKey: req.user.publicKey,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
// decrypt secrets with symmetric key
|
|
||||||
const content = decryptSecrets({
|
|
||||||
secrets,
|
|
||||||
key: symmetricKey,
|
|
||||||
format: 'object'
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: make integration work for other integrations as well
|
|
||||||
const res = await axios.patch(
|
|
||||||
`https://api.heroku.com/apps/${req.integration.app}/config-vars`,
|
|
||||||
content,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/vnd.heroku+json; version=3',
|
|
||||||
Authorization: 'Bearer ' + req.accessToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to sync secrets with integration'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: 'Successfully synced secrets with integration'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change environment or name of integration with id [integrationId]
|
* Change environment or name of integration with id [integrationId]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const modifyIntegration = async (req: Request, res: Response) => {
|
export const updateIntegration = async (req: Request, res: Response) => {
|
||||||
let integration;
|
let integration;
|
||||||
|
|
||||||
|
// TODO: add integration-specific validation to ensure that each
|
||||||
|
// integration has the correct fields populated in [Integration]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { update } = req.body;
|
const {
|
||||||
|
app,
|
||||||
|
environment,
|
||||||
|
isActive,
|
||||||
|
target, // vercel-specific integration param
|
||||||
|
context, // netlify-specific integration param
|
||||||
|
siteId // netlify-specific integration param
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
integration = await Integration.findOneAndUpdate(
|
integration = await Integration.findOneAndUpdate(
|
||||||
{
|
{
|
||||||
_id: req.integration._id
|
_id: req.integration._id
|
||||||
},
|
},
|
||||||
update,
|
{
|
||||||
|
environment,
|
||||||
|
isActive,
|
||||||
|
app,
|
||||||
|
target,
|
||||||
|
context,
|
||||||
|
siteId
|
||||||
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (integration) {
|
||||||
|
// trigger event - push secrets
|
||||||
|
EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId: integration.workspace.toString()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to modify integration'
|
message: 'Failed to update integration'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +83,8 @@ export const modifyIntegration = async (req: Request, res: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete integration with id [integrationId]
|
* Delete integration with id [integrationId] and deactivate bot if there are
|
||||||
|
* no integrations left
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
@@ -144,6 +97,29 @@ export const deleteIntegration = async (req: Request, res: Response) => {
|
|||||||
deletedIntegration = await Integration.findOneAndDelete({
|
deletedIntegration = await Integration.findOneAndDelete({
|
||||||
_id: integrationId
|
_id: integrationId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!deletedIntegration) throw new Error('Failed to find integration');
|
||||||
|
|
||||||
|
const integrations = await Integration.find({
|
||||||
|
workspace: deletedIntegration.workspace
|
||||||
|
});
|
||||||
|
|
||||||
|
if (integrations.length === 0) {
|
||||||
|
// case: no integrations left, deactivate bot
|
||||||
|
const bot = await Bot.findOneAndUpdate({
|
||||||
|
workspace: deletedIntegration.workspace
|
||||||
|
}, {
|
||||||
|
isActive: false
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bot) {
|
||||||
|
await BotKey.deleteOne({
|
||||||
|
bot: bot._id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Request, Response } from 'express';
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Key } from '../models';
|
import { Key } from '../models';
|
||||||
import { findMembership } from '../helpers/membership';
|
import { findMembership } from '../helpers/membership';
|
||||||
import { PUBLIC_KEY } from '../config';
|
|
||||||
import { GRANTED } from '../variables';
|
import { GRANTED } from '../variables';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,16 +16,6 @@ export const uploadKey = async (req: Request, res: Response) => {
|
|||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
const { key } = req.body;
|
const { key } = req.body;
|
||||||
|
|
||||||
// validate membership of sender
|
|
||||||
const senderMembership = await findMembership({
|
|
||||||
user: req.user._id,
|
|
||||||
workspace: workspaceId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!senderMembership) {
|
|
||||||
throw new Error('Failed sender membership validation for workspace');
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate membership of receiver
|
// validate membership of receiver
|
||||||
const receiverMembership = await findMembership({
|
const receiverMembership = await findMembership({
|
||||||
user: key.userId,
|
user: key.userId,
|
||||||
@@ -94,16 +83,4 @@ export const getLatestKey = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send(resObj);
|
return res.status(200).send(resObj);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Return public key of Infisical
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getPublicKeyInfisical = async (req: Request, res: Response) => {
|
|
||||||
return res.status(200).send({
|
|
||||||
publicKey: PUBLIC_KEY
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -217,7 +217,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const { email, code } = req.body;
|
const { email, code } = req.body;
|
||||||
|
|
||||||
user = await User.findOne({ email });
|
user = await User.findOne({ email }).select('+publicKey');
|
||||||
if (user && user?.publicKey) {
|
if (user && user?.publicKey) {
|
||||||
// case: user has already completed account
|
// case: user has already completed account
|
||||||
return res.status(403).send({
|
return res.status(403).send({
|
||||||
@@ -257,7 +257,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
|||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
error: 'Failed email magic link confirmation'
|
error: 'Failed email magic link verification for organization invitation'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,121 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
|
import crypto from 'crypto';
|
||||||
const jsrp = require('jsrp');
|
const jsrp = require('jsrp');
|
||||||
import * as bigintConversion from 'bigint-conversion';
|
import * as bigintConversion from 'bigint-conversion';
|
||||||
import { User, BackupPrivateKey } from '../models';
|
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 = {};
|
const clientPublicKeys: any = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Password reset step 1: Send email verification link to email [email]
|
||||||
|
* for account recovery.
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const emailPasswordReset = async (req: Request, res: Response) => {
|
||||||
|
let email: string;
|
||||||
|
try {
|
||||||
|
email = req.body.email;
|
||||||
|
|
||||||
|
const user = await User.findOne({ email }).select('+publicKey');
|
||||||
|
if (!user || !user?.publicKey) {
|
||||||
|
// case: user has already completed account
|
||||||
|
|
||||||
|
return res.status(403).send({
|
||||||
|
error: 'Failed to send email verification for password reset'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = crypto.randomBytes(16).toString('hex');
|
||||||
|
|
||||||
|
await Token.findOneAndUpdate(
|
||||||
|
{ email },
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
token,
|
||||||
|
createdAt: new Date()
|
||||||
|
},
|
||||||
|
{ upsert: true, new: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
await sendMail({
|
||||||
|
template: 'passwordReset.handlebars',
|
||||||
|
subjectLine: 'Infisical password reset',
|
||||||
|
recipients: [email],
|
||||||
|
substitutions: {
|
||||||
|
email,
|
||||||
|
token,
|
||||||
|
callback_url: SITE_URL + '/password-reset'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to send email for account recovery'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: `Sent an email for account recovery to ${email}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Password reset step 2: Verify email verification link sent to email [email]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
||||||
|
let user, token;
|
||||||
|
try {
|
||||||
|
const { email, code } = req.body;
|
||||||
|
|
||||||
|
user = await User.findOne({ email }).select('+publicKey');
|
||||||
|
if (!user || !user?.publicKey) {
|
||||||
|
// case: user doesn't exist with email [email] or
|
||||||
|
// hasn't even completed their account
|
||||||
|
return res.status(403).send({
|
||||||
|
error: 'Failed email verification for password reset'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkEmailVerification({
|
||||||
|
email,
|
||||||
|
code
|
||||||
|
});
|
||||||
|
|
||||||
|
// generate temporary password-reset token
|
||||||
|
token = createToken({
|
||||||
|
payload: {
|
||||||
|
userId: user._id.toString()
|
||||||
|
},
|
||||||
|
expiresIn: JWT_SIGNUP_LIFETIME,
|
||||||
|
secret: JWT_SIGNUP_SECRET
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed email verification for password reset'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully verified email',
|
||||||
|
user,
|
||||||
|
token
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
* Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
||||||
* @param req
|
* @param req
|
||||||
@@ -43,7 +153,7 @@ export const srp1 = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
error: 'Failed to start change password process'
|
error: 'Failed to start change password process'
|
||||||
@@ -110,7 +220,7 @@ export const changePassword = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
error: 'Failed to change password. Try again?'
|
error: 'Failed to change password. Try again?'
|
||||||
@@ -180,10 +290,73 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to update backup private key'
|
message: 'Failed to update backup private key'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return backup private key for user
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getBackupPrivateKey = async (req: Request, res: Response) => {
|
||||||
|
let backupPrivateKey;
|
||||||
|
try {
|
||||||
|
backupPrivateKey = await BackupPrivateKey.findOne({
|
||||||
|
user: req.user._id
|
||||||
|
}).select('+encryptedPrivateKey +iv +tag');
|
||||||
|
|
||||||
|
if (!backupPrivateKey) throw new Error('Failed to find backup private key');
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email});
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get backup private key'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
backupPrivateKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resetPassword = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
salt,
|
||||||
|
verifier,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
await User.findByIdAndUpdate(
|
||||||
|
req.user._id.toString(),
|
||||||
|
{
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email});
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get backup private key'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully reset password'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -7,8 +7,9 @@ import {
|
|||||||
reformatPullSecrets
|
reformatPullSecrets
|
||||||
} from '../helpers/secret';
|
} from '../helpers/secret';
|
||||||
import { pushKeys } from '../helpers/key';
|
import { pushKeys } from '../helpers/key';
|
||||||
|
import { eventPushSecrets } from '../events';
|
||||||
|
import { EventService } from '../services';
|
||||||
import { ENV_SET } from '../variables';
|
import { ENV_SET } from '../variables';
|
||||||
|
|
||||||
import { postHogClient } from '../services';
|
import { postHogClient } from '../services';
|
||||||
|
|
||||||
interface PushSecret {
|
interface PushSecret {
|
||||||
@@ -60,7 +61,8 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
keys
|
keys
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: 'secrets pushed',
|
event: 'secrets pushed',
|
||||||
@@ -74,6 +76,13 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trigger event - push secrets
|
||||||
|
EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
@@ -192,7 +201,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
// capture secrets pushed event in production
|
// capture secrets pulled event in production
|
||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
distinctId: req.serviceToken.user.email,
|
distinctId: req.serviceToken.user.email,
|
||||||
event: 'secrets pulled',
|
event: 'secrets pulled',
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ export const createServiceToken = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
token = createToken({
|
token = createToken({
|
||||||
payload: {
|
payload: {
|
||||||
serviceTokenId: serviceToken._id.toString()
|
serviceTokenId: serviceToken._id.toString(),
|
||||||
|
workspaceId
|
||||||
},
|
},
|
||||||
expiresIn: expiresIn,
|
expiresIn: expiresIn,
|
||||||
secret: JWT_SERVICE_SECRET
|
secret: JWT_SERVICE_SECRET
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { eventPushSecrets } from "./secret"
|
||||||
|
|
||||||
|
export {
|
||||||
|
eventPushSecrets
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { EVENT_PUSH_SECRETS } from '../variables';
|
||||||
|
|
||||||
|
interface PushSecret {
|
||||||
|
ciphertextKey: string;
|
||||||
|
ivKey: string;
|
||||||
|
tagKey: string;
|
||||||
|
hashKey: string;
|
||||||
|
ciphertextValue: string;
|
||||||
|
ivValue: string;
|
||||||
|
tagValue: string;
|
||||||
|
hashValue: string;
|
||||||
|
type: 'shared' | 'personal';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return event for pushing secrets
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace to push secrets to
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const eventPushSecrets = ({
|
||||||
|
workspaceId,
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
}) => {
|
||||||
|
return ({
|
||||||
|
name: EVENT_PUSH_SECRETS,
|
||||||
|
workspaceId,
|
||||||
|
payload: {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
eventPushSecrets
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Bot,
|
||||||
|
BotKey,
|
||||||
|
Secret,
|
||||||
|
ISecret,
|
||||||
|
IUser
|
||||||
|
} from '../models';
|
||||||
|
import {
|
||||||
|
generateKeyPair,
|
||||||
|
encryptSymmetric,
|
||||||
|
decryptSymmetric,
|
||||||
|
decryptAsymmetric
|
||||||
|
} from '../utils/crypto';
|
||||||
|
import { decryptSecrets } from '../helpers/secret';
|
||||||
|
import { ENCRYPTION_KEY } from '../config';
|
||||||
|
import { SECRET_SHARED } from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an inactive bot with name [name] for workspace with id [workspaceId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.name - name of bot
|
||||||
|
* @param {String} obj.workspaceId - id of workspace that bot belongs to
|
||||||
|
*/
|
||||||
|
const createBot = async ({
|
||||||
|
name,
|
||||||
|
workspaceId,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
workspaceId: string;
|
||||||
|
}) => {
|
||||||
|
let bot;
|
||||||
|
try {
|
||||||
|
const { publicKey, privateKey } = generateKeyPair();
|
||||||
|
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||||
|
plaintext: privateKey,
|
||||||
|
key: ENCRYPTION_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
bot = await new Bot({
|
||||||
|
name,
|
||||||
|
workspace: workspaceId,
|
||||||
|
isActive: false,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey: ciphertext,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
}).save();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to create bot');
|
||||||
|
}
|
||||||
|
|
||||||
|
return bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return decrypted secrets for workspace with id [workspaceId]
|
||||||
|
* and [environment] using bot
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @param {String} obj.environment - environment
|
||||||
|
*/
|
||||||
|
const getSecretsHelper = async ({
|
||||||
|
workspaceId,
|
||||||
|
environment
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
}) => {
|
||||||
|
const content = {} as any;
|
||||||
|
try {
|
||||||
|
const key = await getKey({ workspaceId });
|
||||||
|
const secrets = await Secret.find({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type: SECRET_SHARED
|
||||||
|
});
|
||||||
|
|
||||||
|
secrets.forEach((secret: ISecret) => {
|
||||||
|
const secretKey = decryptSymmetric({
|
||||||
|
ciphertext: secret.secretKeyCiphertext,
|
||||||
|
iv: secret.secretKeyIV,
|
||||||
|
tag: secret.secretKeyTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretValue = decryptSymmetric({
|
||||||
|
ciphertext: secret.secretValueCiphertext,
|
||||||
|
iv: secret.secretValueIV,
|
||||||
|
tag: secret.secretValueTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
content[secretKey] = secretValue;
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get secrets');
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return bot's copy of the workspace key for workspace
|
||||||
|
* with id [workspaceId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @returns {String} key - decrypted workspace key
|
||||||
|
*/
|
||||||
|
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
|
||||||
|
let key;
|
||||||
|
try {
|
||||||
|
const botKey = await BotKey.findOne({
|
||||||
|
workspace: workspaceId
|
||||||
|
}).populate<{ sender: IUser }>('sender', 'publicKey');
|
||||||
|
|
||||||
|
if (!botKey) throw new Error('Failed to find bot key');
|
||||||
|
|
||||||
|
const bot = await Bot.findOne({
|
||||||
|
workspace: workspaceId
|
||||||
|
}).select('+encryptedPrivateKey +iv +tag');
|
||||||
|
|
||||||
|
if (!bot) throw new Error('Failed to find bot');
|
||||||
|
if (!bot.isActive) throw new Error('Bot is not active');
|
||||||
|
|
||||||
|
const privateKeyBot = decryptSymmetric({
|
||||||
|
ciphertext: bot.encryptedPrivateKey,
|
||||||
|
iv: bot.iv,
|
||||||
|
tag: bot.tag,
|
||||||
|
key: ENCRYPTION_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
key = decryptAsymmetric({
|
||||||
|
ciphertext: botKey.encryptedKey,
|
||||||
|
nonce: botKey.nonce,
|
||||||
|
publicKey: botKey.sender.publicKey as string,
|
||||||
|
privateKey: privateKeyBot
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get workspace key');
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return symmetrically encrypted [plaintext] using the
|
||||||
|
* key for workspace with id [workspaceId]
|
||||||
|
* @param {Object} obj1
|
||||||
|
* @param {String} obj1.workspaceId - id of workspace
|
||||||
|
* @param {String} obj1.plaintext - plaintext to encrypt
|
||||||
|
*/
|
||||||
|
const encryptSymmetricHelper = async ({
|
||||||
|
workspaceId,
|
||||||
|
plaintext
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
plaintext: string;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const key = await getKey({ workspaceId });
|
||||||
|
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||||
|
plaintext,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
return ({
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to perform symmetric encryption with bot');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Return symmetrically decrypted [ciphertext] using the
|
||||||
|
* key for workspace with id [workspaceId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||||
|
* @param {String} obj.iv - iv
|
||||||
|
* @param {String} obj.tag - tag
|
||||||
|
*/
|
||||||
|
const decryptSymmetricHelper = async ({
|
||||||
|
workspaceId,
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
ciphertext: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
}) => {
|
||||||
|
let plaintext;
|
||||||
|
try {
|
||||||
|
const key = await getKey({ workspaceId });
|
||||||
|
const plaintext = decryptSymmetric({
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
return plaintext;
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to perform symmetric decryption with bot');
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
createBot,
|
||||||
|
getSecretsHelper,
|
||||||
|
encryptSymmetricHelper,
|
||||||
|
decryptSymmetricHelper
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Bot, IBot } from '../models';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { EVENT_PUSH_SECRETS } from '../variables';
|
||||||
|
import { IntegrationService } from '../services';
|
||||||
|
|
||||||
|
interface Event {
|
||||||
|
name: string;
|
||||||
|
workspaceId: string;
|
||||||
|
payload: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle event [event]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Event} obj.event - an event
|
||||||
|
* @param {String} obj.event.name - name of event
|
||||||
|
* @param {String} obj.event.workspaceId - id of workspace that event is part of
|
||||||
|
* @param {Object} obj.event.payload - payload of event (depends on event)
|
||||||
|
*/
|
||||||
|
const handleEventHelper = async ({
|
||||||
|
event
|
||||||
|
}: {
|
||||||
|
event: Event;
|
||||||
|
}) => {
|
||||||
|
const { workspaceId } = event;
|
||||||
|
|
||||||
|
// TODO: moduralize bot check into separate function
|
||||||
|
const bot = await Bot.findOne({
|
||||||
|
workspace: workspaceId,
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bot) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (event.name) {
|
||||||
|
case EVENT_PUSH_SECRETS:
|
||||||
|
IntegrationService.syncIntegrations({
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
handleEventHelper
|
||||||
|
}
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Bot,
|
||||||
|
Integration,
|
||||||
|
IntegrationAuth,
|
||||||
|
} from '../models';
|
||||||
|
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
|
||||||
|
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;
|
||||||
|
integration: string;
|
||||||
|
teamId?: string;
|
||||||
|
accountId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
|
||||||
|
* named [integration]
|
||||||
|
* - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
|
||||||
|
* - Add placeholder inactive integration
|
||||||
|
* - Create bot sequence for integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @param {String} obj.integration - name of integration
|
||||||
|
* @param {String} obj.code - code
|
||||||
|
*/
|
||||||
|
const handleOAuthExchangeHelper = async ({
|
||||||
|
workspaceId,
|
||||||
|
integration,
|
||||||
|
code
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
integration: string;
|
||||||
|
code: string;
|
||||||
|
}) => {
|
||||||
|
let action;
|
||||||
|
let integrationAuth;
|
||||||
|
try {
|
||||||
|
const bot = await Bot.findOne({
|
||||||
|
workspace: workspaceId,
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
|
||||||
|
|
||||||
|
// exchange code for access and refresh tokens
|
||||||
|
const res = await exchangeCode({
|
||||||
|
integration,
|
||||||
|
code
|
||||||
|
});
|
||||||
|
|
||||||
|
const update: Update = {
|
||||||
|
workspace: workspaceId,
|
||||||
|
integration
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (integration) {
|
||||||
|
case INTEGRATION_VERCEL:
|
||||||
|
update.teamId = res.teamId;
|
||||||
|
break;
|
||||||
|
case INTEGRATION_NETLIFY:
|
||||||
|
update.accountId = res.accountId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
|
workspace: workspaceId,
|
||||||
|
integration
|
||||||
|
}, update, {
|
||||||
|
new: true,
|
||||||
|
upsert: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.refreshToken) {
|
||||||
|
// case: refresh token returned from exchange
|
||||||
|
// set integration auth refresh token
|
||||||
|
await setIntegrationAuthRefreshHelper({
|
||||||
|
integrationAuthId: integrationAuth._id.toString(),
|
||||||
|
refreshToken: res.refreshToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.accessToken) {
|
||||||
|
// case: access token returned from exchange
|
||||||
|
// set integration auth access token
|
||||||
|
await setIntegrationAuthAccessHelper({
|
||||||
|
integrationAuthId: integrationAuth._id.toString(),
|
||||||
|
accessToken: res.accessToken,
|
||||||
|
accessExpiresAt: res.accessExpiresAt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize new integration after exchange
|
||||||
|
await new Integration({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: ENV_DEV,
|
||||||
|
isActive: false,
|
||||||
|
app: null,
|
||||||
|
integration,
|
||||||
|
integrationAuth: integrationAuth._id
|
||||||
|
}).save();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to handle OAuth2 code-token exchange')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sync/push environment variables in workspace with id [workspaceId] to
|
||||||
|
* all active integrations for that workspace
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.workspaceId - id of workspace
|
||||||
|
*/
|
||||||
|
const syncIntegrationsHelper = async ({
|
||||||
|
workspaceId
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
}) => {
|
||||||
|
let integrations;
|
||||||
|
try {
|
||||||
|
|
||||||
|
integrations = await Integration.find({
|
||||||
|
workspace: workspaceId,
|
||||||
|
isActive: true,
|
||||||
|
app: { $ne: null }
|
||||||
|
});
|
||||||
|
|
||||||
|
// for each workspace integration, sync/push secrets
|
||||||
|
// to that integration
|
||||||
|
for await (const integration of integrations) {
|
||||||
|
// get workspace, environment (shared) secrets
|
||||||
|
const secrets = await BotService.getSecrets({ // issue here?
|
||||||
|
workspaceId: integration.workspace.toString(),
|
||||||
|
environment: integration.environment
|
||||||
|
});
|
||||||
|
|
||||||
|
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
|
||||||
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
|
// get integration auth access token
|
||||||
|
const accessToken = await getIntegrationAuthAccessHelper({
|
||||||
|
integrationAuthId: integration.integrationAuth.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// sync secrets to integration
|
||||||
|
await syncSecrets({
|
||||||
|
integration,
|
||||||
|
integrationAuth,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to sync secrets to integrations');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return decrypted refresh token using the bot's copy
|
||||||
|
* of the workspace key for workspace belonging to integration auth
|
||||||
|
* with id [integrationAuthId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @param {String} refreshToken - decrypted refresh token
|
||||||
|
*/
|
||||||
|
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
|
||||||
|
let refreshToken;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const integrationAuth = await IntegrationAuth
|
||||||
|
.findById(integrationAuthId)
|
||||||
|
.select('+refreshCiphertext +refreshIV +refreshTag');
|
||||||
|
|
||||||
|
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
|
||||||
|
|
||||||
|
refreshToken = await BotService.decryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
ciphertext: integrationAuth.refreshCiphertext as string,
|
||||||
|
iv: integrationAuth.refreshIV as string,
|
||||||
|
tag: integrationAuth.refreshTag as string
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
if(err instanceof RequestError)
|
||||||
|
throw err
|
||||||
|
else
|
||||||
|
throw new Error('Failed to get integration refresh token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return decrypted access token using the bot's copy
|
||||||
|
* of the workspace key for workspace belonging to integration auth
|
||||||
|
* with id [integrationAuthId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @returns {String} accessToken - decrypted access token
|
||||||
|
*/
|
||||||
|
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 UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
|
||||||
|
|
||||||
|
accessToken = await BotService.decryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
ciphertext: integrationAuth.accessCiphertext as string,
|
||||||
|
iv: integrationAuth.accessIV as string,
|
||||||
|
tag: integrationAuth.accessTag as string
|
||||||
|
});
|
||||||
|
|
||||||
|
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
|
||||||
|
// there is a access token expiration date
|
||||||
|
// and refresh token to exchange with the OAuth2 server
|
||||||
|
|
||||||
|
if (integrationAuth.accessExpiresAt < new Date()) {
|
||||||
|
// access token is expired
|
||||||
|
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
|
||||||
|
accessToken = await exchangeRefresh({
|
||||||
|
integration: integrationAuth.integration,
|
||||||
|
refreshToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
if(err instanceof RequestError)
|
||||||
|
throw err
|
||||||
|
else
|
||||||
|
throw new Error('Failed to get integration access token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt refresh token [refreshToken] using the bot's copy
|
||||||
|
* of the workspace key for workspace belonging to integration auth
|
||||||
|
* with id [integrationAuthId] and store it
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @param {String} obj.refreshToken - refresh token
|
||||||
|
*/
|
||||||
|
const setIntegrationAuthRefreshHelper = async ({
|
||||||
|
integrationAuthId,
|
||||||
|
refreshToken
|
||||||
|
}: {
|
||||||
|
integrationAuthId: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
let integrationAuth;
|
||||||
|
try {
|
||||||
|
integrationAuth = await IntegrationAuth
|
||||||
|
.findById(integrationAuthId);
|
||||||
|
|
||||||
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
|
const obj = await BotService.encryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
plaintext: refreshToken
|
||||||
|
});
|
||||||
|
|
||||||
|
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
|
_id: integrationAuthId
|
||||||
|
}, {
|
||||||
|
refreshCiphertext: obj.ciphertext,
|
||||||
|
refreshIV: obj.iv,
|
||||||
|
refreshTag: obj.tag
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to set integration auth refresh token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return integrationAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt access token [accessToken] using the bot's copy
|
||||||
|
* of the workspace key for workspace belonging to integration auth
|
||||||
|
* with id [integrationAuthId] and store it along with [accessExpiresAt]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @param {String} obj.accessToken - access token
|
||||||
|
* @param {Date} obj.accessExpiresAt - expiration date of access token
|
||||||
|
*/
|
||||||
|
const setIntegrationAuthAccessHelper = async ({
|
||||||
|
integrationAuthId,
|
||||||
|
accessToken,
|
||||||
|
accessExpiresAt
|
||||||
|
}: {
|
||||||
|
integrationAuthId: string;
|
||||||
|
accessToken: string;
|
||||||
|
accessExpiresAt: Date;
|
||||||
|
}) => {
|
||||||
|
let integrationAuth;
|
||||||
|
try {
|
||||||
|
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
||||||
|
|
||||||
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
|
const obj = await BotService.encryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
plaintext: accessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
|
_id: integrationAuthId
|
||||||
|
}, {
|
||||||
|
accessCiphertext: obj.ciphertext,
|
||||||
|
accessIV: obj.iv,
|
||||||
|
accessTag: obj.tag,
|
||||||
|
accessExpiresAt
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to save integration auth access token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return integrationAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
handleOAuthExchangeHelper,
|
||||||
|
syncIntegrationsHelper,
|
||||||
|
getIntegrationAuthRefreshHelper,
|
||||||
|
getIntegrationAuthAccessHelper,
|
||||||
|
setIntegrationAuthRefreshHelper,
|
||||||
|
setIntegrationAuthAccessHelper
|
||||||
|
}
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { IntegrationAuth } from '../models';
|
|
||||||
import { encryptSymmetric, decryptSymmetric } from '../utils/crypto';
|
|
||||||
import { IIntegrationAuth } from '../models';
|
|
||||||
import {
|
|
||||||
ENCRYPTION_KEY,
|
|
||||||
OAUTH_CLIENT_SECRET_HEROKU,
|
|
||||||
OAUTH_TOKEN_URL_HEROKU
|
|
||||||
} from '../config';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process token exchange and refresh responses from respective OAuth2 authorization servers by
|
|
||||||
* encrypting access and refresh tokens, computing new access token expiration times [accessExpiresAt],
|
|
||||||
* and upserting them into the DB for workspace with id [workspaceId] and integration [integration].
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {String} obj.workspaceId - id of workspace
|
|
||||||
* @param {String} obj.integration - name of integration (e.g. heroku)
|
|
||||||
* @param {Object} obj.res - response from OAuth2 authorization server
|
|
||||||
*/
|
|
||||||
const processOAuthTokenRes = async ({
|
|
||||||
workspaceId,
|
|
||||||
integration,
|
|
||||||
res
|
|
||||||
}: {
|
|
||||||
workspaceId: string;
|
|
||||||
integration: string;
|
|
||||||
res: any;
|
|
||||||
}): Promise<IIntegrationAuth> => {
|
|
||||||
let integrationAuth;
|
|
||||||
try {
|
|
||||||
// encrypt refresh + access tokens
|
|
||||||
const {
|
|
||||||
ciphertext: refreshCiphertext,
|
|
||||||
iv: refreshIV,
|
|
||||||
tag: refreshTag
|
|
||||||
} = encryptSymmetric({
|
|
||||||
plaintext: res.data.refresh_token,
|
|
||||||
key: ENCRYPTION_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
ciphertext: accessCiphertext,
|
|
||||||
iv: accessIV,
|
|
||||||
tag: accessTag
|
|
||||||
} = encryptSymmetric({
|
|
||||||
plaintext: res.data.access_token,
|
|
||||||
key: ENCRYPTION_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
// compute access token expiration date
|
|
||||||
const accessExpiresAt = new Date();
|
|
||||||
accessExpiresAt.setSeconds(
|
|
||||||
accessExpiresAt.getSeconds() + res.data.expires_in
|
|
||||||
);
|
|
||||||
|
|
||||||
// create or replace integration authorization with encrypted tokens
|
|
||||||
// and access token expiration date
|
|
||||||
integrationAuth = await IntegrationAuth.findOneAndUpdate(
|
|
||||||
{ workspace: workspaceId, integration },
|
|
||||||
{
|
|
||||||
workspace: workspaceId,
|
|
||||||
integration,
|
|
||||||
refreshCiphertext,
|
|
||||||
refreshIV,
|
|
||||||
refreshTag,
|
|
||||||
accessCiphertext,
|
|
||||||
accessIV,
|
|
||||||
accessTag,
|
|
||||||
accessExpiresAt
|
|
||||||
},
|
|
||||||
{ upsert: true, new: true }
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error(
|
|
||||||
'Failed to process OAuth2 authorization server token response'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return integrationAuth;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return access token for integration either by decrypting a non-expired access token [accessCiphertext] on
|
|
||||||
* the integration authorization document or by requesting a new one by decrypting and exchanging the
|
|
||||||
* refresh token [refreshCiphertext] with the respective OAuth2 authorization server.
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {IIntegrationAuth} obj.integrationAuth - an integration authorization document
|
|
||||||
* @returns {String} access token - new access token
|
|
||||||
*/
|
|
||||||
const getOAuthAccessToken = async ({
|
|
||||||
integrationAuth
|
|
||||||
}: {
|
|
||||||
integrationAuth: IIntegrationAuth;
|
|
||||||
}) => {
|
|
||||||
let accessToken;
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
refreshCiphertext,
|
|
||||||
refreshIV,
|
|
||||||
refreshTag,
|
|
||||||
accessCiphertext,
|
|
||||||
accessIV,
|
|
||||||
accessTag,
|
|
||||||
accessExpiresAt
|
|
||||||
} = integrationAuth;
|
|
||||||
|
|
||||||
if (
|
|
||||||
refreshCiphertext &&
|
|
||||||
refreshIV &&
|
|
||||||
refreshTag &&
|
|
||||||
accessCiphertext &&
|
|
||||||
accessIV &&
|
|
||||||
accessTag &&
|
|
||||||
accessExpiresAt
|
|
||||||
) {
|
|
||||||
if (accessExpiresAt < new Date()) {
|
|
||||||
// case: access token expired
|
|
||||||
// TODO: fetch another access token
|
|
||||||
|
|
||||||
let clientSecret;
|
|
||||||
switch (integrationAuth.integration) {
|
|
||||||
case 'heroku':
|
|
||||||
clientSecret = OAUTH_CLIENT_SECRET_HEROKU;
|
|
||||||
}
|
|
||||||
|
|
||||||
// record new access token and refresh token
|
|
||||||
// encrypt refresh + access tokens
|
|
||||||
const refreshToken = decryptSymmetric({
|
|
||||||
ciphertext: refreshCiphertext,
|
|
||||||
iv: refreshIV,
|
|
||||||
tag: refreshTag,
|
|
||||||
key: ENCRYPTION_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: make route compatible with other integration types
|
|
||||||
const res = await axios.post(
|
|
||||||
OAUTH_TOKEN_URL_HEROKU, // maybe shouldn't be a config variable?
|
|
||||||
new URLSearchParams({
|
|
||||||
grant_type: 'refresh_token',
|
|
||||||
refresh_token: refreshToken,
|
|
||||||
client_secret: clientSecret
|
|
||||||
} as any)
|
|
||||||
);
|
|
||||||
|
|
||||||
accessToken = res.data.access_token;
|
|
||||||
|
|
||||||
await processOAuthTokenRes({
|
|
||||||
workspaceId: integrationAuth.workspace.toString(),
|
|
||||||
integration: integrationAuth.integration,
|
|
||||||
res
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// case: access token still works
|
|
||||||
accessToken = decryptSymmetric({
|
|
||||||
ciphertext: accessCiphertext,
|
|
||||||
iv: accessIV,
|
|
||||||
tag: accessTag,
|
|
||||||
key: ENCRYPTION_KEY
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to get OAuth2 access token');
|
|
||||||
}
|
|
||||||
|
|
||||||
return accessToken;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { processOAuthTokenRes, getOAuthAccessToken };
|
|
||||||
|
|||||||
@@ -1,6 +1,52 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Membership, Key } from '../models';
|
import { Membership, Key } from '../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
|
||||||
|
* and has at least one of the roles in [acceptedRoles] and statuses in [acceptedStatuses]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.userId - id of user to validate
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
*/
|
||||||
|
const validateMembership = async ({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
acceptedRoles: string[];
|
||||||
|
acceptedStatuses: string[];
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership) throw new Error('Failed to find membership');
|
||||||
|
|
||||||
|
if (!acceptedRoles.includes(membership.role)) {
|
||||||
|
throw new Error('Failed to validate membership role');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!acceptedStatuses.includes(membership.status)) {
|
||||||
|
throw new Error('Failed to validate membership status');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to validate membership');
|
||||||
|
}
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return membership matching criteria specified in query [queryObj]
|
* Return membership matching criteria specified in query [queryObj]
|
||||||
* @param {Object} queryObj - query object
|
* @param {Object} queryObj - query object
|
||||||
@@ -97,4 +143,9 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
|||||||
return deletedMembership;
|
return deletedMembership;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { addMemberships, findMembership, deleteMembership };
|
export {
|
||||||
|
validateMembership,
|
||||||
|
addMemberships,
|
||||||
|
findMembership,
|
||||||
|
deleteMembership
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,21 +2,10 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import handlebars from 'handlebars';
|
import handlebars from 'handlebars';
|
||||||
import nodemailer from 'nodemailer';
|
import nodemailer from 'nodemailer';
|
||||||
import { SMTP_HOST, SMTP_NAME, SMTP_USERNAME, SMTP_PASSWORD } from '../config';
|
import { SMTP_FROM_NAME, SMTP_FROM_ADDRESS } from '../config';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
|
||||||
// create nodemailer transporter
|
let smtpTransporter: nodemailer.Transporter;
|
||||||
const transporter = nodemailer.createTransport({
|
|
||||||
host: SMTP_HOST,
|
|
||||||
port: 587,
|
|
||||||
auth: {
|
|
||||||
user: SMTP_USERNAME,
|
|
||||||
pass: SMTP_PASSWORD
|
|
||||||
}
|
|
||||||
});
|
|
||||||
transporter
|
|
||||||
.verify()
|
|
||||||
.then(() => console.log('SMTP - Successfully connected'))
|
|
||||||
.catch((err) => console.log('SMTP - Failed to connect'));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
@@ -26,33 +15,38 @@ transporter
|
|||||||
* @param {Object} obj.substitutions - object containing template substitutions
|
* @param {Object} obj.substitutions - object containing template substitutions
|
||||||
*/
|
*/
|
||||||
const sendMail = async ({
|
const sendMail = async ({
|
||||||
template,
|
template,
|
||||||
subjectLine,
|
subjectLine,
|
||||||
recipients,
|
recipients,
|
||||||
substitutions
|
substitutions
|
||||||
}: {
|
}: {
|
||||||
template: string;
|
template: string;
|
||||||
subjectLine: string;
|
subjectLine: string;
|
||||||
recipients: string[];
|
recipients: string[];
|
||||||
substitutions: any;
|
substitutions: any;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const html = fs.readFileSync(
|
const html = fs.readFileSync(
|
||||||
path.resolve(__dirname, '../templates/' + template),
|
path.resolve(__dirname, '../templates/' + template),
|
||||||
'utf8'
|
'utf8'
|
||||||
);
|
);
|
||||||
const temp = handlebars.compile(html);
|
const temp = handlebars.compile(html);
|
||||||
const htmlToSend = temp(substitutions);
|
const htmlToSend = temp(substitutions);
|
||||||
|
|
||||||
await transporter.sendMail({
|
await smtpTransporter.sendMail({
|
||||||
from: `"${SMTP_NAME}" <${SMTP_USERNAME}>`,
|
from: `"${SMTP_FROM_NAME}" <${SMTP_FROM_ADDRESS}>`,
|
||||||
to: recipients.join(', '),
|
to: recipients.join(', '),
|
||||||
subject: subjectLine,
|
subject: subjectLine,
|
||||||
html: htmlToSend
|
html: htmlToSend
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
Sentry.setUser(null);
|
||||||
}
|
Sentry.captureException(err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { sendMail };
|
const setTransporter = (transporter: nodemailer.Transporter) => {
|
||||||
|
smtpTransporter = transporter;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { sendMail, setTransporter };
|
||||||
|
|||||||
@@ -2,34 +2,35 @@ import rateLimit from 'express-rate-limit';
|
|||||||
|
|
||||||
// 300 requests per 15 minutes
|
// 300 requests per 15 minutes
|
||||||
const apiLimiter = rateLimit({
|
const apiLimiter = rateLimit({
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 400,
|
max: 400,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false
|
legacyHeaders: false,
|
||||||
|
skip: (request) => request.path === '/healthcheck'
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5 requests per hour
|
// 5 requests per hour
|
||||||
const signupLimiter = rateLimit({
|
const signupLimiter = rateLimit({
|
||||||
windowMs: 60 * 60 * 1000,
|
windowMs: 60 * 60 * 1000,
|
||||||
max: 10,
|
max: 10,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false
|
legacyHeaders: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// 10 requests per hour
|
// 10 requests per hour
|
||||||
const loginLimiter = rateLimit({
|
const loginLimiter = rateLimit({
|
||||||
windowMs: 60 * 60 * 1000,
|
windowMs: 60 * 60 * 1000,
|
||||||
max: 20,
|
max: 20,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false
|
legacyHeaders: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5 requests per hour
|
// 5 requests per hour
|
||||||
const passwordLimiter = rateLimit({
|
const passwordLimiter = rateLimit({
|
||||||
windowMs: 60 * 60 * 1000,
|
windowMs: 60 * 60 * 1000,
|
||||||
max: 10,
|
max: 10,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false
|
legacyHeaders: false
|
||||||
});
|
});
|
||||||
|
|
||||||
export { apiLimiter, signupLimiter, loginLimiter, passwordLimiter };
|
export { apiLimiter, signupLimiter, loginLimiter, passwordLimiter };
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const sendEmailVerification = async ({ email }: { email: string }) => {
|
|||||||
// send mail
|
// send mail
|
||||||
await sendMail({
|
await sendMail({
|
||||||
template: 'emailVerification.handlebars',
|
template: 'emailVerification.handlebars',
|
||||||
subjectLine: 'Infisical workspace invitation',
|
subjectLine: 'Infisical confirmation code',
|
||||||
recipients: [email],
|
recipients: [email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
code: token
|
code: token
|
||||||
@@ -66,7 +66,7 @@ const checkEmailVerification = async ({
|
|||||||
email,
|
email,
|
||||||
token: code
|
token: code
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!token) throw new Error('Failed to find email verification token');
|
if (!token) throw new Error('Failed to find email verification token');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
|
Bot,
|
||||||
Membership,
|
Membership,
|
||||||
Key,
|
Key,
|
||||||
Secret
|
Secret
|
||||||
} from '../models';
|
} from '../models';
|
||||||
|
import { createBot } from '../helpers/bot';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a workspace with name [name] in organization with id [organizationId]
|
* Create a workspace with name [name] in organization with id [organizationId]
|
||||||
|
* and a bot for it.
|
||||||
* @param {String} name - name of workspace to create.
|
* @param {String} name - name of workspace to create.
|
||||||
* @param {String} organizationId - id of organization to create workspace in
|
* @param {String} organizationId - id of organization to create workspace in
|
||||||
* @param {Object} workspace - new workspace
|
* @param {Object} workspace - new workspace
|
||||||
@@ -21,10 +24,16 @@ const createWorkspace = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
let workspace;
|
let workspace;
|
||||||
try {
|
try {
|
||||||
|
// create workspace
|
||||||
workspace = await new Workspace({
|
workspace = await new Workspace({
|
||||||
name,
|
name,
|
||||||
organization: organizationId
|
organization: organizationId
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
|
const bot = await createBot({
|
||||||
|
name: 'Infisical Bot',
|
||||||
|
workspaceId: workspace._id.toString()
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
@@ -43,6 +52,9 @@ const createWorkspace = async ({
|
|||||||
const deleteWorkspace = async ({ id }: { id: string }) => {
|
const deleteWorkspace = async ({ id }: { id: string }) => {
|
||||||
try {
|
try {
|
||||||
await Workspace.deleteOne({ _id: id });
|
await Workspace.deleteOne({ _id: id });
|
||||||
|
await Bot.deleteOne({
|
||||||
|
workspace: id
|
||||||
|
});
|
||||||
await Membership.deleteMany({
|
await Membership.deleteMany({
|
||||||
workspace: id
|
workspace: id
|
||||||
});
|
});
|
||||||
|
|||||||
+17
-83
@@ -1,91 +1,25 @@
|
|||||||
import express from 'express';
|
|
||||||
import helmet from 'helmet';
|
|
||||||
import cors from 'cors';
|
|
||||||
import cookieParser from 'cookie-parser';
|
|
||||||
import mongoose from 'mongoose';
|
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { PORT, SENTRY_DSN, NODE_ENV, MONGO_URL, SITE_URL, POSTHOG_PROJECT_API_KEY, POSTHOG_HOST, TELEMETRY_ENABLED } from './config';
|
import { SENTRY_DSN, NODE_ENV, MONGO_URL } from './config';
|
||||||
import { apiLimiter } from './helpers/rateLimiter';
|
import { server } from './app';
|
||||||
|
import { initDatabase } from './services/database';
|
||||||
|
import { setUpHealthEndpoint } from './services/health';
|
||||||
|
import { initSmtp } from './services/smtp';
|
||||||
|
import { setTransporter } from './helpers/nodemailer';
|
||||||
|
|
||||||
const app = express();
|
initDatabase(MONGO_URL);
|
||||||
|
|
||||||
Sentry.init({
|
setUpHealthEndpoint(server);
|
||||||
dsn: SENTRY_DSN,
|
|
||||||
tracesSampleRate: 1.0,
|
|
||||||
debug: NODE_ENV === 'production' ? false : true,
|
|
||||||
environment: NODE_ENV
|
|
||||||
});
|
|
||||||
|
|
||||||
import {
|
setTransporter(initSmtp());
|
||||||
signup as signupRouter,
|
|
||||||
auth as authRouter,
|
|
||||||
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';
|
|
||||||
|
|
||||||
const connectWithRetry = () => {
|
if (NODE_ENV !== 'test') {
|
||||||
mongoose.connect(MONGO_URL)
|
Sentry.init({
|
||||||
.then(() => console.log('Successfully connected to DB'))
|
dsn: SENTRY_DSN,
|
||||||
.catch((e) => {
|
tracesSampleRate: 1.0,
|
||||||
console.log('Failed to connect to DB ', e);
|
debug: NODE_ENV === 'production' ? false : true,
|
||||||
setTimeout(() => {
|
environment: NODE_ENV
|
||||||
console.log(e);
|
});
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connectWithRetry();
|
|
||||||
|
|
||||||
app.enable('trust proxy');
|
|
||||||
app.use(cookieParser());
|
|
||||||
app.use(cors({
|
|
||||||
credentials: true,
|
|
||||||
origin: SITE_URL
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (NODE_ENV === 'production') {
|
|
||||||
// enable app-wide rate-limiting + helmet security
|
|
||||||
// in production
|
|
||||||
app.disable('x-powered-by');
|
|
||||||
app.use(apiLimiter);
|
|
||||||
app.use(helmet());
|
|
||||||
}
|
|
||||||
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
// routers
|
|
||||||
app.use('/api/v1/signup', signupRouter);
|
|
||||||
app.use('/api/v1/auth', authRouter);
|
|
||||||
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);
|
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log('Listening on PORT ' + PORT);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Octokit } from '@octokit/rest';
|
||||||
|
import { IIntegrationAuth } from '../models';
|
||||||
|
import {
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY,
|
||||||
|
INTEGRATION_GITHUB,
|
||||||
|
INTEGRATION_HEROKU_API_URL,
|
||||||
|
INTEGRATION_VERCEL_API_URL,
|
||||||
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
|
INTEGRATION_GITHUB_API_URL
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
interface GitHubApp {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of names of apps for integration named [integration]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integration - name of integration
|
||||||
|
* @param {String} obj.accessToken - access token for integration
|
||||||
|
* @returns {Object[]} apps - names of integration apps
|
||||||
|
* @returns {String} apps.name - name of integration app
|
||||||
|
*/
|
||||||
|
const getApps = async ({
|
||||||
|
integrationAuth,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integrationAuth: IIntegrationAuth;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
interface App {
|
||||||
|
name: string;
|
||||||
|
siteId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let apps: App[]; // TODO: add type and define payloads for apps
|
||||||
|
try {
|
||||||
|
switch (integrationAuth.integration) {
|
||||||
|
case INTEGRATION_HEROKU:
|
||||||
|
apps = await getAppsHeroku({
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_VERCEL:
|
||||||
|
apps = await getAppsVercel({
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_NETLIFY:
|
||||||
|
apps = await getAppsNetlify({
|
||||||
|
integrationAuth,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_GITHUB:
|
||||||
|
apps = await getAppsGithub({
|
||||||
|
integrationAuth,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get integration apps');
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of names of apps for Heroku integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.accessToken - access token for Heroku API
|
||||||
|
* @returns {Object[]} apps - names of Heroku apps
|
||||||
|
* @returns {String} apps.name - name of Heroku app
|
||||||
|
*/
|
||||||
|
const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
|
||||||
|
let apps;
|
||||||
|
try {
|
||||||
|
const res = (
|
||||||
|
await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/vnd.heroku+json; version=3',
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
apps = res.map((a: any) => ({
|
||||||
|
name: a.name
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get Heroku integration apps');
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of names of apps for Vercel integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.accessToken - access token for Vercel API
|
||||||
|
* @returns {Object[]} apps - names of Vercel apps
|
||||||
|
* @returns {String} apps.name - name of Vercel app
|
||||||
|
*/
|
||||||
|
const getAppsVercel = async ({ accessToken }: { accessToken: string }) => {
|
||||||
|
let apps;
|
||||||
|
try {
|
||||||
|
const res = (
|
||||||
|
await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
apps = res.projects.map((a: any) => ({
|
||||||
|
name: a.name
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get Vercel integration apps');
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of names of sites for Netlify integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.accessToken - access token for Netlify API
|
||||||
|
* @returns {Object[]} apps - names of Netlify sites
|
||||||
|
* @returns {String} apps.name - name of Netlify site
|
||||||
|
*/
|
||||||
|
const getAppsNetlify = async ({
|
||||||
|
integrationAuth,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integrationAuth: IIntegrationAuth;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
let apps;
|
||||||
|
try {
|
||||||
|
const res = (
|
||||||
|
await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
apps = res.map((a: any) => ({
|
||||||
|
name: a.name,
|
||||||
|
siteId: a.site_id
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get Netlify integration apps');
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of names of repositories for Github integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.accessToken - access token for Netlify API
|
||||||
|
* @returns {Object[]} apps - names of Netlify sites
|
||||||
|
* @returns {String} apps.name - name of Netlify site
|
||||||
|
*/
|
||||||
|
const getAppsGithub = async ({
|
||||||
|
integrationAuth,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integrationAuth: IIntegrationAuth;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
let apps;
|
||||||
|
try {
|
||||||
|
const octokit = new Octokit({
|
||||||
|
auth: accessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
const repos = (await octokit.request(
|
||||||
|
'GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}',
|
||||||
|
{}
|
||||||
|
)).data;
|
||||||
|
|
||||||
|
apps = repos
|
||||||
|
.filter((a:any) => a.permissions.admin === true)
|
||||||
|
.map((a: any) => ({
|
||||||
|
name: a.name
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get Github repos');
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getApps };
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY,
|
||||||
|
INTEGRATION_GITHUB,
|
||||||
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
|
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||||
|
INTEGRATION_GITHUB_TOKEN_URL,
|
||||||
|
INTEGRATION_GITHUB_API_URL
|
||||||
|
} from '../variables';
|
||||||
|
import {
|
||||||
|
SITE_URL,
|
||||||
|
CLIENT_ID_VERCEL,
|
||||||
|
CLIENT_ID_NETLIFY,
|
||||||
|
CLIENT_ID_GITHUB,
|
||||||
|
CLIENT_SECRET_HEROKU,
|
||||||
|
CLIENT_SECRET_VERCEL,
|
||||||
|
CLIENT_SECRET_NETLIFY,
|
||||||
|
CLIENT_SECRET_GITHUB
|
||||||
|
} from '../config';
|
||||||
|
import { user } from '../routes';
|
||||||
|
|
||||||
|
interface ExchangeCodeHerokuResponse {
|
||||||
|
token_type: string;
|
||||||
|
access_token: string;
|
||||||
|
expires_in: number;
|
||||||
|
refresh_token: string;
|
||||||
|
user_id: string;
|
||||||
|
session_nonce?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExchangeCodeVercelResponse {
|
||||||
|
token_type: string;
|
||||||
|
access_token: string;
|
||||||
|
installation_id: string;
|
||||||
|
user_id: string;
|
||||||
|
team_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExchangeCodeNetlifyResponse {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
refresh_token: string;
|
||||||
|
scope: string;
|
||||||
|
created_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExchangeCodeGithubResponse {
|
||||||
|
access_token: string;
|
||||||
|
scope: string;
|
||||||
|
token_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2
|
||||||
|
* code-token exchange for integration named [integration]
|
||||||
|
* @param {Object} obj1
|
||||||
|
* @param {String} obj1.integration - name of integration
|
||||||
|
* @param {String} obj1.code - code for code-token exchange
|
||||||
|
* @returns {Object} obj
|
||||||
|
* @returns {String} obj.accessToken - access token for integration
|
||||||
|
* @returns {String} obj.refreshToken - refresh token for integration
|
||||||
|
* @returns {Date} obj.accessExpiresAt - date of expiration for access token
|
||||||
|
* @returns {String} obj.action - integration action for bot sequence
|
||||||
|
*/
|
||||||
|
const exchangeCode = async ({
|
||||||
|
integration,
|
||||||
|
code
|
||||||
|
}: {
|
||||||
|
integration: string;
|
||||||
|
code: string;
|
||||||
|
}) => {
|
||||||
|
let obj = {} as any;
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (integration) {
|
||||||
|
case INTEGRATION_HEROKU:
|
||||||
|
obj = await exchangeCodeHeroku({
|
||||||
|
code
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_VERCEL:
|
||||||
|
obj = await exchangeCodeVercel({
|
||||||
|
code
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_NETLIFY:
|
||||||
|
obj = await exchangeCodeNetlify({
|
||||||
|
code
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_GITHUB:
|
||||||
|
obj = await exchangeCodeGithub({
|
||||||
|
code
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed OAuth2 code-token exchange');
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
|
||||||
|
* OAuth2 code-token exchange
|
||||||
|
* @param {Object} obj1
|
||||||
|
* @param {Object} obj1.code - code for code-token exchange
|
||||||
|
* @returns {Object} obj2
|
||||||
|
* @returns {String} obj2.accessToken - access token for Heroku API
|
||||||
|
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
||||||
|
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||||
|
*/
|
||||||
|
const exchangeCodeHeroku = async ({
|
||||||
|
code
|
||||||
|
}: {
|
||||||
|
code: string;
|
||||||
|
}) => {
|
||||||
|
let res: ExchangeCodeHerokuResponse;
|
||||||
|
const accessExpiresAt = new Date();
|
||||||
|
try {
|
||||||
|
res = (await axios.post(
|
||||||
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code: code,
|
||||||
|
client_secret: CLIENT_SECRET_HEROKU
|
||||||
|
} as any)
|
||||||
|
)).data;
|
||||||
|
|
||||||
|
accessExpiresAt.setSeconds(
|
||||||
|
accessExpiresAt.getSeconds() + res.expires_in
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed OAuth2 code-token exchange with Heroku');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ({
|
||||||
|
accessToken: res.access_token,
|
||||||
|
refreshToken: res.refresh_token,
|
||||||
|
accessExpiresAt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
|
||||||
|
* code-token exchange
|
||||||
|
* @param {Object} obj1
|
||||||
|
* @param {Object} obj1.code - code for code-token exchange
|
||||||
|
* @returns {Object} obj2
|
||||||
|
* @returns {String} obj2.accessToken - access token for Heroku API
|
||||||
|
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
||||||
|
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||||
|
*/
|
||||||
|
const exchangeCodeVercel = async ({ code }: { code: string }) => {
|
||||||
|
let res: ExchangeCodeVercelResponse;
|
||||||
|
try {
|
||||||
|
res = (
|
||||||
|
await axios.post(
|
||||||
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
code: code,
|
||||||
|
client_id: CLIENT_ID_VERCEL,
|
||||||
|
client_secret: CLIENT_SECRET_VERCEL,
|
||||||
|
redirect_uri: `${SITE_URL}/vercel`
|
||||||
|
} as any)
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed OAuth2 code-token exchange with Vercel');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: res.access_token,
|
||||||
|
refreshToken: null,
|
||||||
|
accessExpiresAt: null,
|
||||||
|
teamId: res.team_id
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
|
||||||
|
* code-token exchange
|
||||||
|
* @param {Object} obj1
|
||||||
|
* @param {Object} obj1.code - code for code-token exchange
|
||||||
|
* @returns {Object} obj2
|
||||||
|
* @returns {String} obj2.accessToken - access token for Heroku API
|
||||||
|
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
||||||
|
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||||
|
*/
|
||||||
|
const exchangeCodeNetlify = async ({ code }: { code: string }) => {
|
||||||
|
let res: ExchangeCodeNetlifyResponse;
|
||||||
|
let accountId;
|
||||||
|
try {
|
||||||
|
res = (
|
||||||
|
await axios.post(
|
||||||
|
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code: code,
|
||||||
|
client_id: CLIENT_ID_NETLIFY,
|
||||||
|
client_secret: CLIENT_SECRET_NETLIFY,
|
||||||
|
redirect_uri: `${SITE_URL}/netlify`
|
||||||
|
} as any)
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
const res2 = await axios.get('https://api.netlify.com/api/v1/sites', {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${res.access_token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const res3 = (
|
||||||
|
await axios.get('https://api.netlify.com/api/v1/accounts', {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${res.access_token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
accountId = res3[0].id;
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed OAuth2 code-token exchange with Netlify');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: res.access_token,
|
||||||
|
refreshToken: res.refresh_token,
|
||||||
|
accountId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Github
|
||||||
|
* code-token exchange
|
||||||
|
* @param {Object} obj1
|
||||||
|
* @param {Object} obj1.code - code for code-token exchange
|
||||||
|
* @returns {Object} obj2
|
||||||
|
* @returns {String} obj2.accessToken - access token for Github API
|
||||||
|
* @returns {String} obj2.refreshToken - refresh token for Github API
|
||||||
|
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||||
|
*/
|
||||||
|
const exchangeCodeGithub = async ({ code }: { code: string }) => {
|
||||||
|
let res: ExchangeCodeGithubResponse;
|
||||||
|
try {
|
||||||
|
res = (
|
||||||
|
await axios.get(INTEGRATION_GITHUB_TOKEN_URL, {
|
||||||
|
params: {
|
||||||
|
client_id: CLIENT_ID_GITHUB,
|
||||||
|
client_secret: CLIENT_SECRET_GITHUB,
|
||||||
|
code: code,
|
||||||
|
redirect_uri: `${SITE_URL}/github`
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed OAuth2 code-token exchange with Github');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: res.access_token,
|
||||||
|
refreshToken: null,
|
||||||
|
accessExpiresAt: null
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { exchangeCode };
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { exchangeCode } from './exchange';
|
||||||
|
import { exchangeRefresh } from './refresh';
|
||||||
|
import { getApps } from './apps';
|
||||||
|
import { syncSecrets } from './sync';
|
||||||
|
import { revokeAccess } from './revoke';
|
||||||
|
|
||||||
|
export {
|
||||||
|
exchangeCode,
|
||||||
|
exchangeRefresh,
|
||||||
|
getApps,
|
||||||
|
syncSecrets,
|
||||||
|
revokeAccess
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { INTEGRATION_HEROKU } from '../variables';
|
||||||
|
import {
|
||||||
|
CLIENT_SECRET_HEROKU
|
||||||
|
} from '../config';
|
||||||
|
import {
|
||||||
|
INTEGRATION_HEROKU_TOKEN_URL
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new access token by exchanging refresh token [refreshToken] for integration
|
||||||
|
* named [integration]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integration - name of integration
|
||||||
|
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
|
||||||
|
*/
|
||||||
|
const exchangeRefresh = async ({
|
||||||
|
integration,
|
||||||
|
refreshToken
|
||||||
|
}: {
|
||||||
|
integration: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}) => {
|
||||||
|
let accessToken;
|
||||||
|
try {
|
||||||
|
switch (integration) {
|
||||||
|
case INTEGRATION_HEROKU:
|
||||||
|
accessToken = await exchangeRefreshHeroku({
|
||||||
|
refreshToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get new OAuth2 access token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||||
|
* Heroku integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const exchangeRefreshHeroku = async ({
|
||||||
|
refreshToken
|
||||||
|
}: {
|
||||||
|
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,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
client_secret: CLIENT_SECRET_HEROKU
|
||||||
|
} as any)
|
||||||
|
);
|
||||||
|
|
||||||
|
accessToken = res.data.access_token;
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get new OAuth2 access token for Heroku');
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { exchangeRefresh };
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { IIntegrationAuth, IntegrationAuth, Integration } from '../models';
|
||||||
|
import {
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY,
|
||||||
|
INTEGRATION_GITHUB
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
const revokeAccess = async ({
|
||||||
|
integrationAuth,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integrationAuth: IIntegrationAuth;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
// add any integration-specific revocation logic
|
||||||
|
switch (integrationAuth.integration) {
|
||||||
|
case INTEGRATION_HEROKU:
|
||||||
|
break;
|
||||||
|
case INTEGRATION_VERCEL:
|
||||||
|
break;
|
||||||
|
case INTEGRATION_NETLIFY:
|
||||||
|
break;
|
||||||
|
case INTEGRATION_GITHUB:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
||||||
|
_id: integrationAuth._id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deletedIntegrationAuth) {
|
||||||
|
await Integration.deleteMany({
|
||||||
|
integrationAuth: deletedIntegrationAuth._id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to delete integration authorization');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { revokeAccess };
|
||||||
@@ -0,0 +1,605 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Octokit } from '@octokit/rest';
|
||||||
|
// import * as sodium from 'libsodium-wrappers';
|
||||||
|
import sodium from 'libsodium-wrappers';
|
||||||
|
// const sodium = require('libsodium-wrappers');
|
||||||
|
import { IIntegration, IIntegrationAuth } from '../models';
|
||||||
|
import {
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY,
|
||||||
|
INTEGRATION_GITHUB,
|
||||||
|
INTEGRATION_HEROKU_API_URL,
|
||||||
|
INTEGRATION_VERCEL_API_URL,
|
||||||
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
|
INTEGRATION_GITHUB_API_URL
|
||||||
|
} from '../variables';
|
||||||
|
import { access, appendFile } from 'fs';
|
||||||
|
|
||||||
|
// TODO: need a helper function in the future to handle integration
|
||||||
|
// envar priorities (i.e. prioritize secrets within integration or those on Infisical)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync/push [secrets] to [app] in integration named [integration]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {IIntegration} obj.integration - integration details
|
||||||
|
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
|
||||||
|
* @param {Object} obj.app - app in integration
|
||||||
|
* @param {Object} obj.target - (optional) target (environment) in integration
|
||||||
|
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||||
|
* @param {String} obj.accessToken - access token for integration
|
||||||
|
*/
|
||||||
|
const syncSecrets = async ({
|
||||||
|
integration,
|
||||||
|
integrationAuth,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integration: IIntegration;
|
||||||
|
integrationAuth: IIntegrationAuth;
|
||||||
|
secrets: any;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
switch (integration.integration) {
|
||||||
|
case INTEGRATION_HEROKU:
|
||||||
|
await syncSecretsHeroku({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_VERCEL:
|
||||||
|
await syncSecretsVercel({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_NETLIFY:
|
||||||
|
await syncSecretsNetlify({
|
||||||
|
integration,
|
||||||
|
integrationAuth,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_GITHUB:
|
||||||
|
await syncSecretsGitHub({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to sync secrets to integration');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync/push [secrets] to Heroku [app]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {IIntegration} obj.integration - integration details
|
||||||
|
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||||
|
*/
|
||||||
|
const syncSecretsHeroku = async ({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integration: IIntegration;
|
||||||
|
secrets: any;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const herokuSecrets = (
|
||||||
|
await axios.get(
|
||||||
|
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/vnd.heroku+json; version=3',
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
Object.keys(herokuSecrets).forEach((key) => {
|
||||||
|
if (!(key in secrets)) {
|
||||||
|
secrets[key] = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await axios.patch(
|
||||||
|
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
|
||||||
|
secrets,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/vnd.heroku+json; version=3',
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to sync secrets to Heroku');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync/push [secrets] to Heroku [app]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {IIntegration} obj.integration - integration details
|
||||||
|
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||||
|
*/
|
||||||
|
const syncSecretsVercel = async ({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integration: IIntegration,
|
||||||
|
secrets: any;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
interface VercelSecret {
|
||||||
|
id?: string;
|
||||||
|
type: string;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
target: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all (decrypted) secrets back from Vercel in
|
||||||
|
// decrypted format
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
decrypt: "true"
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = (await Promise.all((await axios.get(
|
||||||
|
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
|
||||||
|
{
|
||||||
|
params,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.data
|
||||||
|
.envs
|
||||||
|
.filter((secret: VercelSecret) => secret.target.includes(integration.target))
|
||||||
|
.map(async (secret: VercelSecret) => (await axios.get(
|
||||||
|
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
)).data)
|
||||||
|
)).reduce((obj: any, secret: any) => ({
|
||||||
|
...obj,
|
||||||
|
[secret.key]: secret
|
||||||
|
}), {});
|
||||||
|
|
||||||
|
const updateSecrets: VercelSecret[] = [];
|
||||||
|
const deleteSecrets: VercelSecret[] = [];
|
||||||
|
const newSecrets: VercelSecret[] = [];
|
||||||
|
|
||||||
|
// Identify secrets to create
|
||||||
|
Object.keys(secrets).map((key) => {
|
||||||
|
if (!(key in res)) {
|
||||||
|
// case: secret has been created
|
||||||
|
newSecrets.push({
|
||||||
|
key: key,
|
||||||
|
value: secrets[key],
|
||||||
|
type: 'encrypted',
|
||||||
|
target: [integration.target]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Identify secrets to update and delete
|
||||||
|
Object.keys(res).map((key) => {
|
||||||
|
if (key in secrets) {
|
||||||
|
if (res[key].value !== secrets[key]) {
|
||||||
|
// case: secret value has changed
|
||||||
|
updateSecrets.push({
|
||||||
|
id: res[key].id,
|
||||||
|
key: key,
|
||||||
|
value: secrets[key],
|
||||||
|
type: 'encrypted',
|
||||||
|
target: [integration.target]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// case: secret has been deleted
|
||||||
|
deleteSecrets.push({
|
||||||
|
id: res[key].id,
|
||||||
|
key: key,
|
||||||
|
value: res[key].value,
|
||||||
|
type: 'encrypted',
|
||||||
|
target: [integration.target],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync/push new secrets
|
||||||
|
if (newSecrets.length > 0) {
|
||||||
|
await axios.post(
|
||||||
|
`${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`,
|
||||||
|
newSecrets,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync/push updated secrets
|
||||||
|
if (updateSecrets.length > 0) {
|
||||||
|
updateSecrets.forEach(async (secret: VercelSecret) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
...updatedSecret
|
||||||
|
} = secret;
|
||||||
|
await axios.patch(
|
||||||
|
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||||
|
updatedSecret,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete secrets
|
||||||
|
if (deleteSecrets.length > 0) {
|
||||||
|
deleteSecrets.forEach(async (secret: VercelSecret) => {
|
||||||
|
await axios.delete(
|
||||||
|
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to sync secrets to Vercel');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync/push [secrets] to Netlify site [app]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {IIntegration} obj.integration - integration details
|
||||||
|
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
|
||||||
|
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||||
|
*/
|
||||||
|
const syncSecretsNetlify = async ({
|
||||||
|
integration,
|
||||||
|
integrationAuth,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integration: IIntegration;
|
||||||
|
integrationAuth: IIntegrationAuth;
|
||||||
|
secrets: any;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
interface NetlifyValue {
|
||||||
|
id?: string;
|
||||||
|
context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production',
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NetlifySecret {
|
||||||
|
key: string;
|
||||||
|
values: NetlifyValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NetlifySecretsRes {
|
||||||
|
[index: string]: NetlifySecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getParams = new URLSearchParams({
|
||||||
|
context_name: 'all', // integration.context or all
|
||||||
|
site_id: integration.siteId
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = (await axios.get(
|
||||||
|
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||||
|
{
|
||||||
|
params: getParams,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.data
|
||||||
|
.reduce((obj: any, secret: any) => ({
|
||||||
|
...obj,
|
||||||
|
[secret.key]: secret
|
||||||
|
}), {});
|
||||||
|
|
||||||
|
const newSecrets: NetlifySecret[] = []; // createEnvVars
|
||||||
|
const deleteSecrets: string[] = []; // deleteEnvVar
|
||||||
|
const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue
|
||||||
|
const updateSecrets: NetlifySecret[] = []; // setEnvVarValue
|
||||||
|
|
||||||
|
// identify secrets to create and update
|
||||||
|
Object.keys(secrets).map((key) => {
|
||||||
|
if (!(key in res)) {
|
||||||
|
// case: Infisical secret does not exist in Netlify -> create secret
|
||||||
|
newSecrets.push({
|
||||||
|
key,
|
||||||
|
values: [{
|
||||||
|
value: secrets[key],
|
||||||
|
context: integration.context
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// case: Infisical secret exists in Netlify
|
||||||
|
const contexts = res[key].values
|
||||||
|
.reduce((obj: any, value: NetlifyValue) => ({
|
||||||
|
...obj,
|
||||||
|
[value.context]: value
|
||||||
|
}), {});
|
||||||
|
|
||||||
|
if (integration.context in contexts) {
|
||||||
|
// case: Netlify secret value exists in integration context
|
||||||
|
if (secrets[key] !== contexts[integration.context].value) {
|
||||||
|
// case: Infisical and Netlify secret values are different
|
||||||
|
// -> update Netlify secret context and value
|
||||||
|
updateSecrets.push({
|
||||||
|
key,
|
||||||
|
values: [{
|
||||||
|
context: integration.context,
|
||||||
|
value: secrets[key]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// case: Netlify secret value does not exist in integration context
|
||||||
|
// -> add the new Netlify secret context and value
|
||||||
|
updateSecrets.push({
|
||||||
|
key,
|
||||||
|
values: [{
|
||||||
|
context: integration.context,
|
||||||
|
value: secrets[key]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// identify secrets to delete
|
||||||
|
// TODO: revise (patch case where 1 context was deleted but others still there
|
||||||
|
Object.keys(res).map((key) => {
|
||||||
|
// loop through each key's context
|
||||||
|
if (!(key in secrets)) {
|
||||||
|
// case: Netlify secret does not exist in Infisical
|
||||||
|
|
||||||
|
const numberOfValues = res[key].values.length;
|
||||||
|
|
||||||
|
res[key].values.forEach((value: NetlifyValue) => {
|
||||||
|
if (value.context === integration.context) {
|
||||||
|
if (numberOfValues <= 1) {
|
||||||
|
// case: Netlify secret value has less than 1 context -> delete secret
|
||||||
|
deleteSecrets.push(key);
|
||||||
|
} else {
|
||||||
|
// case: Netlify secret value has more than 1 context -> delete secret value context
|
||||||
|
deleteSecretValues.push({
|
||||||
|
key,
|
||||||
|
values: [{
|
||||||
|
id: value.id,
|
||||||
|
context: integration.context,
|
||||||
|
value: value.value
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const syncParams = new URLSearchParams({
|
||||||
|
site_id: integration.siteId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newSecrets.length > 0) {
|
||||||
|
await axios.post(
|
||||||
|
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||||
|
newSecrets,
|
||||||
|
{
|
||||||
|
params: syncParams,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateSecrets.length > 0) {
|
||||||
|
updateSecrets.forEach(async (secret: NetlifySecret) => {
|
||||||
|
await axios.patch(
|
||||||
|
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
|
||||||
|
{
|
||||||
|
context: secret.values[0].context,
|
||||||
|
value: secret.values[0].value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params: syncParams,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteSecrets.length > 0) {
|
||||||
|
deleteSecrets.forEach(async (key: string) => {
|
||||||
|
await axios.delete(
|
||||||
|
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`,
|
||||||
|
{
|
||||||
|
params: syncParams,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteSecretValues.length > 0) {
|
||||||
|
deleteSecretValues.forEach(async (secret: NetlifySecret) => {
|
||||||
|
await axios.delete(
|
||||||
|
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`,
|
||||||
|
{
|
||||||
|
params: syncParams,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to sync secrets to Heroku');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync/push [secrets] to GitHub [repo]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {IIntegration} obj.integration - integration details
|
||||||
|
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
|
||||||
|
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||||
|
*/
|
||||||
|
const syncSecretsGitHub = async ({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integration: IIntegration;
|
||||||
|
secrets: any;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
interface GitHubRepoKey {
|
||||||
|
key_id: string;
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GitHubSecret {
|
||||||
|
name: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GitHubSecretRes {
|
||||||
|
[index: string]: GitHubSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSecrets: GitHubSecret[] = [];
|
||||||
|
|
||||||
|
const octokit = new Octokit({
|
||||||
|
auth: accessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = (await octokit.request('GET /user', {})).data;
|
||||||
|
|
||||||
|
const repoPublicKey: GitHubRepoKey = (await octokit.request(
|
||||||
|
'GET /repos/{owner}/{repo}/actions/secrets/public-key',
|
||||||
|
{
|
||||||
|
owner: user.login,
|
||||||
|
repo: integration.app
|
||||||
|
}
|
||||||
|
)).data;
|
||||||
|
|
||||||
|
// // Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key
|
||||||
|
const encryptedSecrets: GitHubSecretRes = (await octokit.request(
|
||||||
|
'GET /repos/{owner}/{repo}/actions/secrets',
|
||||||
|
{
|
||||||
|
owner: user.login,
|
||||||
|
repo: integration.app
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.data
|
||||||
|
.secrets
|
||||||
|
.reduce((obj: any, secret: any) => ({
|
||||||
|
...obj,
|
||||||
|
[secret.name]: secret
|
||||||
|
}), {});
|
||||||
|
|
||||||
|
Object.keys(encryptedSecrets).map(async (key) => {
|
||||||
|
if (!(key in secrets)) {
|
||||||
|
await octokit.request(
|
||||||
|
'DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}',
|
||||||
|
{
|
||||||
|
owner: user.login,
|
||||||
|
repo: integration.app,
|
||||||
|
secret_name: key
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(secrets).map((key) => {
|
||||||
|
// let encryptedSecret;
|
||||||
|
sodium.ready.then(async () => {
|
||||||
|
// convert secret & base64 key to Uint8Array.
|
||||||
|
const binkey = sodium.from_base64(
|
||||||
|
repoPublicKey.key,
|
||||||
|
sodium.base64_variants.ORIGINAL
|
||||||
|
);
|
||||||
|
const binsec = sodium.from_string(secrets[key]);
|
||||||
|
|
||||||
|
// encrypt secret using libsodium
|
||||||
|
const encBytes = sodium.crypto_box_seal(binsec, binkey);
|
||||||
|
|
||||||
|
// convert encrypted Uint8Array to base64
|
||||||
|
const encryptedSecret = sodium.to_base64(
|
||||||
|
encBytes,
|
||||||
|
sodium.base64_variants.ORIGINAL
|
||||||
|
);
|
||||||
|
|
||||||
|
await octokit.request(
|
||||||
|
'PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}',
|
||||||
|
{
|
||||||
|
owner: user.login,
|
||||||
|
repo: integration.app,
|
||||||
|
secret_name: key,
|
||||||
|
encrypted_value: encryptedSecret,
|
||||||
|
key_id: repoPublicKey.key_id
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to sync secrets to GitHub');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { syncSecrets };
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"heroku": {
|
|
||||||
"name": "Heroku",
|
|
||||||
"type": "oauth2",
|
|
||||||
"clientId": "bc132901-935a-4590-b010-f1857efc380d",
|
|
||||||
"docsLink": ""
|
|
||||||
},
|
|
||||||
"netlify": {
|
|
||||||
"name": "Netlify",
|
|
||||||
"type": "oauth2",
|
|
||||||
"clientId": "",
|
|
||||||
"docsLink": ""
|
|
||||||
},
|
|
||||||
"digitalocean": {
|
|
||||||
"name": "Digital Ocean",
|
|
||||||
"type": "oauth2",
|
|
||||||
"clientId": "",
|
|
||||||
"docsLink": ""
|
|
||||||
},
|
|
||||||
"gcp": {
|
|
||||||
"name": "Google Cloud Platform",
|
|
||||||
"type": "oauth2",
|
|
||||||
"clientId": "",
|
|
||||||
"docsLink": ""
|
|
||||||
},
|
|
||||||
"aws": {
|
|
||||||
"name": "Amazon Web Services",
|
|
||||||
"type": "oauth2",
|
|
||||||
"clientId": "",
|
|
||||||
"docsLink": ""
|
|
||||||
},
|
|
||||||
"azure": {
|
|
||||||
"name": "Microsoft Azure",
|
|
||||||
"type": "oauth2",
|
|
||||||
"clientId": "",
|
|
||||||
"docsLink": ""
|
|
||||||
},
|
|
||||||
"travisci": {
|
|
||||||
"name": "Travis CI",
|
|
||||||
"type": "oauth2",
|
|
||||||
"clientId": "",
|
|
||||||
"docsLink": ""
|
|
||||||
},
|
|
||||||
"circleci": {
|
|
||||||
"name": "Circle CI",
|
|
||||||
"type": "oauth2",
|
|
||||||
"clientId": "",
|
|
||||||
"docsLink": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import requireAuth from './requireAuth';
|
import requireAuth from './requireAuth';
|
||||||
|
import requireBotAuth from './requireBotAuth';
|
||||||
import requireSignupAuth from './requireSignupAuth';
|
import requireSignupAuth from './requireSignupAuth';
|
||||||
import requireWorkspaceAuth from './requireWorkspaceAuth';
|
import requireWorkspaceAuth from './requireWorkspaceAuth';
|
||||||
import requireOrganizationAuth from './requireOrganizationAuth';
|
import requireOrganizationAuth from './requireOrganizationAuth';
|
||||||
@@ -9,6 +10,7 @@ import validateRequest from './validateRequest';
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
requireAuth,
|
requireAuth,
|
||||||
|
requireBotAuth,
|
||||||
requireSignupAuth,
|
requireSignupAuth,
|
||||||
requireWorkspaceAuth,
|
requireWorkspaceAuth,
|
||||||
requireOrganizationAuth,
|
requireOrganizationAuth,
|
||||||
|
|||||||
@@ -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((<RequestError>error).levelName.toLowerCase(), (<RequestError>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((<RequestError>error).level)){
|
||||||
|
Sentry.captureException(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status((<RequestError>error).statusCode).json((<RequestError>error).format(req))
|
||||||
|
next()
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { User } from '../models';
|
import { User } from '../models';
|
||||||
import { JWT_AUTH_SECRET } from '../config';
|
import { JWT_AUTH_SECRET } from '../config';
|
||||||
|
import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors';
|
||||||
|
|
||||||
declare module 'jsonwebtoken' {
|
declare module 'jsonwebtoken' {
|
||||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||||
@@ -20,32 +20,25 @@ declare module 'jsonwebtoken' {
|
|||||||
*/
|
*/
|
||||||
const requireAuth = async (req: Request, res: Response, next: NextFunction) => {
|
const requireAuth = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
// JWT authentication middleware
|
// JWT authentication middleware
|
||||||
try {
|
const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
|
||||||
if (!req.headers?.authorization)
|
if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`}))
|
||||||
throw new Error('Failed to locate authorization 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.UserIDJwtPayload>(
|
||||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
jwt.verify(AUTH_TOKEN_VALUE, JWT_AUTH_SECRET)
|
||||||
jwt.verify(token, JWT_AUTH_SECRET)
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
_id: decodedToken.userId
|
_id: decodedToken.userId
|
||||||
}).select('+publicKey');
|
}).select('+publicKey');
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to authenticate unfound user');
|
if (!user) return next(AccountNotFoundError({message: 'Failed to locate User account'}))
|
||||||
if (!user?.publicKey)
|
if (!user?.publicKey)
|
||||||
throw new Error('Failed to authenticate not fully set up account');
|
return next(UnauthorizedRequestError({message: 'Unable to authenticate due to partially set up account'}))
|
||||||
|
|
||||||
req.user = user;
|
req.user = user;
|
||||||
return next();
|
return next();
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(401).send({
|
|
||||||
error: 'Failed to authenticate user. Try logging in'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default requireAuth;
|
export default requireAuth;
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
const requireBotAuth = ({
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses,
|
||||||
|
location = 'params'
|
||||||
|
}: {
|
||||||
|
acceptedRoles: string[];
|
||||||
|
acceptedStatuses: string[];
|
||||||
|
location?: req;
|
||||||
|
}) => {
|
||||||
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default requireBotAuth;
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { Integration, IntegrationAuth, Membership } from '../models';
|
import { Integration, IntegrationAuth } from '../models';
|
||||||
import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
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
|
* Validate if user on request is a member of workspace with proper roles associated
|
||||||
@@ -20,56 +21,40 @@ const requireIntegrationAuth = ({
|
|||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
// integration authorization middleware
|
// integration authorization middleware
|
||||||
|
|
||||||
try {
|
const { integrationId } = req.params;
|
||||||
const { integrationId } = req.params;
|
|
||||||
|
|
||||||
// validate integration accessibility
|
// validate integration accessibility
|
||||||
const integration = await Integration.findOne({
|
const integration = await Integration.findOne({
|
||||||
_id: integrationId
|
_id: integrationId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!integration) {
|
if (!integration) {
|
||||||
throw new Error('Failed to find integration');
|
return next(IntegrationNotFoundError({message: 'Failed to locate Integration'}))
|
||||||
}
|
|
||||||
|
|
||||||
const membership = await Membership.findOne({
|
|
||||||
user: req.user._id,
|
|
||||||
workspace: integration.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!membership) {
|
|
||||||
throw new Error('Failed to find integration workspace membership');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptedRoles.includes(membership.role)) {
|
|
||||||
throw new Error('Failed to validate workspace membership role');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptedStatuses.includes(membership.status)) {
|
|
||||||
throw new Error('Failed to validate workspace membership status');
|
|
||||||
}
|
|
||||||
|
|
||||||
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 getOAuthAccessToken({ integrationAuth });
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(401).send({
|
|
||||||
error: 'Failed integration authorization'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { IntegrationAuth, Membership } from '../models';
|
import { IntegrationAuth } from '../models';
|
||||||
import { decryptSymmetric } from '../utils/crypto';
|
import { IntegrationService } from '../services';
|
||||||
import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
import { validateMembership } from '../helpers/membership';
|
||||||
|
import { UnauthorizedRequestError } from '../utils/errors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate if user on request is a member of workspace with proper roles associated
|
* Validate if user on request is a member of workspace with proper roles associated
|
||||||
@@ -10,62 +11,45 @@ import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
|||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String[]} obj.acceptedRoles - accepted workspace roles
|
* @param {String[]} obj.acceptedRoles - accepted workspace roles
|
||||||
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses
|
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses
|
||||||
* @param {Boolean} obj.attachRefresh - whether or not to decrypt and attach integration authorization refresh token onto request
|
* @param {Boolean} obj.attachAccessToken - whether or not to decrypt and attach integration authorization access token onto request
|
||||||
*/
|
*/
|
||||||
const requireIntegrationAuthorizationAuth = ({
|
const requireIntegrationAuthorizationAuth = ({
|
||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
acceptedStatuses
|
acceptedStatuses,
|
||||||
|
attachAccessToken = true
|
||||||
}: {
|
}: {
|
||||||
acceptedRoles: string[];
|
acceptedRoles: string[];
|
||||||
acceptedStatuses: string[];
|
acceptedStatuses: string[];
|
||||||
|
attachAccessToken?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
// (authorization) integration authorization middleware
|
const { integrationAuthId } = req.params;
|
||||||
|
|
||||||
try {
|
const integrationAuth = await IntegrationAuth.findOne({
|
||||||
const { integrationAuthId } = req.params;
|
_id: integrationAuthId
|
||||||
|
}).select(
|
||||||
|
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
|
||||||
|
);
|
||||||
|
|
||||||
const integrationAuth = await IntegrationAuth.findOne({
|
if (!integrationAuth) {
|
||||||
_id: integrationAuthId
|
return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authorization credentials'}))
|
||||||
}).select(
|
}
|
||||||
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
|
|
||||||
);
|
await validateMembership({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
|
});
|
||||||
|
|
||||||
if (!integrationAuth) {
|
req.integrationAuth = integrationAuth;
|
||||||
throw new Error('Failed to find integration authorization');
|
if (attachAccessToken) {
|
||||||
}
|
req.accessToken = await IntegrationService.getIntegrationAuthAccess({
|
||||||
|
integrationAuthId: integrationAuth._id.toString()
|
||||||
const membership = await Membership.findOne({
|
|
||||||
user: req.user._id,
|
|
||||||
workspace: integrationAuth.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!membership) {
|
|
||||||
throw new Error(
|
|
||||||
'Failed to find integration authorization workspace membership'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptedRoles.includes(membership.role)) {
|
|
||||||
throw new Error('Failed to validate workspace membership role');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptedStatuses.includes(membership.status)) {
|
|
||||||
throw new Error('Failed to validate workspace membership status');
|
|
||||||
}
|
|
||||||
|
|
||||||
req.integrationAuth = integrationAuth;
|
|
||||||
|
|
||||||
// TODO: make compatible with other integration types since they won't necessarily have access tokens
|
|
||||||
req.accessToken = await getOAuthAccessToken({ integrationAuth });
|
|
||||||
return next();
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(401).send({
|
|
||||||
error: 'Failed (authorization) integration authorizationt'
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { IOrganization, MembershipOrg } from '../models';
|
import { IOrganization, MembershipOrg } from '../models';
|
||||||
|
import { UnauthorizedRequestError, ValidationError } from '../utils/errors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate if user on request is a member with proper roles for organization
|
* 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) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
// organization authorization middleware
|
// organization authorization middleware
|
||||||
|
|
||||||
try {
|
// validate organization membership
|
||||||
// validate organization membership
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
user: req.user._id,
|
||||||
user: req.user._id,
|
organization: req.params.organizationId
|
||||||
organization: req.params.organizationId
|
}).populate<{ organization: IOrganization }>('organization');
|
||||||
}).populate<{ organization: IOrganization }>('organization');
|
|
||||||
|
|
||||||
if (!membershipOrg) {
|
|
||||||
throw new Error('Failed to find organization membership');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptedRoles.includes(membershipOrg.role)) {
|
if (!membershipOrg) {
|
||||||
throw new Error('Failed to validate organization membership role');
|
return next(UnauthorizedRequestError({message: "You're not a member of this Organization."}))
|
||||||
}
|
|
||||||
|
|
||||||
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'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
//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();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { ServiceToken } from '../models';
|
import { ServiceToken } from '../models';
|
||||||
import { JWT_SERVICE_SECRET } from '../config';
|
import { JWT_SERVICE_SECRET } from '../config';
|
||||||
|
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
|
||||||
|
|
||||||
declare module 'jsonwebtoken' {
|
declare module 'jsonwebtoken' {
|
||||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||||
@@ -24,33 +24,27 @@ const requireServiceTokenAuth = async (
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
) => {
|
) => {
|
||||||
// JWT service token middleware
|
// JWT service token middleware
|
||||||
try {
|
|
||||||
if (!req.headers?.authorization)
|
const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
|
||||||
throw new Error('Failed to locate authorization header');
|
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.UserIDJwtPayload>(
|
||||||
|
jwt.verify(AUTH_TOKEN_VALUE, JWT_SERVICE_SECRET)
|
||||||
|
);
|
||||||
|
|
||||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
const serviceToken = await ServiceToken.findOne({
|
||||||
jwt.verify(token, JWT_SERVICE_SECRET)
|
_id: decodedToken.serviceTokenId
|
||||||
);
|
})
|
||||||
|
.populate('user', '+publicKey')
|
||||||
|
.select('+encryptedKey +publicKey +nonce');
|
||||||
|
|
||||||
const serviceToken = await ServiceToken.findOne({
|
if (!serviceToken) return next(UnauthorizedRequestError({message: 'The service token does not match the record in the database'}))
|
||||||
_id: decodedToken.serviceTokenId
|
|
||||||
})
|
|
||||||
.populate('user', '+publicKey')
|
|
||||||
.select('+encryptedKey +publicKey +nonce');
|
|
||||||
|
|
||||||
if (!serviceToken) throw new Error('Failed to find service token');
|
req.serviceToken = serviceToken;
|
||||||
|
return next();
|
||||||
req.serviceToken = serviceToken;
|
|
||||||
return next();
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(401).send({
|
|
||||||
error: 'Failed to authenticate service token'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default requireServiceTokenAuth;
|
export default requireServiceTokenAuth;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { User } from '../models';
|
import { User } from '../models';
|
||||||
import { JWT_SIGNUP_SECRET } from '../config';
|
import { JWT_SIGNUP_SECRET } from '../config';
|
||||||
|
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
|
||||||
|
|
||||||
declare module 'jsonwebtoken' {
|
declare module 'jsonwebtoken' {
|
||||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||||
@@ -21,32 +21,24 @@ const requireSignupAuth = async (
|
|||||||
) => {
|
) => {
|
||||||
// JWT (temporary) authentication middleware for complete signup
|
// JWT (temporary) authentication middleware for complete signup
|
||||||
|
|
||||||
try {
|
const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
|
||||||
if (!req.headers?.authorization)
|
if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`}))
|
||||||
throw new Error('Failed to locate authorization 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.UserIDJwtPayload>(
|
||||||
|
jwt.verify(AUTH_TOKEN_VALUE, JWT_SIGNUP_SECRET)
|
||||||
|
);
|
||||||
|
|
||||||
const token = req.headers.authorization.split(' ')[1];
|
const user = await User.findOne({
|
||||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
_id: decodedToken.userId
|
||||||
jwt.verify(token, JWT_SIGNUP_SECRET)
|
}).select('+publicKey');
|
||||||
);
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
if (!user)
|
||||||
_id: decodedToken.userId
|
return next(UnauthorizedRequestError({message: 'Unable to authenticate for User account completion. Try logging in again'}))
|
||||||
}).select('+publicKey');
|
|
||||||
|
|
||||||
if (!user)
|
req.user = user;
|
||||||
throw new Error('Failed to temporarily authenticate unfound user');
|
return next();
|
||||||
|
|
||||||
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'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default requireSignupAuth;
|
export default requireSignupAuth;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { Membership, IWorkspace } from '../models';
|
import { validateMembership } from '../helpers/membership';
|
||||||
|
import { UnauthorizedRequestError } from '../utils/errors';
|
||||||
|
|
||||||
type req = 'params' | 'body' | 'query';
|
type req = 'params' | 'body' | 'query';
|
||||||
|
|
||||||
@@ -25,34 +25,18 @@ const requireWorkspaceAuth = ({
|
|||||||
// workspace authorization middleware
|
// workspace authorization middleware
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// validate workspace membership
|
const membership = await validateMembership({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
const membership = await Membership.findOne({
|
workspaceId: req[location].workspaceId,
|
||||||
user: req.user._id,
|
acceptedRoles,
|
||||||
workspace: req[location].workspaceId
|
acceptedStatuses
|
||||||
}).populate<{ workspace: IWorkspace }>('workspace');
|
});
|
||||||
|
|
||||||
if (!membership) {
|
|
||||||
throw new Error('Failed to find workspace membership');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptedRoles.includes(membership.role)) {
|
|
||||||
throw new Error('Failed to validate workspace membership role');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptedStatuses.includes(membership.status)) {
|
|
||||||
throw new Error('Failed to validate workspace membership status');
|
|
||||||
}
|
|
||||||
|
|
||||||
req.membership = membership;
|
req.membership = membership;
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
return next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(401).send({
|
|
||||||
error: 'Failed workspace authorization'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { validationResult } from 'express-validator';
|
import { validationResult } from 'express-validator';
|
||||||
|
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate intended inputs on [req] via express-validator
|
* Validate intended inputs on [req] via express-validator
|
||||||
@@ -15,16 +15,12 @@ const validate = (req: Request, res: Response, next: NextFunction) => {
|
|||||||
try {
|
try {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
return res.status(400).json({ errors: errors.array() });
|
return next(BadRequestError({context: {errors: errors.array}}))
|
||||||
}
|
}
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
return next(UnauthorizedRequestError({message: 'Unauthenticated requests are not allowed. Try logging in'}))
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(401).send({
|
|
||||||
error: "Looks like you're unauthenticated . Try logging in"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
|
||||||
|
export interface IBot {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
name: string;
|
||||||
|
workspace: Types.ObjectId;
|
||||||
|
isActive: boolean;
|
||||||
|
publicKey: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const botSchema = new Schema<IBot>(
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Workspace',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
publicKey: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
encryptedPrivateKey: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
iv: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
tag: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
select: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Bot = model<IBot>('Bot', botSchema);
|
||||||
|
|
||||||
|
export default Bot;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
|
||||||
|
export interface IBotKey {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
encryptedKey: string;
|
||||||
|
nonce: string;
|
||||||
|
sender: Types.ObjectId;
|
||||||
|
bot: Types.ObjectId;
|
||||||
|
workspace: Types.ObjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const botKeySchema = new Schema<IBotKey>(
|
||||||
|
{
|
||||||
|
encryptedKey: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
nonce: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
bot: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Bot',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Workspace',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const BotKey = model<IBotKey>('BotKey', botKeySchema);
|
||||||
|
|
||||||
|
export default BotKey;
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import BackupPrivateKey, { IBackupPrivateKey } from './backupPrivateKey';
|
import BackupPrivateKey, { IBackupPrivateKey } from './backupPrivateKey';
|
||||||
|
import Bot, { IBot } from './bot';
|
||||||
|
import BotKey, { IBotKey } from './botKey';
|
||||||
import IncidentContactOrg, { IIncidentContactOrg } from './incidentContactOrg';
|
import IncidentContactOrg, { IIncidentContactOrg } from './incidentContactOrg';
|
||||||
import Integration, { IIntegration } from './integration';
|
import Integration, { IIntegration } from './integration';
|
||||||
import IntegrationAuth, { IIntegrationAuth } from './integrationAuth';
|
import IntegrationAuth, { IIntegrationAuth } from './integrationAuth';
|
||||||
@@ -16,6 +18,10 @@ import Workspace, { IWorkspace } from './workspace';
|
|||||||
export {
|
export {
|
||||||
BackupPrivateKey,
|
BackupPrivateKey,
|
||||||
IBackupPrivateKey,
|
IBackupPrivateKey,
|
||||||
|
Bot,
|
||||||
|
IBot,
|
||||||
|
BotKey,
|
||||||
|
IBotKey,
|
||||||
IncidentContactOrg,
|
IncidentContactOrg,
|
||||||
IIncidentContactOrg,
|
IIncidentContactOrg,
|
||||||
Integration,
|
Integration,
|
||||||
|
|||||||
@@ -1,59 +1,83 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
ENV_DEV,
|
ENV_DEV,
|
||||||
ENV_TESTING,
|
ENV_TESTING,
|
||||||
ENV_STAGING,
|
ENV_STAGING,
|
||||||
ENV_PROD,
|
ENV_PROD,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_NETLIFY
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY,
|
||||||
|
INTEGRATION_GITHUB
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
|
|
||||||
export interface IIntegration {
|
export interface IIntegration {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
workspace: Types.ObjectId;
|
workspace: Types.ObjectId;
|
||||||
environment: 'dev' | 'test' | 'staging' | 'prod';
|
environment: 'dev' | 'test' | 'staging' | 'prod';
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
app: string;
|
app: string;
|
||||||
integration: 'heroku' | 'netlify';
|
target: string;
|
||||||
integrationAuth: Types.ObjectId;
|
context: string;
|
||||||
|
siteId: string;
|
||||||
|
integration: 'heroku' | 'vercel' | 'netlify' | 'github';
|
||||||
|
integrationAuth: Types.ObjectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const integrationSchema = new Schema<IIntegration>(
|
const integrationSchema = new Schema<IIntegration>(
|
||||||
{
|
{
|
||||||
workspace: {
|
workspace: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Workspace',
|
ref: 'Workspace',
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
environment: {
|
environment: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
isActive: {
|
isActive: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
// name of app in provider
|
// name of app in provider
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null
|
||||||
required: true
|
},
|
||||||
},
|
target: {
|
||||||
integration: {
|
// vercel-specific target (environment)
|
||||||
type: String,
|
type: String,
|
||||||
enum: [INTEGRATION_HEROKU, INTEGRATION_NETLIFY],
|
default: null
|
||||||
required: true
|
},
|
||||||
},
|
context: {
|
||||||
integrationAuth: {
|
// netlify-specific context (deploy)
|
||||||
type: Schema.Types.ObjectId,
|
type: String,
|
||||||
ref: 'IntegrationAuth',
|
default: null
|
||||||
required: true
|
},
|
||||||
}
|
siteId: {
|
||||||
},
|
// netlify-specific site (app) id
|
||||||
{
|
type: String,
|
||||||
timestamps: true
|
default: null
|
||||||
}
|
},
|
||||||
|
integration: {
|
||||||
|
type: String,
|
||||||
|
enum: [
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY,
|
||||||
|
INTEGRATION_GITHUB
|
||||||
|
],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
integrationAuth: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'IntegrationAuth',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const Integration = model<IIntegration>('Integration', integrationSchema);
|
const Integration = model<IIntegration>('Integration', integrationSchema);
|
||||||
|
|||||||
@@ -1,67 +1,87 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from 'mongoose';
|
||||||
import { INTEGRATION_HEROKU, INTEGRATION_NETLIFY } from '../variables';
|
import {
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY,
|
||||||
|
INTEGRATION_GITHUB
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
export interface IIntegrationAuth {
|
export interface IIntegrationAuth {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
workspace: Types.ObjectId;
|
workspace: Types.ObjectId;
|
||||||
integration: 'heroku' | 'netlify';
|
integration: 'heroku' | 'vercel' | 'netlify' | 'github';
|
||||||
refreshCiphertext?: string;
|
teamId: string;
|
||||||
refreshIV?: string;
|
accountId: string;
|
||||||
refreshTag?: string;
|
refreshCiphertext?: string;
|
||||||
accessCiphertext?: string;
|
refreshIV?: string;
|
||||||
accessIV?: string;
|
refreshTag?: string;
|
||||||
accessTag?: string;
|
accessCiphertext?: string;
|
||||||
accessExpiresAt?: Date;
|
accessIV?: string;
|
||||||
|
accessTag?: string;
|
||||||
|
accessExpiresAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
||||||
{
|
{
|
||||||
workspace: {
|
workspace: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
integration: {
|
integration: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [INTEGRATION_HEROKU, INTEGRATION_NETLIFY],
|
enum: [
|
||||||
required: true
|
INTEGRATION_HEROKU,
|
||||||
},
|
INTEGRATION_VERCEL,
|
||||||
refreshCiphertext: {
|
INTEGRATION_NETLIFY,
|
||||||
type: String,
|
INTEGRATION_GITHUB
|
||||||
select: false
|
],
|
||||||
},
|
required: true
|
||||||
refreshIV: {
|
},
|
||||||
type: String,
|
teamId: {
|
||||||
select: false
|
// vercel-specific integration param
|
||||||
},
|
type: String
|
||||||
refreshTag: {
|
},
|
||||||
type: String,
|
accountId: {
|
||||||
select: false
|
// netlify-specific integration param
|
||||||
},
|
type: String
|
||||||
accessCiphertext: {
|
},
|
||||||
type: String,
|
refreshCiphertext: {
|
||||||
select: false
|
type: String,
|
||||||
},
|
select: false
|
||||||
accessIV: {
|
},
|
||||||
type: String,
|
refreshIV: {
|
||||||
select: false
|
type: String,
|
||||||
},
|
select: false
|
||||||
accessTag: {
|
},
|
||||||
type: String,
|
refreshTag: {
|
||||||
select: false
|
type: String,
|
||||||
},
|
select: false
|
||||||
accessExpiresAt: {
|
},
|
||||||
type: Date,
|
accessCiphertext: {
|
||||||
select: false
|
type: String,
|
||||||
}
|
select: false
|
||||||
},
|
},
|
||||||
{
|
accessIV: {
|
||||||
timestamps: true
|
type: String,
|
||||||
}
|
select: false
|
||||||
|
},
|
||||||
|
accessTag: {
|
||||||
|
type: String,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
accessExpiresAt: {
|
||||||
|
type: Date,
|
||||||
|
select: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const IntegrationAuth = model<IIntegrationAuth>(
|
const IntegrationAuth = model<IIntegrationAuth>(
|
||||||
'IntegrationAuth',
|
'IntegrationAuth',
|
||||||
integrationAuthSchema
|
integrationAuthSchema
|
||||||
);
|
);
|
||||||
|
|
||||||
export default IntegrationAuth;
|
export default IntegrationAuth;
|
||||||
|
|||||||
+21
-16
@@ -2,25 +2,30 @@ import { Schema, model } from 'mongoose';
|
|||||||
import { EMAIL_TOKEN_LIFETIME } from '../config';
|
import { EMAIL_TOKEN_LIFETIME } from '../config';
|
||||||
|
|
||||||
export interface IToken {
|
export interface IToken {
|
||||||
email: String;
|
email: string;
|
||||||
token: String;
|
token: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenSchema = new Schema<IToken>({
|
const tokenSchema = new Schema<IToken>({
|
||||||
email: {
|
email: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
token: {
|
token: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
type: Date,
|
type: Date,
|
||||||
expires: EMAIL_TOKEN_LIFETIME,
|
default: Date.now
|
||||||
default: Date.now
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
tokenSchema.index({
|
||||||
|
createdAt: 1
|
||||||
|
}, {
|
||||||
|
expireAfterSeconds: parseInt(EMAIL_TOKEN_LIFETIME)
|
||||||
});
|
});
|
||||||
|
|
||||||
const Token = model<IToken>('Token', tokenSchema);
|
const Token = model<IToken>('Token', tokenSchema);
|
||||||
|
|||||||
+14
-18
@@ -5,28 +5,24 @@ import { requireAuth, validateRequest } from '../middleware';
|
|||||||
import { authController } from '../controllers';
|
import { authController } from '../controllers';
|
||||||
import { loginLimiter } from '../helpers/rateLimiter';
|
import { loginLimiter } from '../helpers/rateLimiter';
|
||||||
|
|
||||||
|
router.post('/token', validateRequest, authController.getNewToken);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/token',
|
'/login1',
|
||||||
validateRequest,
|
loginLimiter,
|
||||||
authController.getNewToken
|
body('email').exists().trim().notEmpty(),
|
||||||
|
body('clientPublicKey').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
authController.login1
|
||||||
);
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/login1',
|
'/login2',
|
||||||
loginLimiter,
|
loginLimiter,
|
||||||
body('email').exists().trim().notEmpty(),
|
body('email').exists().trim().notEmpty(),
|
||||||
body('clientPublicKey').exists().trim().notEmpty(),
|
body('clientProof').exists().trim().notEmpty(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
authController.login1
|
authController.login2
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/login2',
|
|
||||||
loginLimiter,
|
|
||||||
body('email').exists().trim().notEmpty(),
|
|
||||||
body('clientProof').exists().trim().notEmpty(),
|
|
||||||
validateRequest,
|
|
||||||
authController.login2
|
|
||||||
);
|
);
|
||||||
|
|
||||||
router.post('/logout', requireAuth, authController.logout);
|
router.post('/logout', requireAuth, authController.logout);
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import { body, param } from 'express-validator';
|
||||||
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireBotAuth,
|
||||||
|
requireWorkspaceAuth,
|
||||||
|
validateRequest
|
||||||
|
} from '../middleware';
|
||||||
|
import { botController } from '../controllers';
|
||||||
|
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:workspaceId',
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [COMPLETED, GRANTED]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
botController.getBotByWorkspaceId
|
||||||
|
);
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
'/:botId/active',
|
||||||
|
requireAuth,
|
||||||
|
requireBotAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [COMPLETED, GRANTED]
|
||||||
|
}),
|
||||||
|
body('isActive').isBoolean(),
|
||||||
|
body('botKey'),
|
||||||
|
validateRequest,
|
||||||
|
botController.setBotActiveState
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import signup from './signup';
|
import signup from './signup';
|
||||||
|
import bot from './bot';
|
||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
import user from './user';
|
import user from './user';
|
||||||
import userAction from './userAction';
|
import userAction from './userAction';
|
||||||
@@ -18,6 +19,7 @@ import integrationAuth from './integrationAuth';
|
|||||||
export {
|
export {
|
||||||
signup,
|
signup,
|
||||||
auth,
|
auth,
|
||||||
|
bot,
|
||||||
user,
|
user,
|
||||||
userAction,
|
userAction,
|
||||||
organization,
|
organization,
|
||||||
|
|||||||
@@ -9,22 +9,6 @@ import { ADMIN, MEMBER, GRANTED } from '../variables';
|
|||||||
import { body, param } from 'express-validator';
|
import { body, param } from 'express-validator';
|
||||||
import { integrationController } from '../controllers';
|
import { integrationController } from '../controllers';
|
||||||
|
|
||||||
router.get('/integrations', requireAuth, integrationController.getIntegrations);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/:integrationId/sync',
|
|
||||||
requireAuth,
|
|
||||||
requireIntegrationAuth({
|
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
|
||||||
acceptedStatuses: [GRANTED]
|
|
||||||
}),
|
|
||||||
param('integrationId').exists().trim(),
|
|
||||||
body('key').exists(),
|
|
||||||
body('secrets').exists(),
|
|
||||||
validateRequest,
|
|
||||||
integrationController.syncIntegration
|
|
||||||
);
|
|
||||||
|
|
||||||
router.patch(
|
router.patch(
|
||||||
'/:integrationId',
|
'/:integrationId',
|
||||||
requireAuth,
|
requireAuth,
|
||||||
@@ -32,10 +16,15 @@ router.patch(
|
|||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
acceptedStatuses: [GRANTED]
|
acceptedStatuses: [GRANTED]
|
||||||
}),
|
}),
|
||||||
param('integrationId'),
|
param('integrationId').exists().trim(),
|
||||||
body('update'),
|
body('app').exists().trim(),
|
||||||
|
body('environment').exists().trim(),
|
||||||
|
body('isActive').exists().isBoolean(),
|
||||||
|
body('target').exists(),
|
||||||
|
body('context').exists(),
|
||||||
|
body('siteId').exists(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
integrationController.modifyIntegration
|
integrationController.updateIntegration
|
||||||
);
|
);
|
||||||
|
|
||||||
router.delete(
|
router.delete(
|
||||||
@@ -45,7 +34,7 @@ router.delete(
|
|||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
acceptedStatuses: [GRANTED]
|
acceptedStatuses: [GRANTED]
|
||||||
}),
|
}),
|
||||||
param('integrationId'),
|
param('integrationId').exists().trim(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
integrationController.deleteIntegration
|
integrationController.deleteIntegration
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ import {
|
|||||||
import { ADMIN, MEMBER, GRANTED } from '../variables';
|
import { ADMIN, MEMBER, GRANTED } from '../variables';
|
||||||
import { integrationAuthController } from '../controllers';
|
import { integrationAuthController } from '../controllers';
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/integration-options',
|
||||||
|
requireAuth,
|
||||||
|
integrationAuthController.getIntegrationOptions
|
||||||
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/oauth-token',
|
'/oauth-token',
|
||||||
requireAuth,
|
requireAuth,
|
||||||
@@ -22,7 +28,7 @@ router.post(
|
|||||||
body('code').exists().trim().notEmpty(),
|
body('code').exists().trim().notEmpty(),
|
||||||
body('integration').exists().trim().notEmpty(),
|
body('integration').exists().trim().notEmpty(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
integrationAuthController.integrationAuthOauthExchange
|
integrationAuthController.oAuthExchange
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
@@ -42,7 +48,8 @@ router.delete(
|
|||||||
requireAuth,
|
requireAuth,
|
||||||
requireIntegrationAuthorizationAuth({
|
requireIntegrationAuthorizationAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
acceptedStatuses: [GRANTED]
|
acceptedStatuses: [GRANTED],
|
||||||
|
attachAccessToken: false
|
||||||
}),
|
}),
|
||||||
param('integrationAuthId'),
|
param('integrationAuthId'),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
|
|||||||
@@ -34,6 +34,4 @@ router.get(
|
|||||||
keyController.getLatestKey
|
keyController.getLatestKey
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get('/publicKey/infisical', keyController.getPublicKeyInfisical);
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import { body } from 'express-validator';
|
import { body } from 'express-validator';
|
||||||
import { requireAuth, validateRequest } from '../middleware';
|
import { requireAuth, requireSignupAuth, validateRequest } from '../middleware';
|
||||||
import { passwordController } from '../controllers';
|
import { passwordController } from '../controllers';
|
||||||
import { passwordLimiter } from '../helpers/rateLimiter';
|
import { passwordLimiter } from '../helpers/rateLimiter';
|
||||||
|
|
||||||
@@ -27,6 +27,30 @@ router.post(
|
|||||||
passwordController.changePassword
|
passwordController.changePassword
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/email/password-reset',
|
||||||
|
passwordLimiter,
|
||||||
|
body('email').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
passwordController.emailPasswordReset
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/email/password-reset-verify',
|
||||||
|
passwordLimiter,
|
||||||
|
body('email').exists().trim().notEmpty().isEmail(),
|
||||||
|
body('code').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
passwordController.emailPasswordResetVerify
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/backup-private-key',
|
||||||
|
passwordLimiter,
|
||||||
|
requireSignupAuth,
|
||||||
|
passwordController.getBackupPrivateKey
|
||||||
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/backup-private-key',
|
'/backup-private-key',
|
||||||
passwordLimiter,
|
passwordLimiter,
|
||||||
@@ -41,4 +65,16 @@ router.post(
|
|||||||
passwordController.createBackupPrivateKey
|
passwordController.createBackupPrivateKey
|
||||||
);
|
);
|
||||||
|
|
||||||
export default router;
|
router.post(
|
||||||
|
'/password-reset',
|
||||||
|
requireSignupAuth,
|
||||||
|
body('encryptedPrivateKey').exists().trim().notEmpty(), // private key encrypted under new pwd
|
||||||
|
body('iv').exists().trim().notEmpty(), // new iv for private key
|
||||||
|
body('tag').exists().trim().notEmpty(), // new tag for private key
|
||||||
|
body('salt').exists().trim().notEmpty(), // part of new pwd
|
||||||
|
body('verifier').exists().trim().notEmpty(), // part of new pwd
|
||||||
|
validateRequest,
|
||||||
|
passwordController.resetPassword
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import {
|
||||||
|
getSecretsHelper,
|
||||||
|
encryptSymmetricHelper,
|
||||||
|
decryptSymmetricHelper
|
||||||
|
} from '../helpers/bot';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to handle bot actions
|
||||||
|
*/
|
||||||
|
class BotService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return decrypted secrets for workspace with id [workspaceId] and
|
||||||
|
* environment [environmen] shared to bot.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace of secrets
|
||||||
|
* @param {String} obj.environment - environment for secrets
|
||||||
|
* @returns {Object} secretObj - object where keys are secret keys and values are secret values
|
||||||
|
*/
|
||||||
|
static async getSecrets({
|
||||||
|
workspaceId,
|
||||||
|
environment
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
}) {
|
||||||
|
return await getSecretsHelper({
|
||||||
|
workspaceId,
|
||||||
|
environment
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return symmetrically encrypted [plaintext] using the
|
||||||
|
* bot's copy of the workspace key for workspace with id [workspaceId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @param {String} obj.plaintext - plaintext to encrypt
|
||||||
|
*/
|
||||||
|
static async encryptSymmetric({
|
||||||
|
workspaceId,
|
||||||
|
plaintext
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
plaintext: string;
|
||||||
|
}) {
|
||||||
|
return await encryptSymmetricHelper({
|
||||||
|
workspaceId,
|
||||||
|
plaintext
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return symmetrically decrypted [ciphertext] using the
|
||||||
|
* bot's copy of the workspace key for workspace with id [workspaceId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||||
|
* @param {String} obj.iv - iv
|
||||||
|
* @param {String} obj.tag - tag
|
||||||
|
*/
|
||||||
|
static async decryptSymmetric({
|
||||||
|
workspaceId,
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
ciphertext: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
}) {
|
||||||
|
return await decryptSymmetricHelper({
|
||||||
|
workspaceId,
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BotService;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Bot, IBot } from '../models';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { handleEventHelper } from '../helpers/event';
|
||||||
|
|
||||||
|
interface Event {
|
||||||
|
name: string;
|
||||||
|
workspaceId: string;
|
||||||
|
payload: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to handle events.
|
||||||
|
*/
|
||||||
|
class EventService {
|
||||||
|
/**
|
||||||
|
* Handle event [event]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Event} obj.event - an event
|
||||||
|
* @param {String} obj.event.name - name of event
|
||||||
|
* @param {String} obj.event.workspaceId - id of workspace that event is part of
|
||||||
|
* @param {Object} obj.event.payload - payload of event (depends on event)
|
||||||
|
*/
|
||||||
|
static async handleEvent({ event }: { event: Event }): Promise<void> {
|
||||||
|
await handleEventHelper({
|
||||||
|
event
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventService;
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Integration
|
||||||
|
} from '../models';
|
||||||
|
import {
|
||||||
|
handleOAuthExchangeHelper,
|
||||||
|
syncIntegrationsHelper,
|
||||||
|
getIntegrationAuthRefreshHelper,
|
||||||
|
getIntegrationAuthAccessHelper,
|
||||||
|
setIntegrationAuthRefreshHelper,
|
||||||
|
setIntegrationAuthAccessHelper,
|
||||||
|
} from '../helpers/integration';
|
||||||
|
import { exchangeCode } from '../integrations';
|
||||||
|
import {
|
||||||
|
ENV_DEV,
|
||||||
|
EVENT_PUSH_SECRETS
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
// should sync stuff be here too? Probably.
|
||||||
|
// TODO: move bot functions to IntegrationService.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to handle integrations
|
||||||
|
*/
|
||||||
|
class IntegrationService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
|
||||||
|
* named [integration]
|
||||||
|
* - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
|
||||||
|
* - Add placeholder inactive integration
|
||||||
|
* - Create bot sequence for integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @param {String} obj.integration - name of integration
|
||||||
|
* @param {String} obj.code - code
|
||||||
|
*/
|
||||||
|
static async handleOAuthExchange({
|
||||||
|
workspaceId,
|
||||||
|
integration,
|
||||||
|
code
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
integration: string;
|
||||||
|
code: string;
|
||||||
|
}) {
|
||||||
|
await handleOAuthExchangeHelper({
|
||||||
|
workspaceId,
|
||||||
|
integration,
|
||||||
|
code
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync/push environment variables in workspace with id [workspaceId] to
|
||||||
|
* all associated integrations
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.workspaceId - id of workspace
|
||||||
|
*/
|
||||||
|
static async syncIntegrations({
|
||||||
|
workspaceId
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
}) {
|
||||||
|
return await syncIntegrationsHelper({
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return decrypted refresh token for integration auth
|
||||||
|
* with id [integrationAuthId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @param {String} refreshToken - decrypted refresh token
|
||||||
|
*/
|
||||||
|
static async getIntegrationAuthRefresh({ integrationAuthId }: { integrationAuthId: string}) {
|
||||||
|
return await getIntegrationAuthRefreshHelper({
|
||||||
|
integrationAuthId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return decrypted access token for integration auth
|
||||||
|
* with id [integrationAuthId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @param {String} accessToken - decrypted access token
|
||||||
|
*/
|
||||||
|
static async getIntegrationAuthAccess({ integrationAuthId }: { integrationAuthId: string}) {
|
||||||
|
return await getIntegrationAuthAccessHelper({
|
||||||
|
integrationAuthId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt refresh token [refreshToken] using the bot's copy
|
||||||
|
* of the workspace key for workspace belonging to integration auth
|
||||||
|
* with id [integrationAuthId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @param {String} obj.refreshToken - refresh token
|
||||||
|
* @returns {IntegrationAuth} integrationAuth - updated integration auth
|
||||||
|
*/
|
||||||
|
static async setIntegrationAuthRefresh({
|
||||||
|
integrationAuthId,
|
||||||
|
refreshToken
|
||||||
|
}: {
|
||||||
|
integrationAuthId: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}) {
|
||||||
|
return await setIntegrationAuthRefreshHelper({
|
||||||
|
integrationAuthId,
|
||||||
|
refreshToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt access token [accessToken] using the bot's copy
|
||||||
|
* of the workspace key for workspace belonging to integration auth
|
||||||
|
* with id [integrationAuthId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @param {String} obj.accessToken - access token
|
||||||
|
* @param {String} obj.accessExpiresAt - expiration date of access token
|
||||||
|
* @returns {IntegrationAuth} - updated integration auth
|
||||||
|
*/
|
||||||
|
static async setIntegrationAuthAccess({
|
||||||
|
integrationAuthId,
|
||||||
|
accessToken,
|
||||||
|
accessExpiresAt
|
||||||
|
}: {
|
||||||
|
integrationAuthId: string;
|
||||||
|
accessToken: string;
|
||||||
|
accessExpiresAt: Date;
|
||||||
|
}) {
|
||||||
|
return await setIntegrationAuthAccessHelper({
|
||||||
|
integrationAuthId,
|
||||||
|
accessToken,
|
||||||
|
accessExpiresAt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IntegrationService;
|
||||||
@@ -1,15 +1,27 @@
|
|||||||
import { PostHog } from 'posthog-node';
|
import { PostHog } from 'posthog-node';
|
||||||
import { NODE_ENV, POSTHOG_HOST, POSTHOG_PROJECT_API_KEY, TELEMETRY_ENABLED } from '../config';
|
import {
|
||||||
|
NODE_ENV,
|
||||||
|
POSTHOG_HOST,
|
||||||
|
POSTHOG_PROJECT_API_KEY,
|
||||||
|
TELEMETRY_ENABLED
|
||||||
|
} from '../config';
|
||||||
|
import { getLogger } from '../utils/logger';
|
||||||
|
|
||||||
let postHogClient: any;
|
if(TELEMETRY_ENABLED){
|
||||||
if (
|
getLogger("backend-main").info([
|
||||||
NODE_ENV === 'production'
|
"",
|
||||||
&& TELEMETRY_ENABLED
|
"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.",
|
||||||
// case: enable opt-out telemetry in production
|
"To opt out of telemetry, you can set `TELEMETRY_ENABLED=false` within the environment variables",
|
||||||
postHogClient = new PostHog(POSTHOG_PROJECT_API_KEY, {
|
].join('\n'))
|
||||||
host: POSTHOG_HOST
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default postHogClient;
|
let postHogClient: any;
|
||||||
|
if (NODE_ENV === 'production' && TELEMETRY_ENABLED) {
|
||||||
|
// case: enable opt-out telemetry in production
|
||||||
|
postHogClient = new PostHog(POSTHOG_PROJECT_API_KEY, {
|
||||||
|
host: POSTHOG_HOST
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default postHogClient;
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { getLogger } from '../utils/logger';
|
||||||
|
|
||||||
|
export const initDatabase = (MONGO_URL: string) => {
|
||||||
|
mongoose
|
||||||
|
.connect(MONGO_URL)
|
||||||
|
.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;
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { createTerminus } from '@godaddy/terminus';
|
||||||
|
import { getLogger } from '../utils/logger';
|
||||||
|
|
||||||
|
export const setUpHealthEndpoint = <T>(server: T) => {
|
||||||
|
const onSignal = () => {
|
||||||
|
getLogger('backend-main').info('Server is starting clean-up');
|
||||||
|
return Promise.all([
|
||||||
|
new Promise((resolve) => {
|
||||||
|
if (mongoose.connection && mongoose.connection.readyState == 1) {
|
||||||
|
mongoose.connection.close()
|
||||||
|
.then(() => resolve('Database connection closed'));
|
||||||
|
} else {
|
||||||
|
resolve('Database connection already closed');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const healthCheck = () => {
|
||||||
|
// `state.isShuttingDown` (boolean) shows whether the server is shutting down or not
|
||||||
|
// optionally include a resolve value to be included as info in the health check response
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
createTerminus(server, {
|
||||||
|
healthChecks: {
|
||||||
|
'/healthcheck': healthCheck,
|
||||||
|
onSignal
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
import postHogClient from './PostHogClient';
|
import postHogClient from './PostHogClient';
|
||||||
|
import BotService from './BotService';
|
||||||
|
import EventService from './EventService';
|
||||||
|
import IntegrationService from './IntegrationService';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
postHogClient
|
postHogClient,
|
||||||
|
BotService,
|
||||||
|
EventService,
|
||||||
|
IntegrationService
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
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,
|
||||||
|
port: SMTP_PORT as number
|
||||||
|
};
|
||||||
|
|
||||||
|
if (SMTP_USERNAME && SMTP_PASSWORD) {
|
||||||
|
mailOpts.auth = {
|
||||||
|
user: SMTP_USERNAME,
|
||||||
|
pass: 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
|
||||||
|
.verify()
|
||||||
|
.then(() => {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureMessage('SMTP - Successfully connected');
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(
|
||||||
|
`SMTP - Failed to connect to ${SMTP_HOST}:${SMTP_PORT} \n\t${err}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return transporter;
|
||||||
|
};
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
<title>Email Verification</title>
|
<title>Organization Invitation</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>Infisical</h2>
|
<h2>Infisical</h2>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>Account Recovery</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Infisical</h2>
|
||||||
|
<h2>Reset your password</h2>
|
||||||
|
<p>Someone requested a password reset.</p>
|
||||||
|
<a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a>
|
||||||
|
<p>If you didn't initiate this request, please contact us immediately at [email protected]</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
<title>Email Verification</title>
|
<title>Project Invitation</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>Infisical</h2>
|
<h2>Infisical</h2>
|
||||||
|
|||||||
Vendored
+2
@@ -1,5 +1,6 @@
|
|||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
|
|
||||||
|
|
||||||
// TODO: fix (any) types
|
// TODO: fix (any) types
|
||||||
declare global {
|
declare global {
|
||||||
namespace Express {
|
namespace Express {
|
||||||
@@ -11,6 +12,7 @@ declare global {
|
|||||||
membershipOrg: any;
|
membershipOrg: any;
|
||||||
integration: any;
|
integration: any;
|
||||||
integrationAuth: any;
|
integrationAuth: any;
|
||||||
|
bot: any;
|
||||||
serviceToken: any;
|
serviceToken: any;
|
||||||
accessToken: any;
|
accessToken: any;
|
||||||
query?: any;
|
query?: any;
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
import nacl from 'tweetnacl';
|
import nacl from 'tweetnacl';
|
||||||
import util from 'tweetnacl-util';
|
import util from 'tweetnacl-util';
|
||||||
import AesGCM from './aes-gcm';
|
import AesGCM from './aes-gcm';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new base64, NaCl, public-private key pair.
|
||||||
|
* @returns {Object} obj
|
||||||
|
* @returns {String} obj.publicKey - base64, NaCl, public key
|
||||||
|
* @returns {String} obj.privateKey - base64, NaCl, private key
|
||||||
|
*/
|
||||||
|
const generateKeyPair = () => {
|
||||||
|
const pair = nacl.box.keyPair();
|
||||||
|
|
||||||
|
return ({
|
||||||
|
publicKey: util.encodeBase64(pair.publicKey),
|
||||||
|
privateKey: util.encodeBase64(pair.secretKey)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return assymmetrically encrypted [plaintext] using [publicKey] where
|
* Return assymmetrically encrypted [plaintext] using [publicKey] where
|
||||||
@@ -32,6 +48,8 @@ const encryptAsymmetric = ({
|
|||||||
util.decodeBase64(privateKey)
|
util.decodeBase64(privateKey)
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to perform asymmetric encryption');
|
throw new Error('Failed to perform asymmetric encryption');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +89,8 @@ const decryptAsymmetric = ({
|
|||||||
util.decodeBase64(privateKey)
|
util.decodeBase64(privateKey)
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to perform asymmetric decryption');
|
throw new Error('Failed to perform asymmetric decryption');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +101,7 @@ const decryptAsymmetric = ({
|
|||||||
* Return symmetrically encrypted [plaintext] using [key].
|
* Return symmetrically encrypted [plaintext] using [key].
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String} obj.plaintext - plaintext to encrypt
|
* @param {String} obj.plaintext - plaintext to encrypt
|
||||||
* @param {String} obj.key - 16-byte hex key
|
* @param {String} obj.key - hex key
|
||||||
*/
|
*/
|
||||||
const encryptSymmetric = ({
|
const encryptSymmetric = ({
|
||||||
plaintext,
|
plaintext,
|
||||||
@@ -97,6 +117,8 @@ const encryptSymmetric = ({
|
|||||||
iv = obj.iv;
|
iv = obj.iv;
|
||||||
tag = obj.tag;
|
tag = obj.tag;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to perform symmetric encryption');
|
throw new Error('Failed to perform symmetric encryption');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +136,7 @@ const encryptSymmetric = ({
|
|||||||
* @param {String} obj.ciphertext - ciphertext to decrypt
|
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||||
* @param {String} obj.iv - iv
|
* @param {String} obj.iv - iv
|
||||||
* @param {String} obj.tag - tag
|
* @param {String} obj.tag - tag
|
||||||
* @param {String} obj.key - 32-byte hex key
|
* @param {String} obj.key - hex key
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const decryptSymmetric = ({
|
const decryptSymmetric = ({
|
||||||
@@ -132,6 +154,8 @@ const decryptSymmetric = ({
|
|||||||
try {
|
try {
|
||||||
plaintext = AesGCM.decrypt(ciphertext, iv, tag, key);
|
plaintext = AesGCM.decrypt(ciphertext, iv, tag, key);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to perform symmetric decryption');
|
throw new Error('Failed to perform symmetric decryption');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +163,7 @@ const decryptSymmetric = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
generateKeyPair,
|
||||||
encryptAsymmetric,
|
encryptAsymmetric,
|
||||||
decryptAsymmetric,
|
decryptAsymmetric,
|
||||||
encryptSymmetric,
|
encryptSymmetric,
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import RequestError, { LogLevel, RequestErrorContext } from "./requestError"
|
||||||
|
|
||||||
|
//* ----->[GENERAL HTTP ERRORS]<-----
|
||||||
|
export const RouteNotFoundError = (error?: Partial<RequestErrorContext>) => 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<RequestErrorContext>) => 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<RequestErrorContext>) => 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<RequestErrorContext>) => 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<RequestErrorContext>) => 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<RequestErrorContext>) => 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<RequestErrorContext>) => 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<RequestErrorContext>) => 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<RequestErrorContext>) => 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<RequestErrorContext>) => 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<RequestErrorContext>) => 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<RequestErrorContext>) => 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]<-----
|
||||||
@@ -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]
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
Original work Copyright (c) 2016, Nikolay Nemshilov <nemshilov@gmail.com>
|
||||||
|
Modified work Copyright (c) 2016, David Banham <david@banham.id.au>
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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<string, unknown>,
|
||||||
|
stack?: string|undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class RequestError extends Error{
|
||||||
|
|
||||||
|
private _logLevel: LogLevel
|
||||||
|
private _logName: string
|
||||||
|
statusCode: number
|
||||||
|
type: string
|
||||||
|
context: Record<string, unknown>
|
||||||
|
extra: Record<string, string|number|symbol>[]
|
||||||
|
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<string, string|boolean|number>[]){
|
||||||
|
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
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
// membership roles
|
|
||||||
const OWNER = 'owner';
|
|
||||||
const ADMIN = 'admin';
|
|
||||||
const MEMBER = 'member';
|
|
||||||
|
|
||||||
// membership statuses
|
|
||||||
const INVITED = 'invited';
|
|
||||||
|
|
||||||
// -- organization
|
|
||||||
const ACCEPTED = 'accepted';
|
|
||||||
|
|
||||||
// -- workspace
|
|
||||||
const COMPLETED = 'completed';
|
|
||||||
const GRANTED = 'granted';
|
|
||||||
|
|
||||||
// subscriptions
|
|
||||||
const PLAN_STARTER = 'starter';
|
|
||||||
const PLAN_PRO = 'pro';
|
|
||||||
|
|
||||||
// secrets
|
|
||||||
const SECRET_SHARED = 'shared';
|
|
||||||
const SECRET_PERSONAL = 'personal';
|
|
||||||
|
|
||||||
// environments
|
|
||||||
const ENV_DEV = 'dev';
|
|
||||||
const ENV_TESTING = 'test';
|
|
||||||
const ENV_STAGING = 'staging';
|
|
||||||
const ENV_PROD = 'prod';
|
|
||||||
const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]);
|
|
||||||
|
|
||||||
// integrations
|
|
||||||
const INTEGRATION_HEROKU = 'heroku';
|
|
||||||
const INTEGRATION_NETLIFY = 'netlify';
|
|
||||||
const INTEGRATION_SET = new Set([INTEGRATION_HEROKU, INTEGRATION_NETLIFY]);
|
|
||||||
|
|
||||||
// integration types
|
|
||||||
const INTEGRATION_OAUTH2 = 'oauth2';
|
|
||||||
|
|
||||||
export {
|
|
||||||
OWNER,
|
|
||||||
ADMIN,
|
|
||||||
MEMBER,
|
|
||||||
INVITED,
|
|
||||||
ACCEPTED,
|
|
||||||
COMPLETED,
|
|
||||||
GRANTED,
|
|
||||||
PLAN_STARTER,
|
|
||||||
PLAN_PRO,
|
|
||||||
SECRET_SHARED,
|
|
||||||
SECRET_PERSONAL,
|
|
||||||
ENV_DEV,
|
|
||||||
ENV_TESTING,
|
|
||||||
ENV_STAGING,
|
|
||||||
ENV_PROD,
|
|
||||||
ENV_SET,
|
|
||||||
INTEGRATION_HEROKU,
|
|
||||||
INTEGRATION_NETLIFY,
|
|
||||||
INTEGRATION_SET,
|
|
||||||
INTEGRATION_OAUTH2
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// environments
|
||||||
|
const ENV_DEV = 'dev';
|
||||||
|
const ENV_TESTING = 'test';
|
||||||
|
const ENV_STAGING = 'staging';
|
||||||
|
const ENV_PROD = 'prod';
|
||||||
|
const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]);
|
||||||
|
|
||||||
|
export {
|
||||||
|
ENV_DEV,
|
||||||
|
ENV_TESTING,
|
||||||
|
ENV_STAGING,
|
||||||
|
ENV_PROD,
|
||||||
|
ENV_SET
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const EVENT_PUSH_SECRETS = 'pushSecrets';
|
||||||
|
const EVENT_PULL_SECRETS = 'pullSecrets';
|
||||||
|
|
||||||
|
export {
|
||||||
|
EVENT_PUSH_SECRETS,
|
||||||
|
EVENT_PULL_SECRETS
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import {
|
||||||
|
ENV_DEV,
|
||||||
|
ENV_TESTING,
|
||||||
|
ENV_STAGING,
|
||||||
|
ENV_PROD,
|
||||||
|
ENV_SET
|
||||||
|
} from './environment';
|
||||||
|
import {
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY,
|
||||||
|
INTEGRATION_GITHUB,
|
||||||
|
INTEGRATION_SET,
|
||||||
|
INTEGRATION_OAUTH2,
|
||||||
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
|
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||||
|
INTEGRATION_GITHUB_TOKEN_URL,
|
||||||
|
INTEGRATION_HEROKU_API_URL,
|
||||||
|
INTEGRATION_VERCEL_API_URL,
|
||||||
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
|
INTEGRATION_GITHUB_API_URL,
|
||||||
|
INTEGRATION_OPTIONS
|
||||||
|
} from './integration';
|
||||||
|
import {
|
||||||
|
OWNER,
|
||||||
|
ADMIN,
|
||||||
|
MEMBER,
|
||||||
|
INVITED,
|
||||||
|
ACCEPTED,
|
||||||
|
COMPLETED,
|
||||||
|
GRANTED
|
||||||
|
} from './organization';
|
||||||
|
import { SECRET_SHARED, SECRET_PERSONAL } from './secret';
|
||||||
|
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event';
|
||||||
|
import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp';
|
||||||
|
import { PLAN_STARTER, PLAN_PRO } from './stripe';
|
||||||
|
|
||||||
|
export {
|
||||||
|
OWNER,
|
||||||
|
ADMIN,
|
||||||
|
MEMBER,
|
||||||
|
INVITED,
|
||||||
|
ACCEPTED,
|
||||||
|
COMPLETED,
|
||||||
|
GRANTED,
|
||||||
|
SECRET_SHARED,
|
||||||
|
SECRET_PERSONAL,
|
||||||
|
ENV_DEV,
|
||||||
|
ENV_TESTING,
|
||||||
|
ENV_STAGING,
|
||||||
|
ENV_PROD,
|
||||||
|
ENV_SET,
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY,
|
||||||
|
INTEGRATION_GITHUB,
|
||||||
|
INTEGRATION_SET,
|
||||||
|
INTEGRATION_OAUTH2,
|
||||||
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
|
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||||
|
INTEGRATION_GITHUB_TOKEN_URL,
|
||||||
|
INTEGRATION_HEROKU_API_URL,
|
||||||
|
INTEGRATION_VERCEL_API_URL,
|
||||||
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
|
INTEGRATION_GITHUB_API_URL,
|
||||||
|
EVENT_PUSH_SECRETS,
|
||||||
|
EVENT_PULL_SECRETS,
|
||||||
|
INTEGRATION_OPTIONS,
|
||||||
|
SMTP_HOST_SENDGRID,
|
||||||
|
SMTP_HOST_MAILGUN,
|
||||||
|
PLAN_STARTER,
|
||||||
|
PLAN_PRO,
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user