Merge branch 'main' into language-support-ptbr
36
.github/values.yaml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
frontend:
|
||||||
|
replicaCount: 1
|
||||||
|
image:
|
||||||
|
repository:
|
||||||
|
pullPolicy: Always
|
||||||
|
tag: "latest"
|
||||||
|
kubeSecretRef: managed-secret-frontend
|
||||||
|
|
||||||
|
backend:
|
||||||
|
replicaCount: 1
|
||||||
|
image:
|
||||||
|
repository:
|
||||||
|
pullPolicy: Always
|
||||||
|
tag: "latest"
|
||||||
|
kubeSecretRef: managed-backend-secret
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: "nginx"
|
||||||
|
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||||
|
hostName: gamma.infisical.com
|
||||||
|
frontend:
|
||||||
|
path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
path: /api
|
||||||
|
pathType: Prefix
|
||||||
|
tls:
|
||||||
|
- secretName: echo-tls
|
||||||
|
hosts:
|
||||||
|
- gamma.infisical.com
|
||||||
|
|
||||||
|
backendEnvironmentVariables:
|
||||||
|
|
||||||
|
frontendEnvironmentVariables:
|
||||||
70
.github/workflows/docker-image.yml
vendored
@@ -1,5 +1,4 @@
|
|||||||
name: Push to Docker Hub
|
name: Build, Publish and Deploy to Gamma
|
||||||
|
|
||||||
on: [workflow_dispatch]
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -10,8 +9,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: ☁️ Checkout source
|
- name: ☁️ Checkout source
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: 🔧 Set up QEMU
|
- name: Save commit hashes for tag
|
||||||
uses: docker/setup-qemu-action@v2
|
id: commit
|
||||||
|
uses: pr-mpt/actions-commit-hash@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
|
||||||
@@ -19,9 +19,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Set up Depot CLI
|
||||||
|
uses: depot/setup-action@v1
|
||||||
- name: 📦 Build backend and export to Docker
|
- name: 📦 Build backend and export to Docker
|
||||||
uses: docker/build-push-action@v3
|
uses: depot/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
|
project: 64mmf0n610
|
||||||
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
load: true
|
load: true
|
||||||
context: backend
|
context: backend
|
||||||
tags: infisical/backend:test
|
tags: infisical/backend:test
|
||||||
@@ -35,11 +39,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker compose -f .github/resources/docker-compose.be-test.yml down
|
docker compose -f .github/resources/docker-compose.be-test.yml down
|
||||||
- name: 🏗️ Build backend and push
|
- name: 🏗️ Build backend and push
|
||||||
uses: docker/build-push-action@v3
|
uses: depot/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
|
project: 64mmf0n610
|
||||||
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
push: true
|
push: true
|
||||||
context: backend
|
context: backend
|
||||||
tags: infisical/backend:latest
|
tags: infisical/backend:${{ steps.commit.outputs.short }},
|
||||||
|
infisical/backend:latest
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
frontend-image:
|
frontend-image:
|
||||||
@@ -49,8 +56,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: ☁️ Checkout source
|
- name: ☁️ Checkout source
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: 🔧 Set up QEMU
|
- name: Save commit hashes for tag
|
||||||
uses: docker/setup-qemu-action@v2
|
id: commit
|
||||||
|
uses: pr-mpt/actions-commit-hash@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
|
||||||
@@ -58,10 +66,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Set up Depot CLI
|
||||||
|
uses: depot/setup-action@v1
|
||||||
- name: 📦 Build frontend and export to Docker
|
- name: 📦 Build frontend and export to Docker
|
||||||
uses: docker/build-push-action@v3
|
uses: depot/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
load: true
|
load: true
|
||||||
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
|
project: 64mmf0n610
|
||||||
context: frontend
|
context: frontend
|
||||||
tags: infisical/frontend:test
|
tags: infisical/frontend:test
|
||||||
build-args: |
|
build-args: |
|
||||||
@@ -76,11 +88,45 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker stop infisical-frontend-test
|
docker stop infisical-frontend-test
|
||||||
- name: 🏗️ Build frontend and push
|
- name: 🏗️ Build frontend and push
|
||||||
uses: docker/build-push-action@v3
|
uses: depot/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
|
project: 64mmf0n610
|
||||||
push: true
|
push: true
|
||||||
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
context: frontend
|
context: frontend
|
||||||
tags: infisical/frontend:latest
|
tags: infisical/frontend:${{ steps.commit.outputs.short }},
|
||||||
|
infisical/frontend:latest
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
build-args: |
|
build-args: |
|
||||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||||
|
gamma-deployment:
|
||||||
|
name: Deploy to gamma
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [frontend-image, backend-image]
|
||||||
|
steps:
|
||||||
|
- name: ☁️ Checkout source
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Install Helm
|
||||||
|
uses: azure/setup-helm@v3
|
||||||
|
with:
|
||||||
|
version: v3.10.0
|
||||||
|
- name: Install infisical helm chart
|
||||||
|
run: |
|
||||||
|
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
|
||||||
|
helm repo update
|
||||||
|
- name: Install kubectl
|
||||||
|
uses: azure/setup-kubectl@v3
|
||||||
|
- name: Install doctl
|
||||||
|
uses: digitalocean/action-doctl@v2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
||||||
|
- name: Save DigitalOcean kubeconfig with short-lived credentials
|
||||||
|
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 k8s-1-25-4-do-0-nyc1-1670645170179
|
||||||
|
- name: switch to gamma namespace
|
||||||
|
run: kubectl config set-context --current --namespace=gamma
|
||||||
|
- name: test kubectl
|
||||||
|
run: kubectl get ingress
|
||||||
|
- name: Download helm values to file and upgrade gamma deploy
|
||||||
|
run: |
|
||||||
|
wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml
|
||||||
|
helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --recreate-pods
|
||||||
2
.github/workflows/release_build.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Go releaser
|
name: Build and release CLI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|||||||
3
Makefile
@@ -7,6 +7,9 @@ push:
|
|||||||
up-dev:
|
up-dev:
|
||||||
docker-compose -f docker-compose.dev.yml up --build
|
docker-compose -f docker-compose.dev.yml up --build
|
||||||
|
|
||||||
|
i-dev:
|
||||||
|
infisical export && infisical export > .env && docker-compose -f docker-compose.dev.yml up --build
|
||||||
|
|
||||||
up-prod:
|
up-prod:
|
||||||
docker-compose -f docker-compose.yml up --build
|
docker-compose -f docker-compose.yml up --build
|
||||||
|
|
||||||
|
|||||||
@@ -333,10 +333,10 @@ Infisical officially launched as v.1.0 on November 21st, 2022. There are a lot o
|
|||||||
<!-- 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/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/jon4hz"><img src="https://avatars.githubusercontent.com/u/26183582?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/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?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>
|
<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/JoaoVictor6"><img src="https://avatars.githubusercontent.com/u/68869379?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mocherfaoui"><img src="https://avatars.githubusercontent.com/u/37941426?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jon4hz"><img src="https://avatars.githubusercontent.com/u/26183582?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/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?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>
|
||||||
|
|
||||||
## 🌎 Translations
|
## 🌎 Translations
|
||||||
|
|
||||||
Infisical is currently aviable in English and Korean. Help us translate Infisical to your language!
|
Infisical is currently available in English and Korean. Help us translate Infisical to your language!
|
||||||
|
|
||||||
You can find all the info in [this issue](https://github.com/Infisical/infisical/issues/181).
|
You can find all the info in [this issue](https://github.com/Infisical/infisical/issues/181).
|
||||||
2568
backend/api-documentation.json
Normal file
82
backend/package-lock.json
generated
@@ -37,6 +37,8 @@
|
|||||||
"query-string": "^7.1.3",
|
"query-string": "^7.1.3",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"stripe": "^10.7.0",
|
"stripe": "^10.7.0",
|
||||||
|
"swagger-autogen": "^2.22.0",
|
||||||
|
"swagger-ui-express": "^4.6.0",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
"tweetnacl-util": "^0.15.1",
|
"tweetnacl-util": "^0.15.1",
|
||||||
"typescript": "^4.9.3",
|
"typescript": "^4.9.3",
|
||||||
@@ -4549,7 +4551,6 @@
|
|||||||
"version": "4.2.2",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
|
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -6690,7 +6691,6 @@
|
|||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"json5": "lib/cli.js"
|
"json5": "lib/cli.js"
|
||||||
},
|
},
|
||||||
@@ -11174,6 +11174,47 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/swagger-autogen": {
|
||||||
|
"version": "2.22.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.22.0.tgz",
|
||||||
|
"integrity": "sha512-MPdtwgx/RL3og0RjFVV9hPoQv3x+c3ZRhS0Vjp9k94DLV7iUgIuCg8H+uAT8oD5w48ATTRT1VjcOHlCGH62pdA==",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^7.4.1",
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
|
"glob": "^7.1.7",
|
||||||
|
"json5": "^2.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-autogen/node_modules/acorn": {
|
||||||
|
"version": "7.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||||
|
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
||||||
|
"bin": {
|
||||||
|
"acorn": "bin/acorn"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-ui-dist": {
|
||||||
|
"version": "4.15.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz",
|
||||||
|
"integrity": "sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA=="
|
||||||
|
},
|
||||||
|
"node_modules/swagger-ui-express": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-ZxpQFp1JR2RF8Ar++CyJzEDdvufa08ujNUJgMVTMWPi86CuQeVdBtvaeO/ysrz6dJAYXf9kbVNhWD7JWocwqsA==",
|
||||||
|
"dependencies": {
|
||||||
|
"swagger-ui-dist": ">=4.11.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= v0.10.32"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tar": {
|
"node_modules/tar": {
|
||||||
"version": "6.1.13",
|
"version": "6.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz",
|
||||||
@@ -15582,8 +15623,7 @@
|
|||||||
"deepmerge": {
|
"deepmerge": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
|
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"delayed-stream": {
|
"delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -17200,8 +17240,7 @@
|
|||||||
"json5": {
|
"json5": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"jsonwebtoken": {
|
"jsonwebtoken": {
|
||||||
"version": "9.0.0",
|
"version": "9.0.0",
|
||||||
@@ -20424,6 +20463,37 @@
|
|||||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"swagger-autogen": {
|
||||||
|
"version": "2.22.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.22.0.tgz",
|
||||||
|
"integrity": "sha512-MPdtwgx/RL3og0RjFVV9hPoQv3x+c3ZRhS0Vjp9k94DLV7iUgIuCg8H+uAT8oD5w48ATTRT1VjcOHlCGH62pdA==",
|
||||||
|
"requires": {
|
||||||
|
"acorn": "^7.4.1",
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
|
"glob": "^7.1.7",
|
||||||
|
"json5": "^2.2.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": {
|
||||||
|
"version": "7.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||||
|
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"swagger-ui-dist": {
|
||||||
|
"version": "4.15.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz",
|
||||||
|
"integrity": "sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA=="
|
||||||
|
},
|
||||||
|
"swagger-ui-express": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-ZxpQFp1JR2RF8Ar++CyJzEDdvufa08ujNUJgMVTMWPi86CuQeVdBtvaeO/ysrz6dJAYXf9kbVNhWD7JWocwqsA==",
|
||||||
|
"requires": {
|
||||||
|
"swagger-ui-dist": ">=4.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tar": {
|
"tar": {
|
||||||
"version": "6.1.13",
|
"version": "6.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz",
|
||||||
|
|||||||
@@ -1,46 +1,11 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
|
||||||
"@godaddy/terminus": "^4.11.2",
|
|
||||||
"@octokit/rest": "^19.0.5",
|
|
||||||
"@sentry/node": "^7.14.0",
|
|
||||||
"@sentry/tracing": "^7.19.0",
|
|
||||||
"@types/crypto-js": "^4.1.1",
|
|
||||||
"@types/libsodium-wrappers": "^0.7.10",
|
|
||||||
"await-to-js": "^3.0.0",
|
|
||||||
"axios": "^1.1.3",
|
|
||||||
"bcrypt": "^5.1.0",
|
|
||||||
"bigint-conversion": "^2.2.2",
|
|
||||||
"cookie-parser": "^1.4.6",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"crypto-js": "^4.1.1",
|
|
||||||
"dotenv": "^16.0.1",
|
|
||||||
"express": "^4.18.1",
|
|
||||||
"express-rate-limit": "^6.7.0",
|
|
||||||
"express-validator": "^6.14.2",
|
|
||||||
"handlebars": "^4.7.7",
|
|
||||||
"helmet": "^5.1.1",
|
|
||||||
"jsonwebtoken": "^9.0.0",
|
|
||||||
"jsrp": "^0.2.4",
|
|
||||||
"libsodium-wrappers": "^0.7.10",
|
|
||||||
"mongoose": "^6.7.2",
|
|
||||||
"nodemailer": "^6.8.0",
|
|
||||||
"posthog-node": "^2.2.2",
|
|
||||||
"query-string": "^7.1.3",
|
|
||||||
"rimraf": "^3.0.2",
|
|
||||||
"stripe": "^10.7.0",
|
|
||||||
"tweetnacl": "^1.0.3",
|
|
||||||
"tweetnacl-util": "^0.15.1",
|
|
||||||
"typescript": "^4.9.3",
|
|
||||||
"utility-types": "^3.10.0",
|
|
||||||
"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": {
|
||||||
"start": "npm run build && node build/index.js",
|
"start": "npm run build && node build/index.js",
|
||||||
"dev": "nodemon",
|
"dev": "nodemon",
|
||||||
|
"swagger-autogen": "node ./swagger.ts",
|
||||||
"build": "rimraf ./build && tsc && cp -R ./src/templates ./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",
|
||||||
@@ -108,5 +73,43 @@
|
|||||||
"suiteNameTemplate": "{filepath}",
|
"suiteNameTemplate": "{filepath}",
|
||||||
"classNameTemplate": "{classname}",
|
"classNameTemplate": "{classname}",
|
||||||
"titleTemplate": "{title}"
|
"titleTemplate": "{title}"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@godaddy/terminus": "^4.11.2",
|
||||||
|
"@octokit/rest": "^19.0.5",
|
||||||
|
"@sentry/node": "^7.14.0",
|
||||||
|
"@sentry/tracing": "^7.19.0",
|
||||||
|
"@types/crypto-js": "^4.1.1",
|
||||||
|
"@types/libsodium-wrappers": "^0.7.10",
|
||||||
|
"await-to-js": "^3.0.0",
|
||||||
|
"axios": "^1.1.3",
|
||||||
|
"bcrypt": "^5.1.0",
|
||||||
|
"bigint-conversion": "^2.2.2",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"crypto-js": "^4.1.1",
|
||||||
|
"dotenv": "^16.0.1",
|
||||||
|
"express": "^4.18.1",
|
||||||
|
"express-rate-limit": "^6.7.0",
|
||||||
|
"express-validator": "^6.14.2",
|
||||||
|
"handlebars": "^4.7.7",
|
||||||
|
"helmet": "^5.1.1",
|
||||||
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
"jsrp": "^0.2.4",
|
||||||
|
"libsodium-wrappers": "^0.7.10",
|
||||||
|
"mongoose": "^6.7.2",
|
||||||
|
"nodemailer": "^6.8.0",
|
||||||
|
"posthog-node": "^2.2.2",
|
||||||
|
"query-string": "^7.1.3",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"stripe": "^10.7.0",
|
||||||
|
"swagger-autogen": "^2.22.0",
|
||||||
|
"swagger-ui-express": "^4.6.0",
|
||||||
|
"tweetnacl": "^1.0.3",
|
||||||
|
"tweetnacl-util": "^0.15.1",
|
||||||
|
"typescript": "^4.9.3",
|
||||||
|
"utility-types": "^3.10.0",
|
||||||
|
"winston": "^3.8.2",
|
||||||
|
"winston-loki": "^6.0.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const { patchRouterParam } = require('./utils/patchAsyncRoutes');
|
const { patchRouterParam } = require('./utils/patchAsyncRoutes');
|
||||||
|
|
||||||
import express from 'express';
|
import express, { Request, Response } from 'express';
|
||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
import swaggerUi = require('swagger-ui-express');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const swaggerFile = require('../api-documentation.json')
|
||||||
|
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
import { PORT, NODE_ENV, SITE_URL } from './config';
|
import { PORT, NODE_ENV, SITE_URL } from './config';
|
||||||
@@ -38,11 +42,15 @@ import {
|
|||||||
} from './routes/v1';
|
} from './routes/v1';
|
||||||
import {
|
import {
|
||||||
secret as v2SecretRouter,
|
secret as v2SecretRouter,
|
||||||
|
secrets as v2SecretsRouter,
|
||||||
workspace as v2WorkspaceRouter,
|
workspace as v2WorkspaceRouter,
|
||||||
serviceTokenData as v2ServiceTokenDataRouter,
|
serviceTokenData as v2ServiceTokenDataRouter,
|
||||||
apiKeyData as v2APIKeyDataRouter,
|
apiKeyData as v2APIKeyDataRouter,
|
||||||
|
environment as v2EnvironmentRouter,
|
||||||
} from './routes/v2';
|
} from './routes/v2';
|
||||||
|
|
||||||
|
import { healthCheck } from './routes/status';
|
||||||
|
|
||||||
import { getLogger } from './utils/logger';
|
import { getLogger } from './utils/logger';
|
||||||
import { RouteNotFoundError } from './utils/errors';
|
import { RouteNotFoundError } from './utils/errors';
|
||||||
import { requestErrorHandler } from './middleware/requestErrorHandler';
|
import { requestErrorHandler } from './middleware/requestErrorHandler';
|
||||||
@@ -89,18 +97,26 @@ app.use('/api/v1/membership', v1MembershipRouter);
|
|||||||
app.use('/api/v1/key', v1KeyRouter);
|
app.use('/api/v1/key', v1KeyRouter);
|
||||||
app.use('/api/v1/invite-org', v1InviteOrgRouter);
|
app.use('/api/v1/invite-org', v1InviteOrgRouter);
|
||||||
app.use('/api/v1/secret', v1SecretRouter);
|
app.use('/api/v1/secret', v1SecretRouter);
|
||||||
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate
|
app.use('/api/v1/service-token', v1ServiceTokenRouter); // stop supporting
|
||||||
app.use('/api/v1/password', v1PasswordRouter);
|
app.use('/api/v1/password', v1PasswordRouter);
|
||||||
app.use('/api/v1/stripe', v1StripeRouter);
|
app.use('/api/v1/stripe', v1StripeRouter);
|
||||||
app.use('/api/v1/integration', v1IntegrationRouter);
|
app.use('/api/v1/integration', v1IntegrationRouter);
|
||||||
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
||||||
|
|
||||||
// v2 routes
|
// v2 routes
|
||||||
app.use('/api/v2/workspace', v2WorkspaceRouter);
|
app.use('/api/v2/workspace', v2EnvironmentRouter);
|
||||||
app.use('/api/v2/secret', v2SecretRouter);
|
app.use('/api/v2/workspace', v2WorkspaceRouter); // TODO: turn into plural route
|
||||||
app.use('/api/v2/service-token', v2ServiceTokenDataRouter);
|
app.use('/api/v2/secret', v2SecretRouter); // stop supporting, TODO: revise
|
||||||
|
app.use('/api/v2/secrets', v2SecretsRouter);
|
||||||
|
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
|
||||||
app.use('/api/v2/api-key-data', v2APIKeyDataRouter);
|
app.use('/api/v2/api-key-data', v2APIKeyDataRouter);
|
||||||
|
|
||||||
|
// api docs
|
||||||
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
|
||||||
|
|
||||||
|
// Server status
|
||||||
|
app.use('/api', healthCheck)
|
||||||
|
|
||||||
//* Handle unrouted requests and respond with proper error message as well as status code
|
//* Handle unrouted requests and respond with proper error message as well as status code
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
if (res.headersSent) return next();
|
if (res.headersSent) return next();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ 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 { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../../variables';
|
import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
|
||||||
import { IntegrationService } from '../../services';
|
import { IntegrationService } from '../../services';
|
||||||
import { getApps, revokeAccess } from '../../integrations';
|
import { getApps, revokeAccess } from '../../integrations';
|
||||||
|
|
||||||
@@ -32,10 +32,16 @@ export const oAuthExchange = async (
|
|||||||
if (!INTEGRATION_SET.has(integration))
|
if (!INTEGRATION_SET.has(integration))
|
||||||
throw new Error('Failed to validate integration');
|
throw new Error('Failed to validate integration');
|
||||||
|
|
||||||
|
const environments = req.membership.workspace?.environments || [];
|
||||||
|
if(environments.length === 0){
|
||||||
|
throw new Error("Failed to get environments")
|
||||||
|
}
|
||||||
|
|
||||||
await IntegrationService.handleOAuthExchange({
|
await IntegrationService.handleOAuthExchange({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
integration,
|
integration,
|
||||||
code
|
code,
|
||||||
|
environment: environments[0].slug,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
|
|||||||
@@ -118,10 +118,11 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
invitee = await User.findOne({
|
invitee = await User.findOne({
|
||||||
email: inviteeEmail
|
email: inviteeEmail
|
||||||
});
|
}).select('+publicKey');
|
||||||
|
|
||||||
if (invitee) {
|
if (invitee) {
|
||||||
// case: invitee is an existing user
|
// case: invitee is an existing user
|
||||||
|
|
||||||
inviteeMembershipOrg = await MembershipOrg.findOne({
|
inviteeMembershipOrg = await MembershipOrg.findOne({
|
||||||
user: invitee._id,
|
user: invitee._id,
|
||||||
organization: organizationId
|
organization: organizationId
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
import { pushKeys } from '../../helpers/key';
|
import { pushKeys } from '../../helpers/key';
|
||||||
import { eventPushSecrets } from '../../events';
|
import { eventPushSecrets } from '../../events';
|
||||||
import { EventService } from '../../services';
|
import { EventService } from '../../services';
|
||||||
import { ENV_SET } from '../../variables';
|
|
||||||
import { postHogClient } from '../../services';
|
import { postHogClient } from '../../services';
|
||||||
|
|
||||||
interface PushSecret {
|
interface PushSecret {
|
||||||
@@ -44,7 +43,8 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
|||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
// validate environment
|
// validate environment
|
||||||
if (!ENV_SET.has(environment)) {
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
throw new Error('Failed to validate environment');
|
throw new Error('Failed to validate environment');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +116,8 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
|||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
// validate environment
|
// validate environment
|
||||||
if (!ENV_SET.has(environment)) {
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
throw new Error('Failed to validate environment');
|
throw new Error('Failed to validate environment');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +184,8 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
|||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
// validate environment
|
// validate environment
|
||||||
if (!ENV_SET.has(environment)) {
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
throw new Error('Failed to validate environment');
|
throw new Error('Failed to validate environment');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { ServiceToken } from '../../models';
|
import { ServiceToken } from '../../models';
|
||||||
import { createToken } from '../../helpers/auth';
|
import { createToken } from '../../helpers/auth';
|
||||||
import { ENV_SET } from '../../variables';
|
|
||||||
import { JWT_SERVICE_SECRET } from '../../config';
|
import { JWT_SERVICE_SECRET } from '../../config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,7 +35,8 @@ export const createServiceToken = async (req: Request, res: Response) => {
|
|||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
// validate environment
|
// validate environment
|
||||||
if (!ENV_SET.has(environment)) {
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
throw new Error('Failed to validate environment');
|
throw new Error('Failed to validate environment');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ export const createAPIKeyData = async (req: Request, res: Response) => {
|
|||||||
apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
|
apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(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({
|
||||||
|
|||||||
204
backend/src/controllers/v2/environmentController.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Secret,
|
||||||
|
ServiceToken,
|
||||||
|
Workspace,
|
||||||
|
Integration,
|
||||||
|
ServiceTokenData,
|
||||||
|
} from '../../models';
|
||||||
|
import { SecretVersion } from '../../ee/models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new workspace environment named [environmentName] under workspace with id
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const createWorkspaceEnvironment = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
const { environmentName, environmentSlug } = req.body;
|
||||||
|
try {
|
||||||
|
const workspace = await Workspace.findById(workspaceId).exec();
|
||||||
|
if (
|
||||||
|
!workspace ||
|
||||||
|
workspace?.environments.find(
|
||||||
|
({ name, slug }) => slug === environmentSlug || environmentName === name
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error('Failed to create workspace environment');
|
||||||
|
}
|
||||||
|
|
||||||
|
workspace?.environments.push({
|
||||||
|
name: environmentName.toLowerCase(),
|
||||||
|
slug: environmentSlug.toLowerCase(),
|
||||||
|
});
|
||||||
|
await workspace.save();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to create new workspace environment',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully created new environment',
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: {
|
||||||
|
name: environmentName,
|
||||||
|
slug: environmentSlug,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename workspace environment with new name and slug of a workspace with [workspaceId]
|
||||||
|
* Old slug [oldEnvironmentSlug] must be provided
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const renameWorkspaceEnvironment = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body;
|
||||||
|
try {
|
||||||
|
// user should pass both new slug and env name
|
||||||
|
if (!environmentSlug || !environmentName) {
|
||||||
|
throw new Error('Invalid environment given.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// atomic update the env to avoid conflict
|
||||||
|
const workspace = await Workspace.findById(workspaceId).exec();
|
||||||
|
if (!workspace) {
|
||||||
|
throw new Error('Failed to create workspace environment');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEnvExist = workspace.environments.some(
|
||||||
|
({ name, slug }) =>
|
||||||
|
slug !== oldEnvironmentSlug &&
|
||||||
|
(name === environmentName || slug === environmentSlug)
|
||||||
|
);
|
||||||
|
if (isEnvExist) {
|
||||||
|
throw new Error('Invalid environment given');
|
||||||
|
}
|
||||||
|
|
||||||
|
const envIndex = workspace?.environments.findIndex(
|
||||||
|
({ slug }) => slug === oldEnvironmentSlug
|
||||||
|
);
|
||||||
|
if (envIndex === -1) {
|
||||||
|
throw new Error('Invalid environment given');
|
||||||
|
}
|
||||||
|
|
||||||
|
workspace.environments[envIndex].name = environmentName.toLowerCase();
|
||||||
|
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
|
||||||
|
|
||||||
|
await workspace.save();
|
||||||
|
await Secret.updateMany(
|
||||||
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
|
{ environment: environmentSlug }
|
||||||
|
);
|
||||||
|
await SecretVersion.updateMany(
|
||||||
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
|
{ environment: environmentSlug }
|
||||||
|
);
|
||||||
|
await ServiceToken.updateMany(
|
||||||
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
|
{ environment: environmentSlug }
|
||||||
|
);
|
||||||
|
await ServiceTokenData.updateMany(
|
||||||
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
|
{ environment: environmentSlug }
|
||||||
|
);
|
||||||
|
await Integration.updateMany(
|
||||||
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
|
{ environment: environmentSlug }
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to update workspace environment',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully update environment',
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: {
|
||||||
|
name: environmentName,
|
||||||
|
slug: environmentSlug,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete workspace environment by [environmentSlug] of workspace [workspaceId] and do the clean up
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteWorkspaceEnvironment = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
const { environmentSlug } = req.body;
|
||||||
|
try {
|
||||||
|
// atomic update the env to avoid conflict
|
||||||
|
const workspace = await Workspace.findById(workspaceId).exec();
|
||||||
|
if (!workspace) {
|
||||||
|
throw new Error('Failed to create workspace environment');
|
||||||
|
}
|
||||||
|
|
||||||
|
const envIndex = workspace?.environments.findIndex(
|
||||||
|
({ slug }) => slug === environmentSlug
|
||||||
|
);
|
||||||
|
if (envIndex === -1) {
|
||||||
|
throw new Error('Invalid environment given');
|
||||||
|
}
|
||||||
|
|
||||||
|
workspace.environments.splice(envIndex, 1);
|
||||||
|
await workspace.save();
|
||||||
|
|
||||||
|
// clean up
|
||||||
|
await Secret.deleteMany({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: environmentSlug,
|
||||||
|
});
|
||||||
|
await SecretVersion.deleteMany({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: environmentSlug,
|
||||||
|
});
|
||||||
|
await ServiceToken.deleteMany({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: environmentSlug,
|
||||||
|
});
|
||||||
|
await ServiceTokenData.deleteMany({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: environmentSlug,
|
||||||
|
});
|
||||||
|
await Integration.deleteMany({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: environmentSlug,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to delete workspace environment',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully deleted environment',
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: environmentSlug,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -2,10 +2,14 @@ import * as workspaceController from './workspaceController';
|
|||||||
import * as serviceTokenDataController from './serviceTokenDataController';
|
import * as serviceTokenDataController from './serviceTokenDataController';
|
||||||
import * as apiKeyDataController from './apiKeyDataController';
|
import * as apiKeyDataController from './apiKeyDataController';
|
||||||
import * as secretController from './secretController';
|
import * as secretController from './secretController';
|
||||||
|
import * as secretsController from './secretsController';
|
||||||
|
import * as environmentController from './environmentController';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
workspaceController,
|
workspaceController,
|
||||||
serviceTokenDataController,
|
serviceTokenDataController,
|
||||||
apiKeyDataController,
|
apiKeyDataController,
|
||||||
secretController
|
secretController,
|
||||||
|
secretsController,
|
||||||
|
environmentController
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,68 @@ const { ValidationError } = mongoose.Error;
|
|||||||
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
|
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
|
||||||
import { AnyBulkWriteOperation } from 'mongodb';
|
import { AnyBulkWriteOperation } from 'mongodb';
|
||||||
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
|
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
|
||||||
|
import { postHogClient } from '../../services';
|
||||||
|
|
||||||
export const batchCreateSecrets = async (req: Request, res: Response) => {
|
/**
|
||||||
|
* Create secret for workspace with id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const createSecret = async (req: Request, res: Response) => {
|
||||||
|
const secretToCreate: CreateSecretRequestBody = req.body.secret;
|
||||||
|
const { workspaceId, environment } = req.params
|
||||||
|
const sanitizedSecret: SanitizedSecretForCreate = {
|
||||||
|
secretKeyCiphertext: secretToCreate.secretKeyCiphertext,
|
||||||
|
secretKeyIV: secretToCreate.secretKeyIV,
|
||||||
|
secretKeyTag: secretToCreate.secretKeyTag,
|
||||||
|
secretKeyHash: secretToCreate.secretKeyHash,
|
||||||
|
secretValueCiphertext: secretToCreate.secretValueCiphertext,
|
||||||
|
secretValueIV: secretToCreate.secretValueIV,
|
||||||
|
secretValueTag: secretToCreate.secretValueTag,
|
||||||
|
secretValueHash: secretToCreate.secretValueHash,
|
||||||
|
secretCommentCiphertext: secretToCreate.secretCommentCiphertext,
|
||||||
|
secretCommentIV: secretToCreate.secretCommentIV,
|
||||||
|
secretCommentTag: secretToCreate.secretCommentTag,
|
||||||
|
secretCommentHash: secretToCreate.secretCommentHash,
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
type: secretToCreate.type,
|
||||||
|
user: new Types.ObjectId(req.user._id)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const [error, secret] = await to(Secret.create(sanitizedSecret).then())
|
||||||
|
if (error instanceof ValidationError) {
|
||||||
|
throw RouteValidationError({ message: error.message, stack: error.stack })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets added',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: 1,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send({
|
||||||
|
secret
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create many secrets for workspace wiht id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const createSecrets = async (req: Request, res: Response) => {
|
||||||
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
|
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
|
||||||
const { workspaceId, environmentName } = req.params
|
const { workspaceId, environment } = req.params
|
||||||
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
|
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
|
||||||
|
|
||||||
secretsToCreate.forEach(rawSecret => {
|
secretsToCreate.forEach(rawSecret => {
|
||||||
@@ -28,7 +86,7 @@ export const batchCreateSecrets = async (req: Request, res: Response) => {
|
|||||||
secretCommentTag: rawSecret.secretCommentTag,
|
secretCommentTag: rawSecret.secretCommentTag,
|
||||||
secretCommentHash: rawSecret.secretCommentHash,
|
secretCommentHash: rawSecret.secretCommentHash,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
environment: environmentName,
|
environment,
|
||||||
type: rawSecret.type,
|
type: rawSecret.type,
|
||||||
user: new Types.ObjectId(req.user._id)
|
user: new Types.ObjectId(req.user._id)
|
||||||
}
|
}
|
||||||
@@ -36,7 +94,7 @@ export const batchCreateSecrets = async (req: Request, res: Response) => {
|
|||||||
sanitizedSecretesToCreate.push(safeUpdateFields)
|
sanitizedSecretesToCreate.push(safeUpdateFields)
|
||||||
})
|
})
|
||||||
|
|
||||||
const [bulkCreateError, newlyCreatedSecrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then())
|
const [bulkCreateError, secrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then())
|
||||||
if (bulkCreateError) {
|
if (bulkCreateError) {
|
||||||
if (bulkCreateError instanceof ValidationError) {
|
if (bulkCreateError instanceof ValidationError) {
|
||||||
throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack })
|
throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack })
|
||||||
@@ -45,20 +103,31 @@ export const batchCreateSecrets = async (req: Request, res: Response) => {
|
|||||||
throw InternalServerError({ message: "Unable to process your batch create request. Please try again", stack: bulkCreateError.stack })
|
throw InternalServerError({ message: "Unable to process your batch create request. Please try again", stack: bulkCreateError.stack })
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).send()
|
if (postHogClient) {
|
||||||
}
|
postHogClient.capture({
|
||||||
|
event: 'secrets added',
|
||||||
|
distinctId: req.user.email,
|
||||||
export const createSingleSecret = async (req: Request, res: Response) => {
|
properties: {
|
||||||
try {
|
numberOfSecrets: (secretsToCreate ?? []).length,
|
||||||
const secretFromDB = await Secret.findById(req.params.secretId)
|
workspaceId,
|
||||||
return res.status(200).send(secretFromDB);
|
environment,
|
||||||
} catch (e) {
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
throw BadRequestError({ message: "Unable to find the requested secret" })
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.status(200).send({
|
||||||
|
secrets
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const batchDeleteSecrets = async (req: Request, res: Response) => {
|
/**
|
||||||
|
* Delete secrets in workspace with id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const deleteSecrets = async (req: Request, res: Response) => {
|
||||||
const { workspaceId, environmentName } = req.params
|
const { workspaceId, environmentName } = req.params
|
||||||
const secretIdsToDelete: string[] = req.body.secretIds
|
const secretIdsToDelete: string[] = req.body.secretIds
|
||||||
|
|
||||||
@@ -70,10 +139,12 @@ export const batchDeleteSecrets = async (req: Request, res: Response) => {
|
|||||||
const secretsUserCanDeleteSet: Set<string> = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString()));
|
const secretsUserCanDeleteSet: Set<string> = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString()));
|
||||||
const deleteOperationsToPerform: AnyBulkWriteOperation<ISecret>[] = []
|
const deleteOperationsToPerform: AnyBulkWriteOperation<ISecret>[] = []
|
||||||
|
|
||||||
|
let numSecretsDeleted = 0;
|
||||||
secretIdsToDelete.forEach(secretIdToDelete => {
|
secretIdsToDelete.forEach(secretIdToDelete => {
|
||||||
if (secretsUserCanDeleteSet.has(secretIdToDelete)) {
|
if (secretsUserCanDeleteSet.has(secretIdToDelete)) {
|
||||||
const deleteOperation = { deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } } }
|
const deleteOperation = { deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } } }
|
||||||
deleteOperationsToPerform.push(deleteOperation)
|
deleteOperationsToPerform.push(deleteOperation)
|
||||||
|
numSecretsDeleted++;
|
||||||
} else {
|
} else {
|
||||||
throw RouteValidationError({ message: "You cannot delete secrets that you do not have access to" })
|
throw RouteValidationError({ message: "You cannot delete secrets that you do not have access to" })
|
||||||
}
|
}
|
||||||
@@ -87,10 +158,57 @@ export const batchDeleteSecrets = async (req: Request, res: Response) => {
|
|||||||
throw InternalServerError()
|
throw InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets deleted',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: numSecretsDeleted,
|
||||||
|
environment: environmentName,
|
||||||
|
workspaceId,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
res.status(200).send()
|
res.status(200).send()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const batchModifySecrets = async (req: Request, res: Response) => {
|
/**
|
||||||
|
* Delete secret with id [secretId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const deleteSecret = async (req: Request, res: Response) => {
|
||||||
|
await Secret.findByIdAndDelete(req._secret._id)
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets deleted',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: 1,
|
||||||
|
workspaceId: req._secret.workspace.toString(),
|
||||||
|
environment: req._secret.environment,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send({
|
||||||
|
secret: req._secret
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update secrets for workspace with id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const updateSecrets = async (req: Request, res: Response) => {
|
||||||
const { workspaceId, environmentName } = req.params
|
const { workspaceId, environmentName } = req.params
|
||||||
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
|
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
|
||||||
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
|
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
|
||||||
@@ -101,7 +219,6 @@ export const batchModifySecrets = async (req: Request, res: Response) => {
|
|||||||
const secretsUserCanModifySet: Set<string> = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString()));
|
const secretsUserCanModifySet: Set<string> = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString()));
|
||||||
const updateOperationsToPerform: any = []
|
const updateOperationsToPerform: any = []
|
||||||
|
|
||||||
|
|
||||||
secretsModificationsRequested.forEach(userModifiedSecret => {
|
secretsModificationsRequested.forEach(userModifiedSecret => {
|
||||||
if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) {
|
if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) {
|
||||||
const sanitizedSecret: SanitizedSecretModify = {
|
const sanitizedSecret: SanitizedSecretModify = {
|
||||||
@@ -135,23 +252,99 @@ export const batchModifySecrets = async (req: Request, res: Response) => {
|
|||||||
throw InternalServerError()
|
throw InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets modified',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: (secretsModificationsRequested ?? []).length,
|
||||||
|
environment: environmentName,
|
||||||
|
workspaceId,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).send()
|
return res.status(200).send()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchAllSecrets = async (req: Request, res: Response) => {
|
/**
|
||||||
|
* Update a secret within workspace with id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const updateSecret = async (req: Request, res: Response) => {
|
||||||
|
const { workspaceId, environmentName } = req.params
|
||||||
|
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
|
||||||
|
|
||||||
|
const [secretIdUserCanModifyError, secretIdUserCanModify] = await to(Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
|
||||||
|
if (secretIdUserCanModifyError && !secretIdUserCanModify) {
|
||||||
|
throw BadRequestError()
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedSecret: SanitizedSecretModify = {
|
||||||
|
secretKeyCiphertext: secretModificationsRequested.secretKeyCiphertext,
|
||||||
|
secretKeyIV: secretModificationsRequested.secretKeyIV,
|
||||||
|
secretKeyTag: secretModificationsRequested.secretKeyTag,
|
||||||
|
secretKeyHash: secretModificationsRequested.secretKeyHash,
|
||||||
|
secretValueCiphertext: secretModificationsRequested.secretValueCiphertext,
|
||||||
|
secretValueIV: secretModificationsRequested.secretValueIV,
|
||||||
|
secretValueTag: secretModificationsRequested.secretValueTag,
|
||||||
|
secretValueHash: secretModificationsRequested.secretValueHash,
|
||||||
|
secretCommentCiphertext: secretModificationsRequested.secretCommentCiphertext,
|
||||||
|
secretCommentIV: secretModificationsRequested.secretCommentIV,
|
||||||
|
secretCommentTag: secretModificationsRequested.secretCommentTag,
|
||||||
|
secretCommentHash: secretModificationsRequested.secretCommentHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
const [error, singleModificationUpdate] = await to(Secret.updateOne({ _id: secretModificationsRequested._id, workspace: workspaceId }, { $inc: { version: 1 }, $set: sanitizedSecret }).then())
|
||||||
|
if (error instanceof ValidationError) {
|
||||||
|
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: error.stack })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets modified',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: 1,
|
||||||
|
environment: environmentName,
|
||||||
|
workspaceId,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send(singleModificationUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return secrets for workspace with id [workspaceId], environment [environment] and user
|
||||||
|
* with id [req.user._id]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getSecrets = async (req: Request, res: Response) => {
|
||||||
const { environment } = req.query;
|
const { environment } = req.query;
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
let userId: string | undefined = undefined // Used for choosing the personal secrets to fetch in
|
let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user
|
||||||
|
let userEmail: Types.ObjectId | undefined = undefined // used for posthog
|
||||||
if (req.user) {
|
if (req.user) {
|
||||||
userId = req.user._id.toString();
|
userId = req.user._id;
|
||||||
|
userEmail = req.user.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.serviceTokenData) {
|
if (req.serviceTokenData) {
|
||||||
userId = req.serviceTokenData.user._id
|
userId = req.serviceTokenData.user._id
|
||||||
|
userEmail = req.serviceTokenData.user.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [retriveAllSecretsError, allSecrets] = await to(Secret.find(
|
const [err, secrets] = await to(Secret.find(
|
||||||
{
|
{
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
environment,
|
environment,
|
||||||
@@ -160,9 +353,49 @@ export const fetchAllSecrets = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
).then())
|
).then())
|
||||||
|
|
||||||
if (retriveAllSecretsError instanceof ValidationError) {
|
if (err) {
|
||||||
throw RouteValidationError({ message: "Unable to get secrets, please try again", stack: retriveAllSecretsError.stack })
|
throw RouteValidationError({ message: "Failed to get secrets, please try again", stack: err.stack })
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json(allSecrets)
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets pulled',
|
||||||
|
distinctId: userEmail,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: (secrets ?? []).length,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json(secrets)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return secret with id [secretId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getSecret = async (req: Request, res: Response) => {
|
||||||
|
// if (postHogClient) {
|
||||||
|
// postHogClient.capture({
|
||||||
|
// event: 'secrets pulled',
|
||||||
|
// distinctId: req.user.email,
|
||||||
|
// properties: {
|
||||||
|
// numberOfSecrets: 1,
|
||||||
|
// workspaceId: req._secret.workspace.toString(),
|
||||||
|
// environment: req._secret.environment,
|
||||||
|
// channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
// userAgent: req.headers?.['user-agent']
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secret: req._secret
|
||||||
|
});
|
||||||
}
|
}
|
||||||
476
backend/src/controllers/v2/secretsController.ts
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
import to from 'await-to-js';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { ISecret, Secret } from '../../models';
|
||||||
|
import {
|
||||||
|
SECRET_PERSONAL,
|
||||||
|
SECRET_SHARED,
|
||||||
|
ACTION_ADD_SECRETS,
|
||||||
|
ACTION_READ_SECRETS,
|
||||||
|
ACTION_UPDATE_SECRETS,
|
||||||
|
ACTION_DELETE_SECRETS
|
||||||
|
} from '../../variables';
|
||||||
|
import { ValidationError } from '../../utils/errors';
|
||||||
|
import { EventService } from '../../services';
|
||||||
|
import { eventPushSecrets } from '../../events';
|
||||||
|
import { EESecretService, EELogService } from '../../ee/services';
|
||||||
|
import { postHogClient } from '../../services';
|
||||||
|
import { BadRequestError } from '../../utils/errors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create secret(s) for workspace with id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const createSecrets = async (req: Request, res: Response) => {
|
||||||
|
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
||||||
|
const { workspaceId, environment } = req.body;
|
||||||
|
|
||||||
|
let toAdd;
|
||||||
|
if (Array.isArray(req.body.secrets)) {
|
||||||
|
// case: create multiple secrets
|
||||||
|
toAdd = req.body.secrets;
|
||||||
|
} else if (typeof req.body.secrets === 'object') {
|
||||||
|
// case: create 1 secret
|
||||||
|
toAdd = [req.body.secrets];
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSecrets = await Secret.insertMany(
|
||||||
|
toAdd.map(({
|
||||||
|
type,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
}: {
|
||||||
|
type: string;
|
||||||
|
secretKeyCiphertext: string;
|
||||||
|
secretKeyIV: string;
|
||||||
|
secretKeyTag: string;
|
||||||
|
secretValueCiphertext: string;
|
||||||
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
}) => ({
|
||||||
|
version: 1,
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
type,
|
||||||
|
user: type === SECRET_PERSONAL ? req.user : undefined,
|
||||||
|
environment,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
// trigger event - push secrets
|
||||||
|
await EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// (EE) add secret versions for new secrets
|
||||||
|
await EESecretService.addSecretVersions({
|
||||||
|
secretVersions: newSecrets.map(({
|
||||||
|
_id,
|
||||||
|
version,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash
|
||||||
|
}) => ({
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
secret: _id,
|
||||||
|
version,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
isDeleted: false,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
const addAction = await EELogService.createActionSecret({
|
||||||
|
name: ACTION_ADD_SECRETS,
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId,
|
||||||
|
secretIds: newSecrets.map((n) => n._id)
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) create (audit) log
|
||||||
|
addAction && await EELogService.createLog({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId,
|
||||||
|
actions: [addAction],
|
||||||
|
channel,
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) take a secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets added',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: toAdd.length,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secrets: newSecrets
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return secret(s) for workspace with id [workspaceId], environment [environment] and user
|
||||||
|
* with id [req.user._id]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getSecrets = async (req: Request, res: Response) => {
|
||||||
|
const { workspaceId, environment } = req.query;
|
||||||
|
|
||||||
|
let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user
|
||||||
|
let userEmail: Types.ObjectId | undefined = undefined // used for posthog
|
||||||
|
if (req.user) {
|
||||||
|
userId = req.user._id;
|
||||||
|
userEmail = req.user.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.serviceTokenData) {
|
||||||
|
userId = req.serviceTokenData.user._id
|
||||||
|
userEmail = req.serviceTokenData.user.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [err, secrets] = await to(Secret.find(
|
||||||
|
{
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
$or: [
|
||||||
|
{ user: userId },
|
||||||
|
{ user: { $exists: false } }
|
||||||
|
],
|
||||||
|
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
||||||
|
}
|
||||||
|
).then())
|
||||||
|
|
||||||
|
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
|
||||||
|
|
||||||
|
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
||||||
|
|
||||||
|
const readAction = await EELogService.createActionSecret({
|
||||||
|
name: ACTION_READ_SECRETS,
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: workspaceId as string,
|
||||||
|
secretIds: secrets.map((n: any) => n._id)
|
||||||
|
});
|
||||||
|
|
||||||
|
readAction && await EELogService.createLog({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: workspaceId as string,
|
||||||
|
actions: [readAction],
|
||||||
|
channel,
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets pulled',
|
||||||
|
distinctId: userEmail,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: secrets.length,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel,
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secrets
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update secret(s)
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const updateSecrets = async (req: Request, res: Response) => {
|
||||||
|
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
||||||
|
|
||||||
|
// TODO: move type
|
||||||
|
interface PatchSecret {
|
||||||
|
id: string;
|
||||||
|
secretKeyCiphertext: string;
|
||||||
|
secretKeyIV: string;
|
||||||
|
secretKeyTag: string;
|
||||||
|
secretValueCiphertext: string;
|
||||||
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
secretCommentCiphertext: string;
|
||||||
|
secretCommentIV: string;
|
||||||
|
secretCommentTag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => {
|
||||||
|
const {
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretCommentCiphertext,
|
||||||
|
secretCommentIV,
|
||||||
|
secretCommentTag
|
||||||
|
} = secret;
|
||||||
|
|
||||||
|
return ({
|
||||||
|
updateOne: {
|
||||||
|
filter: { _id: new Types.ObjectId(secret.id) },
|
||||||
|
update: {
|
||||||
|
$inc: {
|
||||||
|
version: 1
|
||||||
|
},
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
...((
|
||||||
|
secretCommentCiphertext &&
|
||||||
|
secretCommentIV &&
|
||||||
|
secretCommentTag
|
||||||
|
) ? {
|
||||||
|
secretCommentCiphertext,
|
||||||
|
secretCommentIV,
|
||||||
|
secretCommentTag
|
||||||
|
} : {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Secret.bulkWrite(updateOperationsToPerform);
|
||||||
|
|
||||||
|
const secretModificationsBySecretId: { [key: string]: PatchSecret } = {};
|
||||||
|
req.body.secrets.forEach((secret: PatchSecret) => {
|
||||||
|
secretModificationsBySecretId[secret.id] = secret;
|
||||||
|
});
|
||||||
|
|
||||||
|
const ListOfSecretsBeforeModifications = req.secrets
|
||||||
|
const secretVersions = {
|
||||||
|
secretVersions: ListOfSecretsBeforeModifications.map((secret: ISecret) => {
|
||||||
|
const {
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretCommentCiphertext,
|
||||||
|
secretCommentIV,
|
||||||
|
secretCommentTag,
|
||||||
|
} = secretModificationsBySecretId[secret._id.toString()]
|
||||||
|
|
||||||
|
return ({
|
||||||
|
secret: secret._id,
|
||||||
|
version: secret.version + 1,
|
||||||
|
workspace: secret.workspace,
|
||||||
|
type: secret.type,
|
||||||
|
environment: secret.environment,
|
||||||
|
secretKeyCiphertext: secretKeyCiphertext ? secretKeyCiphertext : secret.secretKeyCiphertext,
|
||||||
|
secretKeyIV: secretKeyIV ? secretKeyIV : secret.secretKeyIV,
|
||||||
|
secretKeyTag: secretKeyTag ? secretKeyTag : secret.secretKeyTag,
|
||||||
|
secretValueCiphertext: secretValueCiphertext ? secretValueCiphertext : secret.secretValueCiphertext,
|
||||||
|
secretValueIV: secretValueIV ? secretValueIV : secret.secretValueIV,
|
||||||
|
secretValueTag: secretValueTag ? secretValueTag : secret.secretValueTag,
|
||||||
|
secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext,
|
||||||
|
secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
|
||||||
|
secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await EESecretService.addSecretVersions(secretVersions);
|
||||||
|
|
||||||
|
|
||||||
|
// group secrets into workspaces so updated secrets can
|
||||||
|
// be logged and snapshotted separately for each workspace
|
||||||
|
const workspaceSecretObj: any = {};
|
||||||
|
req.secrets.forEach((s: any) => {
|
||||||
|
if (s.workspace.toString() in workspaceSecretObj) {
|
||||||
|
workspaceSecretObj[s.workspace.toString()].push(s);
|
||||||
|
} else {
|
||||||
|
workspaceSecretObj[s.workspace.toString()] = [s]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(workspaceSecretObj).forEach(async (key) => {
|
||||||
|
// trigger event - push secrets
|
||||||
|
setTimeout(async () => {
|
||||||
|
await EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId: key
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
const updateAction = await EELogService.createActionSecret({
|
||||||
|
name: ACTION_UPDATE_SECRETS,
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: key,
|
||||||
|
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) create (audit) log
|
||||||
|
updateAction && await EELogService.createLog({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: key,
|
||||||
|
actions: [updateAction],
|
||||||
|
channel,
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) take a secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId: key
|
||||||
|
})
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets modified',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: workspaceSecretObj[key].length,
|
||||||
|
environment: workspaceSecretObj[key][0].environment,
|
||||||
|
workspaceId: key,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secrets: await Secret.find({
|
||||||
|
_id: {
|
||||||
|
$in: req.secrets.map((secret: ISecret) => secret._id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete secret(s) with id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const deleteSecrets = async (req: Request, res: Response) => {
|
||||||
|
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
||||||
|
const toDelete = req.secrets.map((s: any) => s._id);
|
||||||
|
|
||||||
|
await Secret.deleteMany({
|
||||||
|
_id: {
|
||||||
|
$in: toDelete
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await EESecretService.markDeletedSecretVersions({
|
||||||
|
secretIds: toDelete
|
||||||
|
});
|
||||||
|
|
||||||
|
// group secrets into workspaces so deleted secrets can
|
||||||
|
// be logged and snapshotted separately for each workspace
|
||||||
|
const workspaceSecretObj: any = {};
|
||||||
|
req.secrets.forEach((s: any) => {
|
||||||
|
if (s.workspace.toString() in workspaceSecretObj) {
|
||||||
|
workspaceSecretObj[s.workspace.toString()].push(s);
|
||||||
|
} else {
|
||||||
|
workspaceSecretObj[s.workspace.toString()] = [s]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(workspaceSecretObj).forEach(async (key) => {
|
||||||
|
// trigger event - push secrets
|
||||||
|
await EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId: key
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const deleteAction = await EELogService.createActionSecret({
|
||||||
|
name: ACTION_DELETE_SECRETS,
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: key,
|
||||||
|
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) create (audit) log
|
||||||
|
deleteAction && await EELogService.createLog({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: key,
|
||||||
|
actions: [deleteAction],
|
||||||
|
channel,
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) take a secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId: key
|
||||||
|
})
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets deleted',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: workspaceSecretObj[key].length,
|
||||||
|
environment: workspaceSecretObj[key][0].environment,
|
||||||
|
workspaceId: key,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secrets: req.secrets
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
import { pushKeys } from '../../helpers/key';
|
import { pushKeys } from '../../helpers/key';
|
||||||
import { postHogClient, EventService } from '../../services';
|
import { postHogClient, EventService } from '../../services';
|
||||||
import { eventPushSecrets } from '../../events';
|
import { eventPushSecrets } from '../../events';
|
||||||
import { ENV_SET } from '../../variables';
|
|
||||||
|
|
||||||
interface V2PushSecret {
|
interface V2PushSecret {
|
||||||
type: string; // personal or shared
|
type: string; // personal or shared
|
||||||
@@ -52,7 +51,8 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
|||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
// validate environment
|
// validate environment
|
||||||
if (!ENV_SET.has(environment)) {
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
throw new Error('Failed to validate environment');
|
throw new Error('Failed to validate environment');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +129,11 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
|||||||
} else if (req.serviceTokenData) {
|
} else if (req.serviceTokenData) {
|
||||||
userId = req.serviceTokenData.user._id
|
userId = req.serviceTokenData.user._id
|
||||||
}
|
}
|
||||||
|
// validate environment
|
||||||
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
|
throw new Error('Failed to validate environment');
|
||||||
|
}
|
||||||
|
|
||||||
secrets = await pull({
|
secrets = await pull({
|
||||||
userId,
|
userId,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Secret } from '../../../models';
|
||||||
import { SecretVersion } from '../../models';
|
import { SecretVersion } from '../../models';
|
||||||
|
import { EESecretService } from '../../services';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return secret versions for secret with id [secretId]
|
* Return secret versions for secret with id [secretId]
|
||||||
@@ -34,3 +36,102 @@ import { SecretVersion } from '../../models';
|
|||||||
secretVersions
|
secretVersions
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roll back secret with id [secretId] to version [version]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
||||||
|
let secret;
|
||||||
|
try {
|
||||||
|
const { secretId } = req.params;
|
||||||
|
const { version } = req.body;
|
||||||
|
|
||||||
|
// validate secret version
|
||||||
|
const oldSecretVersion = await SecretVersion.findOne({
|
||||||
|
secret: secretId,
|
||||||
|
version
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!oldSecretVersion) throw new Error('Failed to find secret version');
|
||||||
|
|
||||||
|
const {
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash
|
||||||
|
} = oldSecretVersion;
|
||||||
|
|
||||||
|
// update secret
|
||||||
|
secret = await Secret.findByIdAndUpdate(
|
||||||
|
secretId,
|
||||||
|
{
|
||||||
|
$inc: {
|
||||||
|
version: 1
|
||||||
|
},
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!secret) throw new Error('Failed to find and update secret');
|
||||||
|
|
||||||
|
// add new secret version
|
||||||
|
await new SecretVersion({
|
||||||
|
secret: secretId,
|
||||||
|
version: secret.version,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
isDeleted: false,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
// take secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId: secret.workspace.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to roll back secret version'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secret
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,6 +2,12 @@ import { Request, Response } from 'express';
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { SecretSnapshot } from '../../models';
|
import { SecretSnapshot } from '../../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return secret snapshot with id [secretSnapshotId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export const getSecretSnapshot = async (req: Request, res: Response) => {
|
export const getSecretSnapshot = async (req: Request, res: Response) => {
|
||||||
let secretSnapshot;
|
let secretSnapshot;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import e, { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
Secret
|
||||||
|
} from '../../../models';
|
||||||
import {
|
import {
|
||||||
SecretSnapshot,
|
SecretSnapshot,
|
||||||
Log
|
Log,
|
||||||
|
SecretVersion,
|
||||||
|
ISecretVersion
|
||||||
} from '../../models';
|
} from '../../models';
|
||||||
|
import { EESecretService } from '../../services';
|
||||||
|
import { getLatestSecretVersionIds } from '../../helpers/secretVersion';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return secret snapshots for workspace with id [workspaceId]
|
* Return secret snapshots for workspace with id [workspaceId]
|
||||||
@@ -63,6 +71,159 @@ export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Respon
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rollback secret snapshot with id [secretSnapshotId] to version [version]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Response) => {
|
||||||
|
let secrets;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
const { version } = req.body;
|
||||||
|
|
||||||
|
// validate secret snapshot
|
||||||
|
const secretSnapshot = await SecretSnapshot.findOne({
|
||||||
|
workspace: workspaceId,
|
||||||
|
version
|
||||||
|
}).populate<{ secretVersions: ISecretVersion[]}>('secretVersions');
|
||||||
|
|
||||||
|
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
|
||||||
|
|
||||||
|
// TODO: fix any
|
||||||
|
const oldSecretVersionsObj: any = secretSnapshot.secretVersions
|
||||||
|
.reduce((accumulator, s) => ({
|
||||||
|
...accumulator,
|
||||||
|
[`${s.secret.toString()}`]: s
|
||||||
|
}), {});
|
||||||
|
|
||||||
|
const latestSecretVersionIds = await getLatestSecretVersionIds({
|
||||||
|
secretIds: secretSnapshot.secretVersions.map((sv) => sv.secret)
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: fix any
|
||||||
|
const latestSecretVersions: any = (await SecretVersion.find({
|
||||||
|
_id: {
|
||||||
|
$in: latestSecretVersionIds.map((s) => s.versionId)
|
||||||
|
}
|
||||||
|
}, 'secret version'))
|
||||||
|
.reduce((accumulator, s) => ({
|
||||||
|
...accumulator,
|
||||||
|
[`${s.secret.toString()}`]: s
|
||||||
|
}), {});
|
||||||
|
|
||||||
|
// delete existing secrets
|
||||||
|
await Secret.deleteMany({
|
||||||
|
workspace: workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
// add secrets
|
||||||
|
secrets = await Secret.insertMany(
|
||||||
|
secretSnapshot.secretVersions.map((sv) => {
|
||||||
|
const secretId = sv.secret;
|
||||||
|
const {
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash,
|
||||||
|
createdAt
|
||||||
|
} = oldSecretVersionsObj[secretId.toString()];
|
||||||
|
|
||||||
|
return ({
|
||||||
|
_id: secretId,
|
||||||
|
version: latestSecretVersions[secretId.toString()].version + 1,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash,
|
||||||
|
secretCommentCiphertext: '',
|
||||||
|
secretCommentIV: '',
|
||||||
|
secretCommentTag: '',
|
||||||
|
createdAt
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// add secret versions
|
||||||
|
await SecretVersion.insertMany(
|
||||||
|
secrets.map(({
|
||||||
|
_id,
|
||||||
|
version,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash
|
||||||
|
}) => ({
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
secret: _id,
|
||||||
|
version,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
isDeleted: false,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// update secret versions of restored secrets as not deleted
|
||||||
|
await SecretVersion.updateMany({
|
||||||
|
secret: {
|
||||||
|
$in: secretSnapshot.secretVersions.map((sv) => sv.secret)
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
isDeleted: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// take secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to roll back secret snapshot'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secrets
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return (audit) logs for workspace with id [workspaceId]
|
* Return (audit) logs for workspace with id [workspaceId]
|
||||||
* @param req
|
* @param req
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { Secret } from '../../models';
|
|
||||||
import { SecretVersion, Action } from '../models';
|
import { SecretVersion, Action } from '../models';
|
||||||
|
import {
|
||||||
|
getLatestSecretVersionIds,
|
||||||
|
getLatestNSecretSecretVersionIds
|
||||||
|
} from '../helpers/secretVersion';
|
||||||
import { ACTION_UPDATE_SECRETS } from '../../variables';
|
import { ACTION_UPDATE_SECRETS } from '../../variables';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,65 +33,23 @@ const createActionSecretHelper = async ({
|
|||||||
if (name === ACTION_UPDATE_SECRETS) {
|
if (name === ACTION_UPDATE_SECRETS) {
|
||||||
// case: action is updating secrets
|
// case: action is updating secrets
|
||||||
// -> add old and new secret versions
|
// -> add old and new secret versions
|
||||||
|
latestSecretVersions = (await getLatestNSecretSecretVersionIds({
|
||||||
// TODO: make query more efficient
|
secretIds,
|
||||||
latestSecretVersions = (await SecretVersion.aggregate([
|
n: 2
|
||||||
{
|
}))
|
||||||
$match: {
|
.map((s) => ({
|
||||||
secret: {
|
oldSecretVersion: s.versions[0]._id,
|
||||||
$in: secretIds,
|
newSecretVersion: s.versions[1]._id
|
||||||
},
|
}));
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$sort: { version: -1 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: "$secret",
|
|
||||||
versions: { $push: "$$ROOT" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$project: {
|
|
||||||
_id: 0,
|
|
||||||
secret: "$_id",
|
|
||||||
versions: { $slice: ["$versions", 2] },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]))
|
|
||||||
.map((s) => ({
|
|
||||||
oldSecretVersion: s.versions[0]._id,
|
|
||||||
newSecretVersion: s.versions[1]._id
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// case: action is adding, deleting, or reading secrets
|
// case: action is adding, deleting, or reading secrets
|
||||||
// -> add new secret versions
|
// -> add new secret versions
|
||||||
latestSecretVersions = (await SecretVersion.aggregate([
|
latestSecretVersions = (await getLatestSecretVersionIds({
|
||||||
{
|
secretIds
|
||||||
$match: {
|
}))
|
||||||
secret: {
|
.map((s) => ({
|
||||||
$in: secretIds
|
newSecretVersion: s.versionId
|
||||||
}
|
}));
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: '$secret',
|
|
||||||
version: { $max: '$version' },
|
|
||||||
versionId: { $max: '$_id' } // secret version id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$sort: { version: -1 }
|
|
||||||
}
|
|
||||||
])
|
|
||||||
.exec())
|
|
||||||
.map((s) => ({
|
|
||||||
newSecretVersion: s.versionId
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
action = await new Action({
|
action = await new Action({
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import {
|
import {
|
||||||
Secret,
|
Secret,
|
||||||
ISecret
|
ISecret
|
||||||
} from '../../models';
|
} from '../../models';
|
||||||
import {
|
import {
|
||||||
SecretSnapshot,
|
SecretSnapshot,
|
||||||
SecretVersion,
|
SecretVersion,
|
||||||
ISecretVersion
|
ISecretVersion
|
||||||
} from '../models';
|
} from '../models';
|
||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
* @param {String} obj.workspaceId
|
* @param {String} obj.workspaceId
|
||||||
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot
|
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot
|
||||||
*/
|
*/
|
||||||
const takeSecretSnapshotHelper = async ({
|
const takeSecretSnapshotHelper = async ({
|
||||||
workspaceId
|
workspaceId
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
@@ -48,7 +48,7 @@ import {
|
|||||||
{
|
{
|
||||||
$sort: { version: -1 }
|
$sort: { version: -1 }
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
.exec())
|
.exec())
|
||||||
.map((s) => s.versionId);
|
.map((s) => s.versionId);
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ const addSecretVersionsHelper = async ({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to add secret versions');
|
throw new Error(`Failed to add secret versions [err=${err}]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newSecretVersions;
|
return newSecretVersions;
|
||||||
@@ -125,34 +125,34 @@ const initSecretVersioningHelper = async () => {
|
|||||||
{ $set: { version: 1 } }
|
{ $set: { version: 1 } }
|
||||||
);
|
);
|
||||||
|
|
||||||
const unversionedSecrets: ISecret[] = await Secret.aggregate([
|
const unversionedSecrets: ISecret[] = await Secret.aggregate([
|
||||||
{
|
{
|
||||||
$lookup: {
|
$lookup: {
|
||||||
from: 'secretversions',
|
from: 'secretversions',
|
||||||
localField: '_id',
|
localField: '_id',
|
||||||
foreignField: 'secret',
|
foreignField: 'secret',
|
||||||
as: 'versions',
|
as: 'versions',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$match: {
|
$match: {
|
||||||
versions: { $size: 0 },
|
versions: { $size: 0 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (unversionedSecrets.length > 0) {
|
if (unversionedSecrets.length > 0) {
|
||||||
await addSecretVersionsHelper({
|
await addSecretVersionsHelper({
|
||||||
secretVersions: unversionedSecrets.map((s, idx) => ({
|
secretVersions: unversionedSecrets.map((s, idx) => ({
|
||||||
...s,
|
...s,
|
||||||
secret: s._id,
|
secret: s._id,
|
||||||
version: s.version ? s.version : 1,
|
version: s.version ? s.version : 1,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
workspace: s.workspace,
|
workspace: s.workspace,
|
||||||
environment: s.environment
|
environment: s.environment
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
@@ -162,7 +162,7 @@ const initSecretVersioningHelper = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
takeSecretSnapshotHelper,
|
takeSecretSnapshotHelper,
|
||||||
addSecretVersionsHelper,
|
addSecretVersionsHelper,
|
||||||
markDeletedSecretVersionsHelper,
|
markDeletedSecretVersionsHelper,
|
||||||
initSecretVersioningHelper
|
initSecretVersioningHelper
|
||||||
|
|||||||
110
backend/src/ee/helpers/secretVersion.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import { SecretVersion } from '../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return latest secret versions for secrets with ids [secretIds]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.secretIds = ids of secrets to get latest versions for
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getLatestSecretVersionIds = async ({
|
||||||
|
secretIds
|
||||||
|
}: {
|
||||||
|
secretIds: Types.ObjectId[];
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
interface LatestSecretVersionId {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
version: number;
|
||||||
|
versionId: Types.ObjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
let latestSecretVersionIds: LatestSecretVersionId[];
|
||||||
|
try {
|
||||||
|
latestSecretVersionIds = (await SecretVersion.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
secret: {
|
||||||
|
$in: secretIds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: '$secret',
|
||||||
|
version: { $max: '$version' },
|
||||||
|
versionId: { $max: '$_id' } // id of latest secret version
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$sort: { version: -1 }
|
||||||
|
}
|
||||||
|
])
|
||||||
|
.exec());
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get latest secret versions');
|
||||||
|
}
|
||||||
|
|
||||||
|
return latestSecretVersionIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return latest [n] secret versions for secrets with ids [secretIds]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.secretIds = ids of secrets to get latest versions for
|
||||||
|
* @param {Number} obj.n - number of latest secret versions to return for each secret
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getLatestNSecretSecretVersionIds = async ({
|
||||||
|
secretIds,
|
||||||
|
n
|
||||||
|
}: {
|
||||||
|
secretIds: Types.ObjectId[];
|
||||||
|
n: number;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
// TODO: optimize query
|
||||||
|
let latestNSecretVersions;
|
||||||
|
try {
|
||||||
|
latestNSecretVersions = (await SecretVersion.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
secret: {
|
||||||
|
$in: secretIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$sort: { version: -1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: "$secret",
|
||||||
|
versions: { $push: "$$ROOT" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
_id: 0,
|
||||||
|
secret: "$_id",
|
||||||
|
versions: { $slice: ["$versions", n] },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]));
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get latest n secret versions');
|
||||||
|
}
|
||||||
|
|
||||||
|
return latestNSecretVersions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
getLatestSecretVersionIds,
|
||||||
|
getLatestNSecretSecretVersionIds
|
||||||
|
}
|
||||||
@@ -2,31 +2,18 @@ import { Schema, model, Types } from 'mongoose';
|
|||||||
import {
|
import {
|
||||||
SECRET_SHARED,
|
SECRET_SHARED,
|
||||||
SECRET_PERSONAL,
|
SECRET_PERSONAL,
|
||||||
ENV_DEV,
|
|
||||||
ENV_TESTING,
|
|
||||||
ENV_STAGING,
|
|
||||||
ENV_PROD
|
|
||||||
} from '../../variables';
|
} from '../../variables';
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO:
|
|
||||||
* 1. Modify SecretVersion to also contain XX
|
|
||||||
* - type
|
|
||||||
* - user
|
|
||||||
* - environment
|
|
||||||
* 2. Modify SecretSnapshot to point to arrays of SecretVersion
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface ISecretVersion {
|
export interface ISecretVersion {
|
||||||
_id?: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
secret: Types.ObjectId;
|
secret: Types.ObjectId;
|
||||||
version: number;
|
version: number;
|
||||||
workspace: Types.ObjectId; // new
|
workspace: Types.ObjectId; // new
|
||||||
type: string; // new
|
type: string; // new
|
||||||
user: Types.ObjectId; // new
|
user: Types.ObjectId; // new
|
||||||
environment: string; // new
|
environment: string; // new
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
secretKeyCiphertext: string;
|
secretKeyCiphertext: string;
|
||||||
secretKeyIV: string;
|
secretKeyIV: string;
|
||||||
secretKeyTag: string;
|
secretKeyTag: string;
|
||||||
secretKeyHash: string;
|
secretKeyHash: string;
|
||||||
@@ -37,17 +24,17 @@ export interface ISecretVersion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const secretVersionSchema = new Schema<ISecretVersion>(
|
const secretVersionSchema = new Schema<ISecretVersion>(
|
||||||
{
|
{
|
||||||
secret: { // could be deleted
|
secret: { // could be deleted
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Secret',
|
ref: 'Secret',
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
version: {
|
version: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1,
|
default: 1,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
workspace: {
|
workspace: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Workspace',
|
ref: 'Workspace',
|
||||||
@@ -65,15 +52,14 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
|||||||
},
|
},
|
||||||
environment: {
|
environment: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
isDeleted: {
|
isDeleted: { // consider removing field
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
secretKeyCiphertext: {
|
secretKeyCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
@@ -86,8 +72,7 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
|||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
secretKeyHash: {
|
secretKeyHash: {
|
||||||
type: String,
|
type: String
|
||||||
required: true
|
|
||||||
},
|
},
|
||||||
secretValueCiphertext: {
|
secretValueCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -102,13 +87,12 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
|||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
secretValueHash: {
|
secretValueHash: {
|
||||||
type: String,
|
type: String
|
||||||
required: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true
|
timestamps: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const SecretVersion = model<ISecretVersion>('SecretVersion', secretVersionSchema);
|
const SecretVersion = model<ISecretVersion>('SecretVersion', secretVersionSchema);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
requireSecretAuth,
|
requireSecretAuth,
|
||||||
validateRequest
|
validateRequest
|
||||||
} from '../../../middleware';
|
} from '../../../middleware';
|
||||||
import { query, param } from 'express-validator';
|
import { query, param, body } from 'express-validator';
|
||||||
import { secretController } from '../../controllers/v1';
|
import { secretController } from '../../controllers/v1';
|
||||||
import { ADMIN, MEMBER } from '../../../variables';
|
import { ADMIN, MEMBER } from '../../../variables';
|
||||||
|
|
||||||
@@ -24,4 +24,17 @@ router.get(
|
|||||||
secretController.getSecretVersions
|
secretController.getSecretVersions
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:secretId/secret-versions/rollback',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
requireSecretAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
|
}),
|
||||||
|
param('secretId').exists().trim(),
|
||||||
|
body('version').exists().isInt(),
|
||||||
|
secretController.rollbackSecretVersion
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
requireAuth,
|
requireAuth,
|
||||||
validateRequest
|
validateRequest
|
||||||
} from '../../../middleware';
|
} from '../../../middleware';
|
||||||
import { param } from 'express-validator';
|
import { param, body } from 'express-validator';
|
||||||
import { ADMIN, MEMBER } from '../../../variables';
|
import { ADMIN, MEMBER } from '../../../variables';
|
||||||
import { secretSnapshotController } from '../../controllers/v1';
|
import { secretSnapshotController } from '../../controllers/v1';
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
requireWorkspaceAuth,
|
requireWorkspaceAuth,
|
||||||
validateRequest
|
validateRequest
|
||||||
} from '../../../middleware';
|
} from '../../../middleware';
|
||||||
import { param, query } from 'express-validator';
|
import { param, query, body } from 'express-validator';
|
||||||
import { ADMIN, MEMBER } from '../../../variables';
|
import { ADMIN, MEMBER } from '../../../variables';
|
||||||
import { workspaceController } from '../../controllers/v1';
|
import { workspaceController } from '../../controllers/v1';
|
||||||
|
|
||||||
@@ -37,6 +37,20 @@ router.get(
|
|||||||
workspaceController.getWorkspaceSecretSnapshotsCount
|
workspaceController.getWorkspaceSecretSnapshotsCount
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:workspaceId/secret-snapshots/rollback',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
body('version').exists().isInt(),
|
||||||
|
validateRequest,
|
||||||
|
workspaceController.rollbackWorkspaceSecretSnapshot
|
||||||
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:workspaceId/logs',
|
'/:workspaceId/logs',
|
||||||
requireAuth({
|
requireAuth({
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ const getSecretsHelper = async ({
|
|||||||
try {
|
try {
|
||||||
const key = await getKey({ workspaceId });
|
const key = await getKey({ workspaceId });
|
||||||
const secrets = await Secret.find({
|
const secrets = await Secret.find({
|
||||||
workspaceId,
|
workspace: workspaceId,
|
||||||
environment,
|
environment,
|
||||||
type: SECRET_SHARED
|
type: SECRET_SHARED
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import {
|
|||||||
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
|
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
|
||||||
import { BotService } from '../services';
|
import { BotService } from '../services';
|
||||||
import {
|
import {
|
||||||
ENV_DEV,
|
|
||||||
EVENT_PUSH_SECRETS,
|
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY
|
INTEGRATION_NETLIFY
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
@@ -36,11 +34,13 @@ interface Update {
|
|||||||
const handleOAuthExchangeHelper = async ({
|
const handleOAuthExchangeHelper = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
integration,
|
integration,
|
||||||
code
|
code,
|
||||||
|
environment
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
integration: string;
|
integration: string;
|
||||||
code: string;
|
code: string;
|
||||||
|
environment: string;
|
||||||
}) => {
|
}) => {
|
||||||
let action;
|
let action;
|
||||||
let integrationAuth;
|
let integrationAuth;
|
||||||
@@ -102,9 +102,9 @@ const handleOAuthExchangeHelper = async ({
|
|||||||
// initialize new integration after exchange
|
// initialize new integration after exchange
|
||||||
await new Integration({
|
await new Integration({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
environment: ENV_DEV,
|
|
||||||
isActive: false,
|
isActive: false,
|
||||||
app: null,
|
app: null,
|
||||||
|
environment,
|
||||||
integration,
|
integration,
|
||||||
integrationAuth: integrationAuth._id
|
integrationAuth: integrationAuth._id
|
||||||
}).save();
|
}).save();
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ const apiLimiter = rateLimit({
|
|||||||
max: 450,
|
max: 450,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
skip: (request) => request.path === '/healthcheck'
|
skip: (request) => {
|
||||||
|
return request.path === '/healthcheck' || request.path === '/api/status'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5 requests per hour
|
// 5 requests per hour
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Types } from 'mongoose';
|
|||||||
import {
|
import {
|
||||||
Secret,
|
Secret,
|
||||||
ISecret,
|
ISecret,
|
||||||
|
Membership
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import {
|
import {
|
||||||
EESecretService,
|
EESecretService,
|
||||||
@@ -20,6 +21,46 @@ import {
|
|||||||
ACTION_READ_SECRETS
|
ACTION_READ_SECRETS
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that user with id [userId] can modify secrets with ids [secretIds]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.userId - id of user to validate
|
||||||
|
* @param {Object} obj.secretIds - secret ids
|
||||||
|
* @returns {Secret[]} secrets
|
||||||
|
*/
|
||||||
|
const validateSecrets = async ({
|
||||||
|
userId,
|
||||||
|
secretIds
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
secretIds: string[];
|
||||||
|
}) =>{
|
||||||
|
let secrets;
|
||||||
|
try {
|
||||||
|
secrets = await Secret.find({
|
||||||
|
_id: {
|
||||||
|
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const workspaceIdsSet = new Set((await Membership.find({
|
||||||
|
user: userId
|
||||||
|
}, 'workspace'))
|
||||||
|
.map((m) => m.workspace.toString()));
|
||||||
|
|
||||||
|
secrets.forEach((secret: ISecret) => {
|
||||||
|
if (!workspaceIdsSet.has(secret.workspace.toString())) {
|
||||||
|
throw new Error('Failed to validate secret');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('Failed to validate secrets');
|
||||||
|
}
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
}
|
||||||
|
|
||||||
interface V1PushSecret {
|
interface V1PushSecret {
|
||||||
ciphertextKey: string;
|
ciphertextKey: string;
|
||||||
ivKey: string;
|
ivKey: string;
|
||||||
@@ -187,6 +228,7 @@ const v1PushSecrets = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
const newSecret = newSecretsObj[`${type}-${secretKeyHash}`];
|
const newSecret = newSecretsObj[`${type}-${secretKeyHash}`];
|
||||||
return ({
|
return ({
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
secret: _id,
|
secret: _id,
|
||||||
version: version ? version + 1 : 1,
|
version: version ? version + 1 : 1,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
@@ -258,6 +300,7 @@ const v1PushSecrets = async ({
|
|||||||
secretValueTag,
|
secretValueTag,
|
||||||
secretValueHash
|
secretValueHash
|
||||||
}) => ({
|
}) => ({
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
secret: _id,
|
secret: _id,
|
||||||
version,
|
version,
|
||||||
workspace,
|
workspace,
|
||||||
@@ -280,7 +323,7 @@ const v1PushSecrets = async ({
|
|||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId
|
workspaceId
|
||||||
})
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
@@ -527,6 +570,7 @@ const v1PushSecrets = async ({
|
|||||||
environment: string;
|
environment: string;
|
||||||
}): Promise<ISecret[]> => {
|
}): Promise<ISecret[]> => {
|
||||||
let secrets: any; // TODO: FIX any
|
let secrets: any; // TODO: FIX any
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// get shared workspace secrets
|
// get shared workspace secrets
|
||||||
const sharedSecrets = await Secret.find({
|
const sharedSecrets = await Secret.find({
|
||||||
@@ -655,6 +699,7 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
validateSecrets,
|
||||||
v1PushSecrets,
|
v1PushSecrets,
|
||||||
v2PushSecrets,
|
v2PushSecrets,
|
||||||
pullSecrets,
|
pullSecrets,
|
||||||
|
|||||||
@@ -9,14 +9,9 @@ import {
|
|||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_HEROKU_API_URL,
|
INTEGRATION_HEROKU_API_URL,
|
||||||
INTEGRATION_VERCEL_API_URL,
|
INTEGRATION_VERCEL_API_URL,
|
||||||
INTEGRATION_NETLIFY_API_URL,
|
INTEGRATION_NETLIFY_API_URL
|
||||||
INTEGRATION_GITHUB_API_URL
|
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
|
|
||||||
interface GitHubApp {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return list of names of apps for integration named [integration]
|
* Return list of names of apps for integration named [integration]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
@@ -47,6 +42,7 @@ const getApps = async ({
|
|||||||
break;
|
break;
|
||||||
case INTEGRATION_VERCEL:
|
case INTEGRATION_VERCEL:
|
||||||
apps = await getAppsVercel({
|
apps = await getAppsVercel({
|
||||||
|
integrationAuth,
|
||||||
accessToken
|
accessToken
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@@ -110,14 +106,25 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
* @returns {Object[]} apps - names of Vercel apps
|
* @returns {Object[]} apps - names of Vercel apps
|
||||||
* @returns {String} apps.name - name of Vercel app
|
* @returns {String} apps.name - name of Vercel app
|
||||||
*/
|
*/
|
||||||
const getAppsVercel = async ({ accessToken }: { accessToken: string }) => {
|
const getAppsVercel = async ({
|
||||||
|
integrationAuth,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integrationAuth: IIntegrationAuth;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
let apps;
|
let apps;
|
||||||
try {
|
try {
|
||||||
const res = (
|
const res = (
|
||||||
await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
|
await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
},
|
||||||
|
...( integrationAuth?.teamId ? {
|
||||||
|
params: {
|
||||||
|
teamId: integrationAuth.teamId
|
||||||
}
|
}
|
||||||
|
} : {})
|
||||||
})
|
})
|
||||||
).data;
|
).data;
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ import {
|
|||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
INTEGRATION_VERCEL_TOKEN_URL,
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||||
INTEGRATION_GITHUB_TOKEN_URL,
|
INTEGRATION_GITHUB_TOKEN_URL
|
||||||
INTEGRATION_GITHUB_API_URL
|
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
import {
|
import {
|
||||||
SITE_URL,
|
SITE_URL,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import axios from 'axios';
|
import axios, { AxiosError } from 'axios';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Octokit } from '@octokit/rest';
|
import { Octokit } from '@octokit/rest';
|
||||||
// import * as sodium from 'libsodium-wrappers';
|
// import * as sodium from 'libsodium-wrappers';
|
||||||
@@ -12,14 +12,10 @@ import {
|
|||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_HEROKU_API_URL,
|
INTEGRATION_HEROKU_API_URL,
|
||||||
INTEGRATION_VERCEL_API_URL,
|
INTEGRATION_VERCEL_API_URL,
|
||||||
INTEGRATION_NETLIFY_API_URL,
|
INTEGRATION_NETLIFY_API_URL
|
||||||
INTEGRATION_GITHUB_API_URL
|
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
import { access, appendFile } from 'fs';
|
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]
|
* Sync/push [secrets] to [app] in integration named [integration]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
@@ -53,6 +49,7 @@ const syncSecrets = async ({
|
|||||||
case INTEGRATION_VERCEL:
|
case INTEGRATION_VERCEL:
|
||||||
await syncSecretsVercel({
|
await syncSecretsVercel({
|
||||||
integration,
|
integration,
|
||||||
|
integrationAuth,
|
||||||
secrets,
|
secrets,
|
||||||
accessToken
|
accessToken
|
||||||
});
|
});
|
||||||
@@ -139,14 +136,15 @@ const syncSecretsHeroku = async ({
|
|||||||
*/
|
*/
|
||||||
const syncSecretsVercel = async ({
|
const syncSecretsVercel = async ({
|
||||||
integration,
|
integration,
|
||||||
|
integrationAuth,
|
||||||
secrets,
|
secrets,
|
||||||
accessToken
|
accessToken
|
||||||
}: {
|
}: {
|
||||||
integration: IIntegration,
|
integration: IIntegration,
|
||||||
|
integrationAuth: IIntegrationAuth,
|
||||||
secrets: any;
|
secrets: any;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
interface VercelSecret {
|
interface VercelSecret {
|
||||||
id?: string;
|
id?: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -156,129 +154,135 @@ const syncSecretsVercel = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get all (decrypted) secrets back from Vercel in
|
// Get all (decrypted) secrets back from Vercel in
|
||||||
// decrypted format
|
// decrypted format
|
||||||
const params = new URLSearchParams({
|
const params: { [key: string]: string } = {
|
||||||
decrypt: "true"
|
decrypt: 'true',
|
||||||
});
|
...( integrationAuth?.teamId ? {
|
||||||
|
teamId: integrationAuth.teamId
|
||||||
|
} : {})
|
||||||
|
}
|
||||||
|
|
||||||
const res = (await Promise.all((await axios.get(
|
const res = (await Promise.all((await axios.get(
|
||||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
|
`${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}`,
|
||||||
|
{
|
||||||
params,
|
params,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
))
|
)).data)
|
||||||
.data
|
)).reduce((obj: any, secret: any) => ({
|
||||||
.envs
|
...obj,
|
||||||
.filter((secret: VercelSecret) => secret.target.includes(integration.target))
|
[secret.key]: secret
|
||||||
.map(async (secret: VercelSecret) => (await axios.get(
|
}), {});
|
||||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
|
||||||
{
|
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,
|
||||||
|
{
|
||||||
|
params,
|
||||||
|
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,
|
||||||
|
{
|
||||||
|
params,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
// Delete secrets
|
||||||
)).data)
|
if (deleteSecrets.length > 0) {
|
||||||
)).reduce((obj: any, secret: any) => ({
|
deleteSecrets.forEach(async (secret: VercelSecret) => {
|
||||||
...obj,
|
await axios.delete(
|
||||||
[secret.key]: secret
|
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||||
}), {});
|
{
|
||||||
|
params,
|
||||||
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: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
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) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to sync secrets to Vercel');
|
throw new Error('Failed to sync secrets to Vercel');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,188 +306,188 @@ const syncSecretsNetlify = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
interface NetlifyValue {
|
interface NetlifyValue {
|
||||||
id?: string;
|
id?: string;
|
||||||
context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production',
|
context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production',
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NetlifySecret {
|
interface NetlifySecret {
|
||||||
key: string;
|
key: string;
|
||||||
values: NetlifyValue[];
|
values: NetlifyValue[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NetlifySecretsRes {
|
interface NetlifySecretsRes {
|
||||||
[index: string]: NetlifySecret;
|
[index: string]: NetlifySecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getParams = new URLSearchParams({
|
const getParams = new URLSearchParams({
|
||||||
context_name: 'all', // integration.context or all
|
context_name: 'all', // integration.context or all
|
||||||
site_id: integration.siteId
|
site_id: integration.siteId
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = (await axios.get(
|
const res = (await axios.get(
|
||||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||||
{
|
{
|
||||||
params: getParams,
|
params: getParams,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
.data
|
.data
|
||||||
.reduce((obj: any, secret: any) => ({
|
.reduce((obj: any, secret: any) => ({
|
||||||
...obj,
|
...obj,
|
||||||
[secret.key]: secret
|
[secret.key]: secret
|
||||||
}), {});
|
}), {});
|
||||||
|
|
||||||
const newSecrets: NetlifySecret[] = []; // createEnvVars
|
const newSecrets: NetlifySecret[] = []; // createEnvVars
|
||||||
const deleteSecrets: string[] = []; // deleteEnvVar
|
const deleteSecrets: string[] = []; // deleteEnvVar
|
||||||
const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue
|
const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue
|
||||||
const updateSecrets: NetlifySecret[] = []; // setEnvVarValue
|
const updateSecrets: NetlifySecret[] = []; // setEnvVarValue
|
||||||
|
|
||||||
// identify secrets to create and update
|
// identify secrets to create and update
|
||||||
Object.keys(secrets).map((key) => {
|
Object.keys(secrets).map((key) => {
|
||||||
if (!(key in res)) {
|
if (!(key in res)) {
|
||||||
// case: Infisical secret does not exist in Netlify -> create secret
|
// case: Infisical secret does not exist in Netlify -> create secret
|
||||||
newSecrets.push({
|
newSecrets.push({
|
||||||
key,
|
key,
|
||||||
values: [{
|
values: [{
|
||||||
value: secrets[key],
|
value: secrets[key],
|
||||||
context: integration.context
|
context: integration.context
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// case: Infisical secret exists in Netlify
|
// case: Infisical secret exists in Netlify
|
||||||
const contexts = res[key].values
|
const contexts = res[key].values
|
||||||
.reduce((obj: any, value: NetlifyValue) => ({
|
.reduce((obj: any, value: NetlifyValue) => ({
|
||||||
...obj,
|
...obj,
|
||||||
[value.context]: value
|
[value.context]: value
|
||||||
}), {});
|
}), {});
|
||||||
|
|
||||||
if (integration.context in contexts) {
|
if (integration.context in contexts) {
|
||||||
// case: Netlify secret value exists in integration context
|
// case: Netlify secret value exists in integration context
|
||||||
if (secrets[key] !== contexts[integration.context].value) {
|
if (secrets[key] !== contexts[integration.context].value) {
|
||||||
// case: Infisical and Netlify secret values are different
|
// case: Infisical and Netlify secret values are different
|
||||||
// -> update Netlify secret context and value
|
// -> update Netlify secret context and value
|
||||||
updateSecrets.push({
|
updateSecrets.push({
|
||||||
key,
|
key,
|
||||||
values: [{
|
values: [{
|
||||||
context: integration.context,
|
context: integration.context,
|
||||||
value: secrets[key]
|
value: secrets[key]
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// case: Netlify secret value does not exist in integration context
|
// case: Netlify secret value does not exist in integration context
|
||||||
// -> add the new Netlify secret context and value
|
// -> add the new Netlify secret context and value
|
||||||
updateSecrets.push({
|
updateSecrets.push({
|
||||||
key,
|
key,
|
||||||
values: [{
|
values: [{
|
||||||
context: integration.context,
|
context: integration.context,
|
||||||
value: secrets[key]
|
value: secrets[key]
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// identify secrets to delete
|
// identify secrets to delete
|
||||||
// TODO: revise (patch case where 1 context was deleted but others still there
|
// TODO: revise (patch case where 1 context was deleted but others still there
|
||||||
Object.keys(res).map((key) => {
|
Object.keys(res).map((key) => {
|
||||||
// loop through each key's context
|
// loop through each key's context
|
||||||
if (!(key in secrets)) {
|
if (!(key in secrets)) {
|
||||||
// case: Netlify secret does not exist in Infisical
|
// case: Netlify secret does not exist in Infisical
|
||||||
|
|
||||||
const numberOfValues = res[key].values.length;
|
const numberOfValues = res[key].values.length;
|
||||||
|
|
||||||
res[key].values.forEach((value: NetlifyValue) => {
|
res[key].values.forEach((value: NetlifyValue) => {
|
||||||
if (value.context === integration.context) {
|
if (value.context === integration.context) {
|
||||||
if (numberOfValues <= 1) {
|
if (numberOfValues <= 1) {
|
||||||
// case: Netlify secret value has less than 1 context -> delete secret
|
// case: Netlify secret value has less than 1 context -> delete secret
|
||||||
deleteSecrets.push(key);
|
deleteSecrets.push(key);
|
||||||
} else {
|
} else {
|
||||||
// case: Netlify secret value has more than 1 context -> delete secret value context
|
// case: Netlify secret value has more than 1 context -> delete secret value context
|
||||||
deleteSecretValues.push({
|
deleteSecretValues.push({
|
||||||
key,
|
key,
|
||||||
values: [{
|
values: [{
|
||||||
id: value.id,
|
id: value.id,
|
||||||
context: integration.context,
|
context: integration.context,
|
||||||
value: value.value
|
value: value.value
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const syncParams = new URLSearchParams({
|
const syncParams = new URLSearchParams({
|
||||||
site_id: integration.siteId
|
site_id: integration.siteId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newSecrets.length > 0) {
|
if (newSecrets.length > 0) {
|
||||||
await axios.post(
|
await axios.post(
|
||||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||||
newSecrets,
|
newSecrets,
|
||||||
{
|
{
|
||||||
params: syncParams,
|
params: syncParams,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateSecrets.length > 0) {
|
if (updateSecrets.length > 0) {
|
||||||
updateSecrets.forEach(async (secret: NetlifySecret) => {
|
updateSecrets.forEach(async (secret: NetlifySecret) => {
|
||||||
await axios.patch(
|
await axios.patch(
|
||||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
|
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
|
||||||
{
|
{
|
||||||
context: secret.values[0].context,
|
context: secret.values[0].context,
|
||||||
value: secret.values[0].value
|
value: secret.values[0].value
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
params: syncParams,
|
params: syncParams,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deleteSecrets.length > 0) {
|
if (deleteSecrets.length > 0) {
|
||||||
deleteSecrets.forEach(async (key: string) => {
|
deleteSecrets.forEach(async (key: string) => {
|
||||||
await axios.delete(
|
await axios.delete(
|
||||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`,
|
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`,
|
||||||
{
|
{
|
||||||
params: syncParams,
|
params: syncParams,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deleteSecretValues.length > 0) {
|
if (deleteSecretValues.length > 0) {
|
||||||
deleteSecretValues.forEach(async (secret: NetlifySecret) => {
|
deleteSecretValues.forEach(async (secret: NetlifySecret) => {
|
||||||
await axios.delete(
|
await axios.delete(
|
||||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`,
|
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`,
|
||||||
{
|
{
|
||||||
params: syncParams,
|
params: syncParams,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to sync secrets to Heroku');
|
throw new Error('Failed to sync secrets to Heroku');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizati
|
|||||||
import requireServiceTokenAuth from './requireServiceTokenAuth';
|
import requireServiceTokenAuth from './requireServiceTokenAuth';
|
||||||
import requireServiceTokenDataAuth from './requireServiceTokenDataAuth';
|
import requireServiceTokenDataAuth from './requireServiceTokenDataAuth';
|
||||||
import requireSecretAuth from './requireSecretAuth';
|
import requireSecretAuth from './requireSecretAuth';
|
||||||
|
import requireSecretsAuth from './requireSecretsAuth';
|
||||||
import validateRequest from './validateRequest';
|
import validateRequest from './validateRequest';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -21,5 +22,6 @@ export {
|
|||||||
requireServiceTokenAuth,
|
requireServiceTokenAuth,
|
||||||
requireServiceTokenDataAuth,
|
requireServiceTokenDataAuth,
|
||||||
requireSecretAuth,
|
requireSecretAuth,
|
||||||
|
requireSecretsAuth,
|
||||||
validateRequest
|
validateRequest
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import {
|
|||||||
validateMembership
|
validateMembership
|
||||||
} from '../helpers/membership';
|
} from '../helpers/membership';
|
||||||
|
|
||||||
|
// note: used for old /v1/secret and /v2/secret routes.
|
||||||
|
// newer /v2/secrets routes use [requireSecretsAuth] middleware
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate if user on request has proper membership to modify secret.
|
* Validate if user on request has proper membership to modify secret.
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
@@ -34,7 +37,7 @@ const requireSecretAuth = ({
|
|||||||
acceptedRoles
|
acceptedRoles
|
||||||
});
|
});
|
||||||
|
|
||||||
req.secret = secret as any;
|
req._secret = secret;
|
||||||
|
|
||||||
next();
|
next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
49
backend/src/middleware/requireSecretsAuth.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { UnauthorizedRequestError } from '../utils/errors';
|
||||||
|
import { Secret, Membership } from '../models';
|
||||||
|
import { validateSecrets } from '../helpers/secret';
|
||||||
|
|
||||||
|
// TODO: make this work for delete route
|
||||||
|
|
||||||
|
const requireSecretsAuth = ({
|
||||||
|
acceptedRoles
|
||||||
|
}: {
|
||||||
|
acceptedRoles: string[];
|
||||||
|
}) => {
|
||||||
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
let secrets;
|
||||||
|
try {
|
||||||
|
if (Array.isArray(req.body.secrets)) {
|
||||||
|
// case: validate multiple secrets
|
||||||
|
secrets = await validateSecrets({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
secretIds: req.body.secrets.map((s: any) => s.id)
|
||||||
|
});
|
||||||
|
} else if (typeof req.body.secrets === 'object') { // change this to check for object
|
||||||
|
// case: validate 1 secret
|
||||||
|
secrets = await validateSecrets({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
secretIds: req.body.secrets.id
|
||||||
|
});
|
||||||
|
} else if (Array.isArray(req.body.secretIds)) {
|
||||||
|
secrets = await validateSecrets({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
secretIds: req.body.secretIds
|
||||||
|
});
|
||||||
|
} else if (typeof req.body.secretIds === 'string') {
|
||||||
|
// case: validate secretIds
|
||||||
|
secrets = await validateSecrets({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
secretIds: [req.body.secretIds]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
req.secrets = secrets;
|
||||||
|
return next();
|
||||||
|
} catch (err) {
|
||||||
|
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret(s)' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default requireSecretsAuth;
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
ENV_DEV,
|
|
||||||
ENV_TESTING,
|
|
||||||
ENV_STAGING,
|
|
||||||
ENV_PROD,
|
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
@@ -13,7 +9,7 @@ import {
|
|||||||
export interface IIntegration {
|
export interface IIntegration {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
workspace: Types.ObjectId;
|
workspace: Types.ObjectId;
|
||||||
environment: 'dev' | 'test' | 'staging' | 'prod';
|
environment: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
app: string;
|
app: string;
|
||||||
target: string;
|
target: string;
|
||||||
@@ -32,7 +28,6 @@ const integrationSchema = new Schema<IIntegration>(
|
|||||||
},
|
},
|
||||||
environment: {
|
environment: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
isActive: {
|
isActive: {
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ import { Schema, model, Types } from 'mongoose';
|
|||||||
import {
|
import {
|
||||||
SECRET_SHARED,
|
SECRET_SHARED,
|
||||||
SECRET_PERSONAL,
|
SECRET_PERSONAL,
|
||||||
ENV_DEV,
|
|
||||||
ENV_TESTING,
|
|
||||||
ENV_STAGING,
|
|
||||||
ENV_PROD
|
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
|
|
||||||
export interface ISecret {
|
export interface ISecret {
|
||||||
@@ -53,7 +49,6 @@ const secretSchema = new Schema<ISecret>(
|
|||||||
},
|
},
|
||||||
environment: {
|
environment: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
secretKeyCiphertext: {
|
secretKeyCiphertext: {
|
||||||
@@ -69,8 +64,7 @@ const secretSchema = new Schema<ISecret>(
|
|||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
secretKeyHash: {
|
secretKeyHash: {
|
||||||
type: String,
|
type: String
|
||||||
required: true
|
|
||||||
},
|
},
|
||||||
secretValueCiphertext: {
|
secretValueCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -85,8 +79,7 @@ const secretSchema = new Schema<ISecret>(
|
|||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
secretValueHash: {
|
secretValueHash: {
|
||||||
type: String,
|
type: String
|
||||||
required: true
|
|
||||||
},
|
},
|
||||||
secretCommentCiphertext: {
|
secretCommentCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from 'mongoose';
|
||||||
import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables';
|
|
||||||
|
|
||||||
// TODO: deprecate
|
|
||||||
export interface IServiceToken {
|
export interface IServiceToken {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -33,7 +30,6 @@ const serviceTokenSchema = new Schema<IServiceToken>(
|
|||||||
},
|
},
|
||||||
environment: {
|
environment: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
expiresAt: {
|
expiresAt: {
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ export interface IWorkspace {
|
|||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
name: string;
|
name: string;
|
||||||
organization: Types.ObjectId;
|
organization: Types.ObjectId;
|
||||||
|
environments: Array<{
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaceSchema = new Schema<IWorkspace>({
|
const workspaceSchema = new Schema<IWorkspace>({
|
||||||
@@ -15,7 +19,33 @@ const workspaceSchema = new Schema<IWorkspace>({
|
|||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Organization',
|
ref: 'Organization',
|
||||||
required: true
|
required: true
|
||||||
}
|
},
|
||||||
|
environments: {
|
||||||
|
type: [
|
||||||
|
{
|
||||||
|
name: String,
|
||||||
|
slug: String,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: [
|
||||||
|
{
|
||||||
|
name: "development",
|
||||||
|
slug: "dev"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test",
|
||||||
|
slug: "test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "staging",
|
||||||
|
slug: "staging"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "production",
|
||||||
|
slug: "prod"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const Workspace = model<IWorkspace>('Workspace', workspaceSchema);
|
const Workspace = model<IWorkspace>('Workspace', workspaceSchema);
|
||||||
|
|||||||
5
backend/src/routes/status/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import healthCheck from './status';
|
||||||
|
|
||||||
|
export {
|
||||||
|
healthCheck
|
||||||
|
}
|
||||||
15
backend/src/routes/status/status.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import express, { Request, Response } from 'express';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/status',
|
||||||
|
(req: Request, res: Response) => {
|
||||||
|
res.status(200).json({
|
||||||
|
date: new Date(),
|
||||||
|
message: 'Ok',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router
|
||||||
57
backend/src/routes/v2/environment.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import express, { Response, Request } from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import { body, param } from 'express-validator';
|
||||||
|
import { environmentController } from '../../controllers/v2';
|
||||||
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth,
|
||||||
|
validateRequest,
|
||||||
|
} from '../../middleware';
|
||||||
|
import { ADMIN, MEMBER } from '../../variables';
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:workspaceId/environments',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt'],
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
body('environmentSlug').exists().trim(),
|
||||||
|
body('environmentName').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
environmentController.createWorkspaceEnvironment
|
||||||
|
);
|
||||||
|
|
||||||
|
router.put(
|
||||||
|
'/:workspaceId/environments',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt'],
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
body('environmentSlug').exists().trim(),
|
||||||
|
body('environmentName').exists().trim(),
|
||||||
|
body('oldEnvironmentSlug').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
environmentController.renameWorkspaceEnvironment
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
'/:workspaceId/environments',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt'],
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN],
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
body('environmentSlug').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
environmentController.deleteWorkspaceEnvironment
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
import secret from './secret';
|
import secret from './secret'; // stop-supporting
|
||||||
|
import secrets from './secrets';
|
||||||
import workspace from './workspace';
|
import workspace from './workspace';
|
||||||
import serviceTokenData from './serviceTokenData';
|
import serviceTokenData from './serviceTokenData';
|
||||||
import apiKeyData from './apiKeyData';
|
import apiKeyData from './apiKeyData';
|
||||||
|
import environment from "./environment"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
secret,
|
secret,
|
||||||
|
secrets,
|
||||||
workspace,
|
workspace,
|
||||||
serviceTokenData,
|
serviceTokenData,
|
||||||
apiKeyData
|
apiKeyData,
|
||||||
|
environment
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,21 @@
|
|||||||
import express, { Request, Response } from 'express';
|
import express from 'express';
|
||||||
import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware';
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth,
|
||||||
|
requireSecretAuth,
|
||||||
|
validateRequest
|
||||||
|
} from '../../middleware';
|
||||||
import { body, param, query } from 'express-validator';
|
import { body, param, query } from 'express-validator';
|
||||||
import { ADMIN, MEMBER } from '../../variables';
|
import { ADMIN, MEMBER } from '../../variables';
|
||||||
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
|
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
|
||||||
import { secretController } from '../../controllers/v2';
|
import { secretController } from '../../controllers/v2';
|
||||||
import { fetchAllSecrets } from '../../controllers/v2/secretController';
|
|
||||||
|
// note to devs: stop supporting
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
/**
|
|
||||||
* Create many secrets for a given workspace and environmentName
|
|
||||||
*/
|
|
||||||
router.post(
|
router.post(
|
||||||
'/batch-create/workspace/:workspaceId/environment/:environmentName',
|
'/batch-create/workspace/:workspaceId/environment/:environment',
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt']
|
acceptedAuthModes: ['jwt']
|
||||||
}),
|
}),
|
||||||
@@ -20,15 +23,29 @@ router.post(
|
|||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
}),
|
}),
|
||||||
param('workspaceId').exists().isMongoId().trim(),
|
param('workspaceId').exists().isMongoId().trim(),
|
||||||
param('environmentName').exists().trim(),
|
param('environment').exists().trim(),
|
||||||
body('secrets').exists().isArray().custom((value) => value.every((item: CreateSecretRequestBody) => typeof item === 'object')),
|
body('secrets').exists().isArray().custom((value) => value.every((item: CreateSecretRequestBody) => typeof item === 'object')),
|
||||||
|
body('channel'),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
secretController.batchCreateSecrets
|
secretController.createSecrets
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/workspace/:workspaceId/environment/:environment',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().isMongoId().trim(),
|
||||||
|
param('environment').exists().trim(),
|
||||||
|
body('secret').exists().isObject(),
|
||||||
|
body('channel'),
|
||||||
|
validateRequest,
|
||||||
|
secretController.createSecret
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all secrets for a given environment and workspace id
|
|
||||||
*/
|
|
||||||
router.get(
|
router.get(
|
||||||
'/workspace/:workspaceId',
|
'/workspace/:workspaceId',
|
||||||
param('workspaceId').exists().trim(),
|
param('workspaceId').exists().trim(),
|
||||||
@@ -39,13 +56,23 @@ router.get(
|
|||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
}),
|
}),
|
||||||
|
query('channel'),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
fetchAllSecrets
|
secretController.getSecrets
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:secretId',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt', 'serviceToken']
|
||||||
|
}),
|
||||||
|
requireSecretAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
|
}),
|
||||||
|
validateRequest,
|
||||||
|
secretController.getSecret
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Batch delete secrets in a given workspace and environment name
|
|
||||||
*/
|
|
||||||
router.delete(
|
router.delete(
|
||||||
'/batch/workspace/:workspaceId/environment/:environmentName',
|
'/batch/workspace/:workspaceId/environment/:environmentName',
|
||||||
requireAuth({
|
requireAuth({
|
||||||
@@ -58,13 +85,22 @@ router.delete(
|
|||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
}),
|
}),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
secretController.batchDeleteSecrets
|
secretController.deleteSecrets
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
'/:secretId',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
requireSecretAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
|
}),
|
||||||
|
param('secretId').isMongoId(),
|
||||||
|
validateRequest,
|
||||||
|
secretController.deleteSecret
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply modifications to many existing secrets in a given workspace and environment
|
|
||||||
*/
|
|
||||||
router.patch(
|
router.patch(
|
||||||
'/batch-modify/workspace/:workspaceId/environment/:environmentName',
|
'/batch-modify/workspace/:workspaceId/environment/:environmentName',
|
||||||
requireAuth({
|
requireAuth({
|
||||||
@@ -77,7 +113,23 @@ router.patch(
|
|||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
}),
|
}),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
secretController.batchModifySecrets
|
secretController.updateSecrets
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
'/workspace/:workspaceId/environment/:environmentName',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
body('secret').isObject(),
|
||||||
|
param('workspaceId').exists().isMongoId().trim(),
|
||||||
|
param('environmentName').exists().trim(),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
|
}),
|
||||||
|
validateRequest,
|
||||||
|
secretController.updateSecret
|
||||||
);
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
157
backend/src/routes/v2/secrets.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth,
|
||||||
|
requireSecretsAuth,
|
||||||
|
validateRequest
|
||||||
|
} from '../../middleware';
|
||||||
|
import { query, check, body } from 'express-validator';
|
||||||
|
import { secretsController } from '../../controllers/v2';
|
||||||
|
import {
|
||||||
|
ADMIN,
|
||||||
|
MEMBER,
|
||||||
|
SECRET_PERSONAL,
|
||||||
|
SECRET_SHARED
|
||||||
|
} from '../../variables';
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/',
|
||||||
|
body('workspaceId').exists().isString().trim(),
|
||||||
|
body('environment').exists().isString().trim(),
|
||||||
|
body('secrets')
|
||||||
|
.exists()
|
||||||
|
.custom((value) => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
// case: create multiple secrets
|
||||||
|
if (value.length === 0) throw new Error('secrets cannot be an empty array')
|
||||||
|
for (const secret of value) {
|
||||||
|
if (
|
||||||
|
!secret.type ||
|
||||||
|
!(secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED) ||
|
||||||
|
!secret.secretKeyCiphertext ||
|
||||||
|
!secret.secretKeyIV ||
|
||||||
|
!secret.secretKeyTag ||
|
||||||
|
!secret.secretValueCiphertext ||
|
||||||
|
!secret.secretValueIV ||
|
||||||
|
!secret.secretValueTag
|
||||||
|
) {
|
||||||
|
throw new Error('secrets array must contain objects that have required secret properties');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
// case: update 1 secret
|
||||||
|
if (
|
||||||
|
!value.type ||
|
||||||
|
!(value.type === SECRET_PERSONAL || value.type === SECRET_SHARED) ||
|
||||||
|
!value.secretKeyCiphertext ||
|
||||||
|
!value.secretKeyIV ||
|
||||||
|
!value.secretKeyTag ||
|
||||||
|
!value.secretValueCiphertext ||
|
||||||
|
!value.secretValueIV ||
|
||||||
|
!value.secretValueTag
|
||||||
|
) {
|
||||||
|
throw new Error('secrets object is missing required secret properties');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('secrets must be an object or an array of objects')
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
validateRequest,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
location: 'body'
|
||||||
|
}),
|
||||||
|
secretsController.createSecrets
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
query('workspaceId').exists().trim(),
|
||||||
|
query('environment').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt', 'serviceToken']
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
location: 'query'
|
||||||
|
}),
|
||||||
|
secretsController.getSecrets
|
||||||
|
);
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
'/',
|
||||||
|
body('secrets')
|
||||||
|
.exists()
|
||||||
|
.custom((value) => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
// case: update multiple secrets
|
||||||
|
if (value.length === 0) throw new Error('secrets cannot be an empty array')
|
||||||
|
for (const secret of value) {
|
||||||
|
if (
|
||||||
|
!secret.id
|
||||||
|
) {
|
||||||
|
throw new Error('Each secret must contain a ID property');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
// case: update 1 secret
|
||||||
|
if (
|
||||||
|
!value.id
|
||||||
|
) {
|
||||||
|
throw new Error('secret must contain a ID property');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('secrets must be an object or an array of objects')
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
validateRequest,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
requireSecretsAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
|
}),
|
||||||
|
secretsController.updateSecrets
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
'/',
|
||||||
|
body('secretIds')
|
||||||
|
.exists()
|
||||||
|
.custom((value) => {
|
||||||
|
// case: delete 1 secret
|
||||||
|
if (typeof value === 'string') return true;
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
// case: delete multiple secrets
|
||||||
|
if (value.length === 0) throw new Error('secrets cannot be an empty array');
|
||||||
|
return value.every((id: string) => typeof id === 'string')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('secretIds must be a string or an array of strings');
|
||||||
|
})
|
||||||
|
.not()
|
||||||
|
.isEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
requireSecretsAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
|
}),
|
||||||
|
secretsController.deleteSecrets
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -11,10 +11,6 @@ import {
|
|||||||
setIntegrationAuthAccessHelper,
|
setIntegrationAuthAccessHelper,
|
||||||
} from '../helpers/integration';
|
} from '../helpers/integration';
|
||||||
import { exchangeCode } from '../integrations';
|
import { exchangeCode } from '../integrations';
|
||||||
import {
|
|
||||||
ENV_DEV,
|
|
||||||
EVENT_PUSH_SECRETS
|
|
||||||
} from '../variables';
|
|
||||||
|
|
||||||
// should sync stuff be here too? Probably.
|
// should sync stuff be here too? Probably.
|
||||||
// TODO: move bot functions to IntegrationService.
|
// TODO: move bot functions to IntegrationService.
|
||||||
@@ -32,22 +28,26 @@ class IntegrationService {
|
|||||||
* - Create bot sequence for integration
|
* - Create bot sequence for integration
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String} obj.workspaceId - id of workspace
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @param {String} obj.environment - workspace environment
|
||||||
* @param {String} obj.integration - name of integration
|
* @param {String} obj.integration - name of integration
|
||||||
* @param {String} obj.code - code
|
* @param {String} obj.code - code
|
||||||
*/
|
*/
|
||||||
static async handleOAuthExchange({
|
static async handleOAuthExchange({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
integration,
|
integration,
|
||||||
code
|
code,
|
||||||
|
environment
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
integration: string;
|
integration: string;
|
||||||
code: string;
|
code: string;
|
||||||
|
environment: string;
|
||||||
}) {
|
}) {
|
||||||
await handleOAuthExchangeHelper({
|
await handleOAuthExchangeHelper({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
integration,
|
integration,
|
||||||
code
|
code,
|
||||||
|
environment
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import {
|
|||||||
} from '../config';
|
} from '../config';
|
||||||
import { getLogger } from '../utils/logger';
|
import { getLogger } from '../utils/logger';
|
||||||
|
|
||||||
if(TELEMETRY_ENABLED){
|
if(!TELEMETRY_ENABLED){
|
||||||
getLogger("backend-main").info([
|
getLogger("backend-main").info([
|
||||||
"",
|
"",
|
||||||
"Infisical collects telemetry data about general usage.",
|
"To improve, 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.",
|
"This 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 as we support Infisical as open-source software.",
|
||||||
"To opt out of telemetry, you can set `TELEMETRY_ENABLED=false` within the environment variables",
|
"To opt into telemetry, you can set `TELEMETRY_ENABLED=true` within the environment variables.",
|
||||||
].join('\n'))
|
].join('\n'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,13 @@ if (SMTP_SECURE) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
mailOpts.secure = true;
|
if (SMTP_HOST.includes('amazonaws.com')) {
|
||||||
|
mailOpts.tls = {
|
||||||
|
ciphers: 'TLSv1.2'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mailOpts.secure = true;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
backend/src/types/express/index.d.ts
vendored
@@ -1,4 +1,5 @@
|
|||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
|
import { ISecret } from '../../models';
|
||||||
|
|
||||||
// TODO: fix (any) types
|
// TODO: fix (any) types
|
||||||
declare global {
|
declare global {
|
||||||
@@ -12,7 +13,8 @@ declare global {
|
|||||||
integration: any;
|
integration: any;
|
||||||
integrationAuth: any;
|
integrationAuth: any;
|
||||||
bot: any;
|
bot: any;
|
||||||
secret: any;
|
_secret: any;
|
||||||
|
secrets: any;
|
||||||
secretSnapshot: any;
|
secretSnapshot: any;
|
||||||
serviceToken: any;
|
serviceToken: any;
|
||||||
accessToken: any;
|
accessToken: any;
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
INTEGRATION_HEROKU_API_URL,
|
INTEGRATION_HEROKU_API_URL,
|
||||||
INTEGRATION_VERCEL_API_URL,
|
INTEGRATION_VERCEL_API_URL,
|
||||||
INTEGRATION_NETLIFY_API_URL,
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
INTEGRATION_GITHUB_API_URL,
|
|
||||||
INTEGRATION_OPTIONS
|
INTEGRATION_OPTIONS
|
||||||
} from './integration';
|
} from './integration';
|
||||||
import {
|
import {
|
||||||
@@ -66,7 +65,6 @@ export {
|
|||||||
INTEGRATION_HEROKU_API_URL,
|
INTEGRATION_HEROKU_API_URL,
|
||||||
INTEGRATION_VERCEL_API_URL,
|
INTEGRATION_VERCEL_API_URL,
|
||||||
INTEGRATION_NETLIFY_API_URL,
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
INTEGRATION_GITHUB_API_URL,
|
|
||||||
EVENT_PUSH_SECRETS,
|
EVENT_PUSH_SECRETS,
|
||||||
EVENT_PULL_SECRETS,
|
EVENT_PULL_SECRETS,
|
||||||
ACTION_ADD_SECRETS,
|
ACTION_ADD_SECRETS,
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ const INTEGRATION_VERCEL = 'vercel';
|
|||||||
const INTEGRATION_NETLIFY = 'netlify';
|
const INTEGRATION_NETLIFY = 'netlify';
|
||||||
const INTEGRATION_GITHUB = 'github';
|
const INTEGRATION_GITHUB = 'github';
|
||||||
const INTEGRATION_SET = new Set([
|
const INTEGRATION_SET = new Set([
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB
|
INTEGRATION_GITHUB
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// integration types
|
// integration types
|
||||||
@@ -23,16 +23,15 @@ const INTEGRATION_OAUTH2 = 'oauth2';
|
|||||||
// integration oauth endpoints
|
// integration oauth endpoints
|
||||||
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
|
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
|
||||||
const INTEGRATION_VERCEL_TOKEN_URL =
|
const INTEGRATION_VERCEL_TOKEN_URL =
|
||||||
'https://api.vercel.com/v2/oauth/access_token';
|
'https://api.vercel.com/v2/oauth/access_token';
|
||||||
const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token';
|
const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token';
|
||||||
const INTEGRATION_GITHUB_TOKEN_URL =
|
const INTEGRATION_GITHUB_TOKEN_URL =
|
||||||
'https://github.com/login/oauth/access_token';
|
'https://github.com/login/oauth/access_token';
|
||||||
|
|
||||||
// integration apps endpoints
|
// integration apps endpoints
|
||||||
const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com';
|
const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com';
|
||||||
const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com';
|
const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com';
|
||||||
const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com';
|
const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com';
|
||||||
const INTEGRATION_GITHUB_API_URL = 'https://api.github.com';
|
|
||||||
|
|
||||||
const INTEGRATION_OPTIONS = [
|
const INTEGRATION_OPTIONS = [
|
||||||
{
|
{
|
||||||
@@ -48,7 +47,7 @@ const INTEGRATION_OPTIONS = [
|
|||||||
name: 'Vercel',
|
name: 'Vercel',
|
||||||
slug: 'vercel',
|
slug: 'vercel',
|
||||||
image: 'Vercel',
|
image: 'Vercel',
|
||||||
isAvailable: false,
|
isAvailable: true,
|
||||||
type: 'vercel',
|
type: 'vercel',
|
||||||
clientId: '',
|
clientId: '',
|
||||||
clientSlug: CLIENT_SLUG_VERCEL,
|
clientSlug: CLIENT_SLUG_VERCEL,
|
||||||
@@ -121,19 +120,18 @@ const INTEGRATION_OPTIONS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export {
|
export {
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
INTEGRATION_VERCEL_TOKEN_URL,
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||||
INTEGRATION_GITHUB_TOKEN_URL,
|
INTEGRATION_GITHUB_TOKEN_URL,
|
||||||
INTEGRATION_HEROKU_API_URL,
|
INTEGRATION_HEROKU_API_URL,
|
||||||
INTEGRATION_VERCEL_API_URL,
|
INTEGRATION_VERCEL_API_URL,
|
||||||
INTEGRATION_NETLIFY_API_URL,
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
INTEGRATION_GITHUB_API_URL,
|
INTEGRATION_OPTIONS
|
||||||
INTEGRATION_OPTIONS
|
|
||||||
};
|
};
|
||||||
|
|||||||
22
backend/swagger.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' });
|
||||||
|
|
||||||
|
const doc = {
|
||||||
|
info: {
|
||||||
|
title: 'Infisical API',
|
||||||
|
description: 'List of all available APIs that can be consumed',
|
||||||
|
},
|
||||||
|
host: ['https://infisical.com'],
|
||||||
|
securityDefinitions: {
|
||||||
|
bearerAuth: {
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
bearerFormat: 'JWT'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const outputFile = './api-documentation.json';
|
||||||
|
const endpointsFiles = ['./src/app.ts'];
|
||||||
|
|
||||||
|
swaggerAutogen(outputFile, endpointsFiles, doc);
|
||||||
@@ -31,8 +31,8 @@ var exportCmd = &cobra.Command{
|
|||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
PreRun: func(cmd *cobra.Command, args []string) {
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
toggleDebug(cmd, args)
|
toggleDebug(cmd, args)
|
||||||
util.RequireLogin()
|
// util.RequireLogin()
|
||||||
util.RequireLocalWorkspaceFile()
|
// util.RequireLocalWorkspaceFile()
|
||||||
},
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
envName, err := cmd.Flags().GetString("env")
|
envName, err := cmd.Flags().GetString("env")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -17,6 +18,7 @@ import (
|
|||||||
"github.com/Infisical/infisical-merge/packages/models"
|
"github.com/Infisical/infisical-merge/packages/models"
|
||||||
"github.com/Infisical/infisical-merge/packages/srp"
|
"github.com/Infisical/infisical-merge/packages/srp"
|
||||||
"github.com/Infisical/infisical-merge/packages/util"
|
"github.com/Infisical/infisical-merge/packages/util"
|
||||||
|
"github.com/fatih/color"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
"github.com/manifoldco/promptui"
|
"github.com/manifoldco/promptui"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -31,7 +33,9 @@ var loginCmd = &cobra.Command{
|
|||||||
PreRun: toggleDebug,
|
PreRun: toggleDebug,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
|
currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
|
||||||
if err != nil {
|
if err != nil && strings.Contains(err.Error(), "The specified item could not be found in the keyring") { // if the key can't be found allow them to override
|
||||||
|
log.Debug(err)
|
||||||
|
} else if err != nil {
|
||||||
util.HandleError(err)
|
util.HandleError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +101,7 @@ var loginCmd = &cobra.Command{
|
|||||||
util.HandleError(err, "Unable to write write to Infisical Config file. Please try again")
|
util.HandleError(err, "Unable to write write to Infisical Config file. Please try again")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infoln("Nice! You are loggin as:", email)
|
color.Green("Nice! You are logged in as: %v", email)
|
||||||
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ var rootCmd = &cobra.Command{
|
|||||||
Short: "Infisical CLI is used to inject environment variables into any process",
|
Short: "Infisical CLI is used to inject environment variables into any process",
|
||||||
Long: `Infisical is a simple, end-to-end encrypted service that enables teams to sync and manage their environment variables across their development life cycle.`,
|
Long: `Infisical is a simple, end-to-end encrypted service that enables teams to sync and manage their environment variables across their development life cycle.`,
|
||||||
CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true},
|
CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true},
|
||||||
Version: "0.2.0",
|
Version: "0.2.1",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/Infisical/infisical-merge/packages/models"
|
|
||||||
"github.com/Infisical/infisical-merge/packages/util"
|
"github.com/Infisical/infisical-merge/packages/util"
|
||||||
|
"github.com/fatih/color"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -85,16 +85,50 @@ var runCmd = &cobra.Command{
|
|||||||
secrets = util.OverrideWithPersonalSecrets(secrets)
|
secrets = util.OverrideWithPersonalSecrets(secrets)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
secretsByKey := getSecretsByKeys(secrets)
|
||||||
|
environmentVariables := make(map[string]string)
|
||||||
|
|
||||||
|
// add all existing environment vars
|
||||||
|
for _, s := range os.Environ() {
|
||||||
|
kv := strings.SplitN(s, "=", 2)
|
||||||
|
key := kv[0]
|
||||||
|
value := kv[1]
|
||||||
|
environmentVariables[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// check to see if there are any reserved key words in secrets to inject
|
||||||
|
reservedEnvironmentVariables := []string{"HOME", "PATH", "PS1", "PS2"}
|
||||||
|
for _, reservedEnvName := range reservedEnvironmentVariables {
|
||||||
|
if _, ok := secretsByKey[reservedEnvName]; ok {
|
||||||
|
delete(secretsByKey, reservedEnvName)
|
||||||
|
util.PrintWarning(fmt.Sprintf("Infisical secret named [%v] has been removed because it is a reserved secret name", reservedEnvName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// now add infisical secrets
|
||||||
|
for k, v := range secretsByKey {
|
||||||
|
environmentVariables[k] = v.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// turn it back into a list of envs
|
||||||
|
var env []string
|
||||||
|
for key, value := range environmentVariables {
|
||||||
|
s := key + "=" + value
|
||||||
|
env = append(env, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("injecting the following environment variables into shell: %v", env)
|
||||||
|
|
||||||
if cmd.Flags().Changed("command") {
|
if cmd.Flags().Changed("command") {
|
||||||
command := cmd.Flag("command").Value.String()
|
command := cmd.Flag("command").Value.String()
|
||||||
|
|
||||||
err = executeMultipleCommandWithEnvs(command, secrets)
|
err = executeMultipleCommandWithEnvs(command, len(secretsByKey), env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to execute your chained command")
|
util.HandleError(err, "Unable to execute your chained command")
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
err = executeSingleCommandWithEnvs(args, secrets)
|
err = executeSingleCommandWithEnvs(args, len(secretsByKey), env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to execute your single command")
|
util.HandleError(err, "Unable to execute your single command")
|
||||||
}
|
}
|
||||||
@@ -111,24 +145,21 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Will execute a single command and pass in the given secrets into the process
|
// Will execute a single command and pass in the given secrets into the process
|
||||||
func executeSingleCommandWithEnvs(args []string, secrets []models.SingleEnvironmentVariable) error {
|
func executeSingleCommandWithEnvs(args []string, secretsCount int, env []string) error {
|
||||||
command := args[0]
|
command := args[0]
|
||||||
argsForCommand := args[1:]
|
argsForCommand := args[1:]
|
||||||
numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(secrets))
|
color.Green("Injecting %v Infisical secrets into your application process", secretsCount)
|
||||||
log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected)
|
|
||||||
log.Debugf("executing command: %s %s \n", command, strings.Join(argsForCommand, " "))
|
|
||||||
log.Debugf("Secrets injected: %v", secrets)
|
|
||||||
|
|
||||||
cmd := exec.Command(command, argsForCommand...)
|
cmd := exec.Command(command, argsForCommand...)
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
cmd.Env = getAllEnvs(secrets)
|
cmd.Env = env
|
||||||
|
|
||||||
return execCmd(cmd)
|
return execCmd(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeMultipleCommandWithEnvs(fullCommand string, secrets []models.SingleEnvironmentVariable) error {
|
func executeMultipleCommandWithEnvs(fullCommand string, secretsCount int, env []string) error {
|
||||||
shell := [2]string{"sh", "-c"}
|
shell := [2]string{"sh", "-c"}
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
shell = [2]string{"cmd", "/C"}
|
shell = [2]string{"cmd", "/C"}
|
||||||
@@ -140,12 +171,10 @@ func executeMultipleCommandWithEnvs(fullCommand string, secrets []models.SingleE
|
|||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
cmd.Env = getAllEnvs(secrets)
|
cmd.Env = env
|
||||||
|
|
||||||
numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(secrets))
|
color.Green("Injecting %v Infisical secrets into your application process", secretsCount)
|
||||||
log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected)
|
|
||||||
log.Debugf("executing command: %s %s %s \n", shell[0], shell[1], fullCommand)
|
log.Debugf("executing command: %s %s %s \n", shell[0], shell[1], fullCommand)
|
||||||
log.Debugf("Secrets injected: %v", secrets)
|
|
||||||
|
|
||||||
return execCmd(cmd)
|
return execCmd(cmd)
|
||||||
}
|
}
|
||||||
@@ -175,23 +204,3 @@ func execCmd(cmd *exec.Cmd) error {
|
|||||||
os.Exit(waitStatus.ExitStatus())
|
os.Exit(waitStatus.ExitStatus())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAllEnvs(envsToInject []models.SingleEnvironmentVariable) []string {
|
|
||||||
env_map := make(map[string]string)
|
|
||||||
|
|
||||||
for _, env := range os.Environ() {
|
|
||||||
splitEnv := strings.Split(env, "=")
|
|
||||||
env_map[splitEnv[0]] = splitEnv[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, env := range envsToInject {
|
|
||||||
env_map[env.Key] = env.Value // overrite any envs with ones to inject if they clash
|
|
||||||
}
|
|
||||||
|
|
||||||
var allEnvs []string
|
|
||||||
for key, value := range env_map {
|
|
||||||
allEnvs = append(allEnvs, fmt.Sprintf("%s=%s", key, value))
|
|
||||||
}
|
|
||||||
|
|
||||||
return allEnvs
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -311,14 +311,25 @@ var secretsDeleteCmd = &cobra.Command{
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
secretsCmd.AddCommand(secretsGetCmd)
|
secretsCmd.AddCommand(secretsGetCmd)
|
||||||
secretsCmd.AddCommand(secretsSetCmd)
|
secretsGetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||||
secretsCmd.AddCommand(secretsDeleteCmd)
|
|
||||||
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
|
|
||||||
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
|
||||||
secretsCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
|
||||||
util.RequireLogin()
|
util.RequireLogin()
|
||||||
util.RequireLocalWorkspaceFile()
|
util.RequireLocalWorkspaceFile()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
secretsCmd.AddCommand(secretsSetCmd)
|
||||||
|
secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||||
|
util.RequireLogin()
|
||||||
|
util.RequireLocalWorkspaceFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
secretsCmd.AddCommand(secretsDeleteCmd)
|
||||||
|
secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||||
|
util.RequireLogin()
|
||||||
|
util.RequireLocalWorkspaceFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
|
||||||
|
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
||||||
rootCmd.AddCommand(secretsCmd)
|
rootCmd.AddCommand(secretsCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ func PrintErrorAndExit(exitCode int, err error, messages ...string) {
|
|||||||
os.Exit(exitCode)
|
os.Exit(exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PrintWarning(message string) {
|
||||||
|
color.Yellow("Warning: %v", message)
|
||||||
|
}
|
||||||
|
|
||||||
func PrintMessageAndExit(messages ...string) {
|
func PrintMessageAndExit(messages ...string) {
|
||||||
if len(messages) > 0 {
|
if len(messages) > 0 {
|
||||||
for _, message := range messages {
|
for _, message := range messages {
|
||||||
@@ -33,6 +33,7 @@ func GetKeyRing() (keyring.Keyring, error) {
|
|||||||
LibSecretCollectionName: KEYRING_SERVICE_NAME,
|
LibSecretCollectionName: KEYRING_SERVICE_NAME,
|
||||||
KWalletAppID: KEYRING_SERVICE_NAME,
|
KWalletAppID: KEYRING_SERVICE_NAME,
|
||||||
KWalletFolder: KEYRING_SERVICE_NAME,
|
KWalletFolder: KEYRING_SERVICE_NAME,
|
||||||
|
KeychainName: "login", // default so user will not be prompted
|
||||||
KeychainTrustApplication: true,
|
KeychainTrustApplication: true,
|
||||||
WinCredPrefix: KEYRING_SERVICE_NAME,
|
WinCredPrefix: KEYRING_SERVICE_NAME,
|
||||||
FileDir: fmt.Sprintf("~/%s-file-vault", KEYRING_SERVICE_NAME),
|
FileDir: fmt.Sprintf("~/%s-file-vault", KEYRING_SERVICE_NAME),
|
||||||
@@ -53,9 +54,10 @@ func GetKeyRing() (keyring.Keyring, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fileKeyringPassphrasePrompt(prompt string) (string, error) {
|
func fileKeyringPassphrasePrompt(prompt string) (string, error) {
|
||||||
fmt.Println("You may set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable to avoid typing password")
|
|
||||||
if password, ok := os.LookupEnv("INFISICAL_VAULT_FILE_PASSPHRASE"); ok {
|
if password, ok := os.LookupEnv("INFISICAL_VAULT_FILE_PASSPHRASE"); ok {
|
||||||
return password, nil
|
return password, nil
|
||||||
|
} else {
|
||||||
|
fmt.Println("You may set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable to avoid typing password")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "%s:", prompt)
|
fmt.Fprintf(os.Stderr, "%s:", prompt)
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ services:
|
|||||||
- ./backend/src:/app/src
|
- ./backend/src:/app/src
|
||||||
- ./backend/nodemon.json:/app/nodemon.json
|
- ./backend/nodemon.json:/app/nodemon.json
|
||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
|
- ./backend/api-documentation.json:/app/api-documentation.json
|
||||||
|
- ./backend/swagger.ts:/app/swagger.ts
|
||||||
command: npm run dev
|
command: npm run dev
|
||||||
env_file: .env
|
env_file: .env
|
||||||
environment:
|
environment:
|
||||||
@@ -48,6 +50,7 @@ services:
|
|||||||
- ./frontend/public:/app/public
|
- ./frontend/public:/app/public
|
||||||
- ./frontend/styles:/app/styles
|
- ./frontend/styles:/app/styles
|
||||||
- ./frontend/components:/app/components
|
- ./frontend/components:/app/components
|
||||||
|
- ./frontend/ee:/app/ee
|
||||||
- ./frontend/locales:/app/locales
|
- ./frontend/locales:/app/locales
|
||||||
- ./frontend/next-i18next.config.js:/app/next-i18next.config.js
|
- ./frontend/next-i18next.config.js:/app/next-i18next.config.js
|
||||||
env_file: .env
|
env_file: .env
|
||||||
|
|||||||
4
docs/api-reference/endpoints/secrets/create.mdx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Create"
|
||||||
|
openapi: "POST /api/v2/secrets/"
|
||||||
|
---
|
||||||
4
docs/api-reference/endpoints/secrets/delete.mdx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v2/secrets/"
|
||||||
|
---
|
||||||
4
docs/api-reference/endpoints/secrets/read.mdx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Read"
|
||||||
|
openapi: "GET /api/v2/secrets/"
|
||||||
|
---
|
||||||
4
docs/api-reference/endpoints/secrets/update.mdx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Update"
|
||||||
|
openapi: "PATCH /api/v2/secrets/"
|
||||||
|
---
|
||||||
3
docs/api-reference/overview/authentication.mdx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
title: "Authentication"
|
||||||
|
---
|
||||||
3
docs/api-reference/overview/introduction.mdx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
title: "Introduction"
|
||||||
|
---
|
||||||
@@ -2,8 +2,10 @@
|
|||||||
title: "Activity Logs"
|
title: "Activity Logs"
|
||||||
---
|
---
|
||||||
|
|
||||||
Activity logs record all actions going through Infisical including CRUD operations applied to environment variables. They help answer questions like:
|
Activity logs record all actions going through Infisical including who performed which CRUD operations on environment variables and from what IP address. They help answer questions like:
|
||||||
|
|
||||||
- Who added or updated environment variables recently?
|
- Who added or updated environment variables recently?
|
||||||
- Did Bob read environment variables last week (if at all)?
|
- Did Bob read environment variables last week (if at all)?
|
||||||
- What IP address was used for that action?
|
- What IP address was used for that action?
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@@ -2,4 +2,22 @@
|
|||||||
title: "Point-in-Time Recovery"
|
title: "Point-in-Time Recovery"
|
||||||
---
|
---
|
||||||
|
|
||||||
Point-in-time (PIT) recovery allows environment variables to be rolled back to any point in time. It's powered by snapshots that get captured after mutations to environment variables.
|
Point-in-time recovery allows environment variables to be rolled back to any point in time. It's powered by snapshots that get captured after mutations to environment variables.
|
||||||
|
|
||||||
|
## Commits
|
||||||
|
|
||||||
|
Similar to Git, a commit in Infisical is a snapshot of your project's secrets at a specific point in time. You can browse and view your project's snapshots via the "Point-in-Time Recovery" sidebar.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## Rolling back
|
||||||
|
|
||||||
|
Environment variables can be rolled back to any point in time via the "Rollback to this snapshot" button.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Rolling back environment variables to a past snapshot creates a new commit and
|
||||||
|
snapshot at the top of the stack and updates secret versions.
|
||||||
|
</Note>
|
||||||
|
|||||||
14
docs/getting-started/dashboard/secret-versioning.mdx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: "Secret Versioning"
|
||||||
|
---
|
||||||
|
|
||||||
|
Secret versioning records changes made to every secret.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Note>
|
||||||
|
You can copy and paste a secret version value to the "Value" input field "roll
|
||||||
|
back" to that secret version. This creates a new secret version at the top of
|
||||||
|
the stack. We're releasing the ability to press and automatically roll back to
|
||||||
|
a secret version soon.
|
||||||
|
</Note>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Secret Versioning"
|
|
||||||
---
|
|
||||||
|
|
||||||
Secret versioning allows an individual environment variable to be rolled back without touching other project environment variables.
|
|
||||||
BIN
docs/images/activity-logs.png
Normal file
|
After Width: | Height: | Size: 394 KiB |
BIN
docs/images/email-aws-ses-console.png
Normal file
|
After Width: | Height: | Size: 370 KiB |
BIN
docs/images/email-aws-ses-user.png
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
docs/images/pit-commits.png
Normal file
|
After Width: | Height: | Size: 271 KiB |
BIN
docs/images/pit-snapshot.png
Normal file
|
After Width: | Height: | Size: 269 KiB |
BIN
docs/images/pit-snapshots.png
Normal file
|
After Width: | Height: | Size: 364 KiB |
BIN
docs/images/secret-versioning.png
Normal file
|
After Width: | Height: | Size: 337 KiB |
@@ -81,7 +81,7 @@
|
|||||||
"getting-started/dashboard/project",
|
"getting-started/dashboard/project",
|
||||||
"getting-started/dashboard/integrations",
|
"getting-started/dashboard/integrations",
|
||||||
"getting-started/dashboard/pit-recovery",
|
"getting-started/dashboard/pit-recovery",
|
||||||
"getting-started/dashboard/versioning",
|
"getting-started/dashboard/secret-versioning",
|
||||||
"getting-started/dashboard/audit-logs",
|
"getting-started/dashboard/audit-logs",
|
||||||
"getting-started/dashboard/token"
|
"getting-started/dashboard/token"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ SMTP_FROM_NAME=Infisical
|
|||||||
```
|
```
|
||||||
|
|
||||||
<Info>
|
<Info>
|
||||||
Remember that you will need to restart Infisical for this to work properly.
|
Remember that you will need to restart Infisical for this to work properly.
|
||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
## Mailgun
|
## Mailgun
|
||||||
@@ -70,6 +70,28 @@ SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out em
|
|||||||
SMTP_FROM_NAME=Infisical
|
SMTP_FROM_NAME=Infisical
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## AWS SES
|
||||||
|
|
||||||
|
1. Create an account and [configure AWS SES](https://aws.amazon.com/premiumsupport/knowledge-center/ses-set-up-connect-smtp/) to send emails in the Amazon SES console.
|
||||||
|
2. Create an IAM user for SMTP authentication and obtain SMTP credentials in SMTP settings > Create SMTP credentials
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. With your AWS SES SMTP credentials, you can now set up your SMTP environment variables:
|
||||||
|
|
||||||
|
```
|
||||||
|
SMTP_HOST=smtp.mailgun.org # obtained from credentials page
|
||||||
|
SMTP_HOST=email-smtp.ap-northeast-1.amazonaws.com # SMTP endpoint obtained from SMTP settings
|
||||||
|
SMTP_USERNAME=xxx # your SMTP username
|
||||||
|
SMTP_PASSWORD=xxx # your SMTP password
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_SECURE=true
|
||||||
|
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
|
||||||
|
SMTP_FROM_NAME=Infisical
|
||||||
|
```
|
||||||
|
|
||||||
<Info>
|
<Info>
|
||||||
Remember that you will need to restart Infisical for this to work properly.
|
Remember that you will need to restart Infisical for this to work properly.
|
||||||
</Info>
|
</Info>
|
||||||
@@ -9,7 +9,6 @@ export const initPostHog = () => {
|
|||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
||||||
console.log("Outside of posthog", "POSTHOG_API_KEY", POSTHOG_API_KEY, "POSTHOG_HOST", POSTHOG_HOST)
|
|
||||||
posthog.init(POSTHOG_API_KEY, {
|
posthog.init(POSTHOG_API_KEY, {
|
||||||
api_host: POSTHOG_HOST
|
api_host: POSTHOG_HOST
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
|
|
||||||
export default function Error({ text }: { text: string }): JSX.Element {
|
export default function Error({ text }: { text: string }): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="relative bg-red-500 opacity-100 border flex flex-row justify-center m-auto items-center w-fit rounded-full mb-4">
|
<div className="relative flex flex-row justify-center m-auto items-center w-fit rounded-full">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faExclamationTriangle}
|
icon={faExclamationTriangle}
|
||||||
className="text-white mt-1.5 mb-2 mx-2"
|
className="text-red mt-1.5 mb-2 mx-2"
|
||||||
/>
|
/>
|
||||||
{text && (
|
{text && (
|
||||||
<p className="relative top-0 text-white mr-2 text-sm py-1">{text}</p>
|
<p className="relative top-0 text-red mr-2 text-sm py-1">{text}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default function EventFilter({
|
|||||||
{selected != '' ? (
|
{selected != '' ? (
|
||||||
<p className="select-none text-bunker-100">{t("activity:event." + selected)}</p>
|
<p className="select-none text-bunker-100">{t("activity:event." + selected)}</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="select-none">Select an event</p>
|
<p className="select-none">{String(t("common:select-event"))}</p>
|
||||||
)}
|
)}
|
||||||
{selected != '' ? (
|
{selected != '' ? (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
faKey,
|
faKey,
|
||||||
faMobile,
|
faMobile,
|
||||||
faPlug,
|
faPlug,
|
||||||
faTimeline,
|
|
||||||
faUser,
|
faUser,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -20,7 +19,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import getOrganizations from "~/pages/api/organization/getOrgs";
|
import getOrganizations from "~/pages/api/organization/getOrgs";
|
||||||
import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProjects";
|
import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProjects";
|
||||||
import getOrganizationUsers from "~/pages/api/organization/GetOrgUsers";
|
import getOrganizationUsers from "~/pages/api/organization/GetOrgUsers";
|
||||||
import checkUserAction from "~/pages/api/userActions/checkUserAction";
|
import getUser from "~/pages/api/user/getUser";
|
||||||
import addUserToWorkspace from "~/pages/api/workspace/addUserToWorkspace";
|
import addUserToWorkspace from "~/pages/api/workspace/addUserToWorkspace";
|
||||||
import createWorkspace from "~/pages/api/workspace/createWorkspace";
|
import createWorkspace from "~/pages/api/workspace/createWorkspace";
|
||||||
import getWorkspaces from "~/pages/api/workspace/getWorkspaces";
|
import getWorkspaces from "~/pages/api/workspace/getWorkspaces";
|
||||||
@@ -40,6 +39,7 @@ import Listbox from "./Listbox";
|
|||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
const crypto = require("crypto");
|
||||||
|
|
||||||
export default function Layout({ children }: LayoutProps) {
|
export default function Layout({ children }: LayoutProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -84,12 +84,31 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
});
|
});
|
||||||
const newWorkspaceId = newWorkspace._id;
|
const newWorkspaceId = newWorkspace._id;
|
||||||
|
|
||||||
|
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||||
|
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
|
||||||
|
|
||||||
|
const myUser = await getUser();
|
||||||
|
|
||||||
|
const { ciphertext, nonce } = encryptAssymmetric({
|
||||||
|
plaintext: randomBytes,
|
||||||
|
publicKey: myUser.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY,
|
||||||
|
}) as { ciphertext: string; nonce: string };
|
||||||
|
|
||||||
|
await uploadKeys(
|
||||||
|
newWorkspaceId,
|
||||||
|
myUser._id,
|
||||||
|
ciphertext,
|
||||||
|
nonce
|
||||||
|
);
|
||||||
|
|
||||||
if (addAllUsers) {
|
if (addAllUsers) {
|
||||||
|
console.log('adding other users')
|
||||||
const orgUsers = await getOrganizationUsers({
|
const orgUsers = await getOrganizationUsers({
|
||||||
orgId: tempLocalStorage("orgData.id"),
|
orgId: tempLocalStorage("orgData.id"),
|
||||||
});
|
});
|
||||||
orgUsers.map(async (user: any) => {
|
orgUsers.map(async (user: any) => {
|
||||||
if (user.status == "accepted") {
|
if (user.status == "accepted" && user.email != myUser.email) {
|
||||||
const result = await addUserToWorkspace(
|
const result = await addUserToWorkspace(
|
||||||
user.user.email,
|
user.user.email,
|
||||||
newWorkspaceId
|
newWorkspaceId
|
||||||
@@ -184,6 +203,7 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
if (
|
if (
|
||||||
userWorkspaces.length == 0 &&
|
userWorkspaces.length == 0 &&
|
||||||
router.asPath != "/noprojects" &&
|
router.asPath != "/noprojects" &&
|
||||||
|
!router.asPath.includes("home")&&
|
||||||
!router.asPath.includes("settings")
|
!router.asPath.includes("settings")
|
||||||
) {
|
) {
|
||||||
router.push("/noprojects");
|
router.push("/noprojects");
|
||||||
@@ -191,9 +211,16 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
const intendedWorkspaceId = router.asPath
|
const intendedWorkspaceId = router.asPath
|
||||||
.split("/")
|
.split("/")
|
||||||
[router.asPath.split("/").length - 1].split("?")[0];
|
[router.asPath.split("/").length - 1].split("?")[0];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!["heroku", "vercel", "github", "netlify"].includes(intendedWorkspaceId)
|
||||||
|
) {
|
||||||
|
localStorage.setItem("projectData.id", intendedWorkspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
|
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
|
||||||
if (
|
if (
|
||||||
intendedWorkspaceId != "heroku" &&
|
!["heroku", "vercel", "github", "netlify"].includes(intendedWorkspaceId) &&
|
||||||
!userWorkspaces
|
!userWorkspaces
|
||||||
.map((workspace: { _id: string }) => workspace._id)
|
.map((workspace: { _id: string }) => workspace._id)
|
||||||
.includes(intendedWorkspaceId)
|
.includes(intendedWorkspaceId)
|
||||||
@@ -239,14 +266,14 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
.split("/")
|
.split("/")
|
||||||
[router.asPath.split("/").length - 1].split("?")[0]
|
[router.asPath.split("/").length - 1].split("?")[0]
|
||||||
) {
|
) {
|
||||||
router.push(
|
|
||||||
"/dashboard/" +
|
|
||||||
workspaceMapping[workspaceSelected as any]
|
|
||||||
);
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"projectData.id",
|
"projectData.id",
|
||||||
`${workspaceMapping[workspaceSelected as any]}`
|
`${workspaceMapping[workspaceSelected as any]}`
|
||||||
);
|
);
|
||||||
|
router.push(
|
||||||
|
"/dashboard/" +
|
||||||
|
workspaceMapping[workspaceSelected as any]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default function ListBox({
|
|||||||
>
|
>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
{text}
|
{text}
|
||||||
<span className="ml-1 cursor-pointer block truncate font-semibold text-gray-300">
|
<span className="ml-1 cursor-pointer block truncate font-semibold text-gray-300 capitalize">
|
||||||
{' '}
|
{' '}
|
||||||
{selected}
|
{selected}
|
||||||
</span>
|
</span>
|
||||||
@@ -69,7 +69,7 @@ export default function ListBox({
|
|||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={personIdx}
|
key={personIdx}
|
||||||
className={({ active, selected }) =>
|
className={({ active, selected }) =>
|
||||||
`my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md ${
|
`my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md capitalize ${
|
||||||
selected ? 'bg-white/10 text-gray-400 font-bold' : ''
|
selected ? 'bg-white/10 text-gray-400 font-bold' : ''
|
||||||
} ${
|
} ${
|
||||||
active && !selected
|
active && !selected
|
||||||
|
|||||||
@@ -1,27 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Switch } from "@headlessui/react";
|
import { Switch } from "@headlessui/react";
|
||||||
|
|
||||||
|
|
||||||
interface OverrideProps {
|
|
||||||
id: string;
|
|
||||||
keyName: string;
|
|
||||||
value: string;
|
|
||||||
pos: number;
|
|
||||||
comment: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ToggleProps {
|
interface ToggleProps {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
setEnabled: (value: boolean) => void;
|
setEnabled: (value: boolean) => void;
|
||||||
addOverride: (value: OverrideProps) => void;
|
addOverride: (value: string | undefined, pos: number) => void;
|
||||||
keyName: string;
|
|
||||||
value: string;
|
|
||||||
pos: number;
|
pos: number;
|
||||||
id: string;
|
|
||||||
comment: string;
|
|
||||||
deleteOverride: (id: string) => void;
|
|
||||||
sharedToHide: string[];
|
|
||||||
setSharedToHide: (values: string[]) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,41 +14,23 @@ interface ToggleProps {
|
|||||||
* @param {boolean} obj.enabled - whether the toggle is turned on or off
|
* @param {boolean} obj.enabled - whether the toggle is turned on or off
|
||||||
* @param {function} obj.setEnabled - change the state of the toggle
|
* @param {function} obj.setEnabled - change the state of the toggle
|
||||||
* @param {function} obj.addOverride - a function that adds an override to a certain secret
|
* @param {function} obj.addOverride - a function that adds an override to a certain secret
|
||||||
* @param {string} obj.keyName - key of a certain secret
|
|
||||||
* @param {string} obj.value - value of a certain secret
|
|
||||||
* @param {number} obj.pos - position of a certain secret
|
* @param {number} obj.pos - position of a certain secret
|
||||||
#TODO: make the secret id persistent?
|
|
||||||
* @param {string} obj.id - id of a certain secret
|
|
||||||
* @param {function} obj.deleteOverride - a function that deleted an override for a certain secret
|
|
||||||
* @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden.
|
|
||||||
* @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually
|
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export default function Toggle ({
|
export default function Toggle ({
|
||||||
enabled,
|
enabled,
|
||||||
setEnabled,
|
setEnabled,
|
||||||
addOverride,
|
addOverride,
|
||||||
keyName,
|
pos
|
||||||
value,
|
|
||||||
pos,
|
|
||||||
id,
|
|
||||||
comment,
|
|
||||||
deleteOverride,
|
|
||||||
sharedToHide,
|
|
||||||
setSharedToHide
|
|
||||||
}: ToggleProps): JSX.Element {
|
}: ToggleProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
checked={enabled}
|
checked={enabled}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
if (enabled == false) {
|
if (enabled == false) {
|
||||||
addOverride({ id, keyName, value, pos, comment });
|
addOverride('', pos);
|
||||||
setSharedToHide([
|
|
||||||
...sharedToHide!,
|
|
||||||
id
|
|
||||||
])
|
|
||||||
} else {
|
} else {
|
||||||
deleteOverride(id);
|
addOverride(undefined, pos);
|
||||||
}
|
}
|
||||||
setEnabled(!enabled);
|
setEnabled(!enabled);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import Image from "next/image";
|
|||||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||||
import {
|
import {
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
FontAwesomeIconProps,
|
|
||||||
} from "@fortawesome/react-fontawesome";
|
} from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
const classNames = require("classnames");
|
const classNames = require("classnames");
|
||||||
@@ -101,7 +100,7 @@ export default function Button(props: ButtonProps): JSX.Element {
|
|||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
props.loading == true ? "opacity-100" : "opacity-0"
|
props.loading == true ? "opacity-100" : "opacity-0"
|
||||||
} absolute flex items-center px-2 duration-200`}
|
} absolute flex items-center px-3 bg-primary duration-200 w-full`}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src="/images/loading/loadingblack.gif"
|
src="/images/loading/loadingblack.gif"
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ const AddProjectMemberDialog = ({
|
|||||||
selected={email ? email : data[0]}
|
selected={email ? email : data[0]}
|
||||||
onChange={setEmail}
|
onChange={setEmail}
|
||||||
data={data}
|
data={data}
|
||||||
width="full"
|
isFull={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import nacl from "tweetnacl";
|
|||||||
import addServiceToken from "~/pages/api/serviceToken/addServiceToken";
|
import addServiceToken from "~/pages/api/serviceToken/addServiceToken";
|
||||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
||||||
|
|
||||||
import { envMapping } from "../../../public/data/frequentConstants";
|
|
||||||
import {
|
import {
|
||||||
decryptAssymmetric,
|
decryptAssymmetric,
|
||||||
encryptAssymmetric,
|
encryptAssymmetric,
|
||||||
@@ -34,11 +33,12 @@ const AddServiceTokenDialog = ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
workspaceName,
|
workspaceName,
|
||||||
serviceTokens,
|
serviceTokens,
|
||||||
|
environments,
|
||||||
setServiceTokens
|
setServiceTokens
|
||||||
}) => {
|
}) => {
|
||||||
const [serviceToken, setServiceToken] = useState("");
|
const [serviceToken, setServiceToken] = useState("");
|
||||||
const [serviceTokenName, setServiceTokenName] = useState("");
|
const [serviceTokenName, setServiceTokenName] = useState("");
|
||||||
const [serviceTokenEnv, setServiceTokenEnv] = useState("Development");
|
const [selectedServiceTokenEnv, setSelectedServiceTokenEnv] = useState(environments?.[0]);
|
||||||
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day");
|
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day");
|
||||||
const [serviceTokenCopied, setServiceTokenCopied] = useState(false);
|
const [serviceTokenCopied, setServiceTokenCopied] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -66,15 +66,13 @@ const AddServiceTokenDialog = ({
|
|||||||
let newServiceToken = await addServiceToken({
|
let newServiceToken = await addServiceToken({
|
||||||
name: serviceTokenName,
|
name: serviceTokenName,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment: envMapping[serviceTokenEnv],
|
environment: selectedServiceTokenEnv?.slug ? selectedServiceTokenEnv.slug : environments[0]?.name,
|
||||||
expiresIn: expiryMapping[serviceTokenExpiresIn],
|
expiresIn: expiryMapping[serviceTokenExpiresIn],
|
||||||
encryptedKey: ciphertext,
|
encryptedKey: ciphertext,
|
||||||
iv,
|
iv,
|
||||||
tag
|
tag
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('newServiceToken', newServiceToken);
|
|
||||||
|
|
||||||
setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData]));
|
setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData]));
|
||||||
setServiceToken(newServiceToken.serviceToken + "." + randomBytes);
|
setServiceToken(newServiceToken.serviceToken + "." + randomBytes);
|
||||||
};
|
};
|
||||||
@@ -103,155 +101,159 @@ const AddServiceTokenDialog = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="z-50">
|
<div className='z-50'>
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog as="div" className="relative" onClose={closeModal}>
|
<Dialog as='div' className='relative' onClose={closeModal}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter='ease-out duration-300'
|
||||||
enterFrom="opacity-0"
|
enterFrom='opacity-0'
|
||||||
enterTo="opacity-100"
|
enterTo='opacity-100'
|
||||||
leave="ease-in duration-200"
|
leave='ease-in duration-200'
|
||||||
leaveFrom="opacity-100"
|
leaveFrom='opacity-100'
|
||||||
leaveTo="opacity-0"
|
leaveTo='opacity-0'
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-bunker-700 bg-opacity-80" />
|
<div className='fixed inset-0 bg-bunker-700 bg-opacity-80' />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 overflow-y-auto">
|
<div className='fixed inset-0 overflow-y-auto'>
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter='ease-out duration-300'
|
||||||
enterFrom="opacity-0 scale-95"
|
enterFrom='opacity-0 scale-95'
|
||||||
enterTo="opacity-100 scale-100"
|
enterTo='opacity-100 scale-100'
|
||||||
leave="ease-in duration-200"
|
leave='ease-in duration-200'
|
||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom='opacity-100 scale-100'
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo='opacity-0 scale-95'
|
||||||
>
|
>
|
||||||
{serviceToken == "" ? (
|
{serviceToken == '' ? (
|
||||||
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
as="h3"
|
as='h3'
|
||||||
className="text-lg font-medium leading-6 text-gray-400 z-50"
|
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
||||||
>
|
>
|
||||||
{t("section-token:add-dialog.title", {
|
{t('section-token:add-dialog.title', {
|
||||||
target: workspaceName,
|
target: workspaceName,
|
||||||
})}
|
})}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2 mb-4">
|
<div className='mt-2 mb-4'>
|
||||||
<div className="flex flex-col">
|
<div className='flex flex-col'>
|
||||||
<p className="text-sm text-gray-500">
|
<p className='text-sm text-gray-500'>
|
||||||
{t("section-token:add-dialog.description")}
|
{t('section-token:add-dialog.description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-28 mb-2">
|
<div className='max-h-28 mb-2'>
|
||||||
<InputField
|
<InputField
|
||||||
label={t("section-token:add-dialog.name")}
|
label={t('section-token:add-dialog.name')}
|
||||||
onChangeHandler={setServiceTokenName}
|
onChangeHandler={setServiceTokenName}
|
||||||
type="varName"
|
type='varName'
|
||||||
value={serviceTokenName}
|
value={serviceTokenName}
|
||||||
placeholder=""
|
placeholder=''
|
||||||
isRequired
|
isRequired
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-28 mb-2">
|
<div className='max-h-28 mb-2'>
|
||||||
<ListBox
|
<ListBox
|
||||||
selected={serviceTokenEnv}
|
selected={selectedServiceTokenEnv?.name ? selectedServiceTokenEnv?.name : environments[0]?.name}
|
||||||
onChange={setServiceTokenEnv}
|
data={environments.map(({ name }) => name)}
|
||||||
data={[
|
onChange={(envName) =>
|
||||||
"Development",
|
setSelectedServiceTokenEnv(
|
||||||
"Staging",
|
environments.find(
|
||||||
"Production",
|
({ name }) => envName === name
|
||||||
"Testing",
|
) || {
|
||||||
]}
|
name: 'unknown',
|
||||||
|
slug: 'unknown',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
isFull={true}
|
isFull={true}
|
||||||
text={`${t("common:environment")}: `}
|
text={`${t('common:environment')}: `}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-28">
|
<div className='max-h-28'>
|
||||||
<ListBox
|
<ListBox
|
||||||
selected={serviceTokenExpiresIn}
|
selected={serviceTokenExpiresIn}
|
||||||
onChange={setServiceTokenExpiresIn}
|
onChange={setServiceTokenExpiresIn}
|
||||||
data={[
|
data={[
|
||||||
"1 day",
|
'1 day',
|
||||||
"7 days",
|
'7 days',
|
||||||
"1 month",
|
'1 month',
|
||||||
"6 months",
|
'6 months',
|
||||||
"12 months",
|
'12 months',
|
||||||
]}
|
]}
|
||||||
isFull={true}
|
isFull={true}
|
||||||
text={`${t("common:expired-in")}: `}
|
text={`${t('common:expired-in')}: `}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-max">
|
<div className='max-w-max'>
|
||||||
<div className="mt-6 flex flex-col justify-start w-max">
|
<div className='mt-6 flex flex-col justify-start w-max'>
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() => generateServiceToken()}
|
onButtonPressed={() => generateServiceToken()}
|
||||||
color="mineshaft"
|
color='mineshaft'
|
||||||
text={t("section-token:add-dialog.add")}
|
text={t('section-token:add-dialog.add')}
|
||||||
textDisabled={t("section-token:add-dialog.add")}
|
textDisabled={t('section-token:add-dialog.add')}
|
||||||
size="md"
|
size='md'
|
||||||
active={serviceTokenName == "" ? false : true}
|
active={serviceTokenName == '' ? false : true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
) : (
|
) : (
|
||||||
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
as="h3"
|
as='h3'
|
||||||
className="text-lg font-medium leading-6 text-gray-400 z-50"
|
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
||||||
>
|
>
|
||||||
{t("section-token:add-dialog.copy-service-token")}
|
{t('section-token:add-dialog.copy-service-token')}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2 mb-4">
|
<div className='mt-2 mb-4'>
|
||||||
<div className="flex flex-col">
|
<div className='flex flex-col'>
|
||||||
<p className="text-sm text-gray-500">
|
<p className='text-sm text-gray-500'>
|
||||||
{t(
|
{t(
|
||||||
"section-token:add-dialog.copy-service-token-description"
|
'section-token:add-dialog.copy-service-token-description'
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className='w-full'>
|
||||||
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-20">
|
<div className='flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-20'>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type='text'
|
||||||
value={serviceToken}
|
value={serviceToken}
|
||||||
id="serviceToken"
|
id='serviceToken'
|
||||||
className="invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none"
|
className='invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none'
|
||||||
></input>
|
></input>
|
||||||
<div className="bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none">
|
<div className='bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none'>
|
||||||
{serviceToken}
|
{serviceToken}
|
||||||
</div>
|
</div>
|
||||||
<div className="group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200">
|
<div className='group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200'>
|
||||||
<button
|
<button
|
||||||
onClick={copyToClipboard}
|
onClick={copyToClipboard}
|
||||||
className="h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200"
|
className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
|
||||||
>
|
>
|
||||||
{serviceTokenCopied ? (
|
{serviceTokenCopied ? (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faCheck}
|
icon={faCheck}
|
||||||
className="pr-0.5"
|
className='pr-0.5'
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FontAwesomeIcon icon={faCopy} />
|
<FontAwesomeIcon icon={faCopy} />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm">
|
<span className='absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm'>
|
||||||
{t("common:click-to-copy")}
|
{t('common:click-to-copy')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 flex flex-col justify-start w-max">
|
<div className='mt-6 flex flex-col justify-start w-max'>
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() => closeAddServiceTokenModal()}
|
onButtonPressed={() => closeAddServiceTokenModal()}
|
||||||
color="mineshaft"
|
color='mineshaft'
|
||||||
text="Close"
|
text='Close'
|
||||||
size="md"
|
size='md'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
|
|||||||
145
frontend/components/basic/dialog/AddUpdateEnvironmentDialog.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { FormEventHandler, Fragment, useEffect, useState } from 'react';
|
||||||
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
|
|
||||||
|
import Button from '../buttons/Button';
|
||||||
|
import InputField from '../InputField';
|
||||||
|
|
||||||
|
type FormFields = { name: string; slug: string };
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen?: boolean;
|
||||||
|
isEditMode?: boolean;
|
||||||
|
// on edit mode load up initial values
|
||||||
|
initialValues?: FormFields;
|
||||||
|
onClose: () => void;
|
||||||
|
onCreateSubmit: (data: FormFields) => void;
|
||||||
|
onEditSubmit: (data: FormFields) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Migrate to better form management and validation. Preferable react-hook-form + yup
|
||||||
|
/**
|
||||||
|
* The dialog modal for when the user wants to create a new workspace
|
||||||
|
* @param {*} param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const AddUpdateEnvironmentDialog = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onCreateSubmit,
|
||||||
|
onEditSubmit,
|
||||||
|
initialValues,
|
||||||
|
isEditMode,
|
||||||
|
}: Props) => {
|
||||||
|
const [formInput, setFormInput] = useState<FormFields>({
|
||||||
|
name: '',
|
||||||
|
slug: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// This use effect can be removed when the unmount is happening from outside the component
|
||||||
|
// When unmount happens outside state gets unmounted also
|
||||||
|
useEffect(() => {
|
||||||
|
setFormInput(initialValues || { name: '', slug: '' });
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// REFACTOR: Move to react-hook-form with yup for better form management
|
||||||
|
const onInputChange = (fieldName: string, fieldValue: string) => {
|
||||||
|
setFormInput((state) => ({ ...state, [fieldName]: fieldValue }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFormSubmit: FormEventHandler = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const data = {
|
||||||
|
name: formInput.name.toLowerCase(),
|
||||||
|
slug: formInput.slug.toLowerCase(),
|
||||||
|
};
|
||||||
|
if (isEditMode) {
|
||||||
|
onEditSubmit(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onCreateSubmit(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
|
<Dialog as='div' className='relative z-20' onClose={onClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter='ease-out duration-300'
|
||||||
|
enterFrom='opacity-0'
|
||||||
|
enterTo='opacity-100'
|
||||||
|
leave='ease-out duration-150'
|
||||||
|
leaveFrom='opacity-100'
|
||||||
|
leaveTo='opacity-0'
|
||||||
|
>
|
||||||
|
<div className='fixed inset-0 bg-black bg-opacity-70' />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className='fixed inset-0 overflow-y-auto z-50'>
|
||||||
|
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter='ease-out duration-300'
|
||||||
|
enterFrom='opacity-0 scale-95'
|
||||||
|
enterTo='opacity-100 scale-100'
|
||||||
|
leave='ease-in duration-200'
|
||||||
|
leaveFrom='opacity-100 scale-100'
|
||||||
|
leaveTo='opacity-0 scale-95'
|
||||||
|
>
|
||||||
|
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-2xl bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||||
|
<Dialog.Title
|
||||||
|
as='h3'
|
||||||
|
className='text-lg font-medium leading-6 text-gray-400'
|
||||||
|
>
|
||||||
|
{isEditMode
|
||||||
|
? 'Update environment'
|
||||||
|
: 'Create a new environment'}
|
||||||
|
</Dialog.Title>
|
||||||
|
<form onSubmit={onFormSubmit}>
|
||||||
|
<div className='max-h-28 mt-4'>
|
||||||
|
<InputField
|
||||||
|
label='Environment Name'
|
||||||
|
onChangeHandler={(val) => onInputChange('name', val)}
|
||||||
|
type='varName'
|
||||||
|
value={formInput.name}
|
||||||
|
placeholder=''
|
||||||
|
isRequired
|
||||||
|
// error={error.length > 0}
|
||||||
|
// errorText={error}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='max-h-28 mt-4'>
|
||||||
|
<InputField
|
||||||
|
label='Environment Slug'
|
||||||
|
onChangeHandler={(val) => onInputChange('slug', val)}
|
||||||
|
type='varName'
|
||||||
|
value={formInput.slug}
|
||||||
|
placeholder=''
|
||||||
|
isRequired
|
||||||
|
// error={error.length > 0}
|
||||||
|
// errorText={error}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className='text-xs text-gray-500 mt-2'>
|
||||||
|
Slugs are shorthands used in cli to access environment
|
||||||
|
</p>
|
||||||
|
<div className='mt-4 max-w-min'>
|
||||||
|
<Button
|
||||||
|
onButtonPressed={() => null}
|
||||||
|
type='submit'
|
||||||
|
color='mineshaft'
|
||||||
|
text={isEditMode ? 'Update' : 'Create'}
|
||||||
|
active={formInput.name !== '' && formInput.slug !== ''}
|
||||||
|
size='md'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
104
frontend/components/basic/dialog/DeleteActionModal.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { Fragment, useEffect, useState } from 'react';
|
||||||
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
|
|
||||||
|
import InputField from '../InputField';
|
||||||
|
|
||||||
|
// REFACTOR: Move all these modals into one reusable one
|
||||||
|
type Props = {
|
||||||
|
isOpen?: boolean;
|
||||||
|
onClose: ()=>void;
|
||||||
|
title: string;
|
||||||
|
onSubmit:()=>void;
|
||||||
|
deleteKey?:string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteActionModal = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
title,
|
||||||
|
onSubmit,
|
||||||
|
deleteKey
|
||||||
|
}:Props) => {
|
||||||
|
const [deleteInputField, setDeleteInputField] = useState("")
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDeleteInputField("");
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
|
<Dialog as='div' className='relative z-10' onClose={onClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter='ease-out duration-300'
|
||||||
|
enterFrom='opacity-0'
|
||||||
|
enterTo='opacity-100'
|
||||||
|
leave='ease-in duration-150'
|
||||||
|
leaveFrom='opacity-100'
|
||||||
|
leaveTo='opacity-0'
|
||||||
|
>
|
||||||
|
<div className='fixed inset-0 bg-black bg-opacity-25' />
|
||||||
|
</Transition.Child>
|
||||||
|
<div className='fixed inset-0 overflow-y-auto'>
|
||||||
|
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter='ease-out duration-300'
|
||||||
|
enterFrom='opacity-0 scale-95'
|
||||||
|
enterTo='opacity-100 scale-100'
|
||||||
|
leave='ease-in duration-200'
|
||||||
|
leaveFrom='opacity-100 scale-100'
|
||||||
|
leaveTo='opacity-0 scale-95'
|
||||||
|
>
|
||||||
|
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-2xl bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||||
|
<Dialog.Title
|
||||||
|
as='h3'
|
||||||
|
className='text-lg font-medium leading-6 text-gray-400'
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Dialog.Title>
|
||||||
|
<div className='mt-2'>
|
||||||
|
<p className='text-sm text-gray-500'>
|
||||||
|
This action is irrevertible.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='mt-2'>
|
||||||
|
<InputField
|
||||||
|
isRequired
|
||||||
|
label={`Type ${deleteKey} to delete the resource`}
|
||||||
|
onChangeHandler={(val) => setDeleteInputField(val)}
|
||||||
|
value={deleteInputField}
|
||||||
|
type='text'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='mt-6'>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-alizarin hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||||
|
onClick={onSubmit}
|
||||||
|
disabled={
|
||||||
|
Boolean(deleteKey) && deleteInputField !== deleteKey
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:border-white hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteActionModal;
|
||||||
77
frontend/components/basic/dialog/DeleteEnvVar.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { Fragment } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
|
// #TODO: USE THIS. Currently it's not. Kinda complicated to set up because of state.
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onSubmit: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-10" onClose={() => {}}>
|
||||||
|
<div className="fixed inset-0 overflow-y-auto">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-70" onClick={onClose} />
|
||||||
|
</Transition.Child>
|
||||||
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||||
|
<Dialog.Title
|
||||||
|
as="h3"
|
||||||
|
className="text-lg font-medium leading-6 text-gray-400"
|
||||||
|
>
|
||||||
|
{t('dashboard:sidebar.delete-key-dialog.title')}
|
||||||
|
</Dialog.Title>
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{t('dashboard:sidebar.delete-key-dialog.confirm-delete-message')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 flex justify-start">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex justify-center rounded-md border border-transparent bg-red-700 hover:bg-red-600 px-4 py-2 text-sm font-medium text-bunker-200 hover:text-white text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
|
onClick={onSubmit}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ml-2 inline-flex justify-center rounded-md border border-transparent bg-bunker-500 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-mineshaft-500 hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -36,7 +36,7 @@ export default function BottonRightPopup({
|
|||||||
}: PopupProps): JSX.Element {
|
}: PopupProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-xl absolute bottom-0 right-0 mr-6 mb-6"
|
className="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-md absolute bottom-0 right-0 mr-6 mb-6"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">
|
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">
|
||||||
|
|||||||
167
frontend/components/basic/table/EnvironmentsTable.tsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import { faPencil, faPlus, faX } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
import { usePopUp } from '../../../hooks/usePopUp';
|
||||||
|
import Button from '../buttons/Button';
|
||||||
|
import { AddUpdateEnvironmentDialog } from '../dialog/AddUpdateEnvironmentDialog';
|
||||||
|
import DeleteActionModal from '../dialog/DeleteActionModal';
|
||||||
|
|
||||||
|
type Env = { name: string; slug: string };
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: Env[];
|
||||||
|
onCreateEnv: (arg0: Env) => Promise<void>;
|
||||||
|
onUpdateEnv: (oldSlug: string, arg0: Env) => Promise<void>;
|
||||||
|
onDeleteEnv: (slug: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EnvironmentTable = ({
|
||||||
|
data = [],
|
||||||
|
onCreateEnv,
|
||||||
|
onDeleteEnv,
|
||||||
|
onUpdateEnv,
|
||||||
|
}: Props) => {
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
|
'createUpdateEnv',
|
||||||
|
'deleteEnv',
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const onEnvCreateCB = async (env: Env) => {
|
||||||
|
try {
|
||||||
|
await onCreateEnv(env);
|
||||||
|
handlePopUpClose('createUpdateEnv');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEnvUpdateCB = async (env: Env) => {
|
||||||
|
try {
|
||||||
|
await onUpdateEnv(
|
||||||
|
(popUp.createUpdateEnv?.data as Pick<Env, 'slug'>)?.slug,
|
||||||
|
env
|
||||||
|
);
|
||||||
|
handlePopUpClose('createUpdateEnv');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEnvDeleteCB = async () => {
|
||||||
|
try {
|
||||||
|
await onDeleteEnv(
|
||||||
|
(popUp.deleteEnv?.data as Pick<Env, 'slug'>)?.slug
|
||||||
|
);
|
||||||
|
handlePopUpClose('deleteEnv');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='flex flex-row justify-between w-full'>
|
||||||
|
<div className='flex flex-col w-full'>
|
||||||
|
<p className='text-xl font-semibold mb-3'>Project Environments</p>
|
||||||
|
<p className='text-base text-gray-400 mb-4'>
|
||||||
|
Choose which environments will show up in your dashboard like
|
||||||
|
development, staging, production
|
||||||
|
</p>
|
||||||
|
<p className='text-sm mr-1 text-gray-500 self-start'>
|
||||||
|
Note: the text in slugs shows how these environmant should be
|
||||||
|
accessed in CLI.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='w-48'>
|
||||||
|
<Button
|
||||||
|
text='Add New Env'
|
||||||
|
onButtonPressed={() => handlePopUpOpen('createUpdateEnv')}
|
||||||
|
color='mineshaft'
|
||||||
|
icon={faPlus}
|
||||||
|
size='md'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1'>
|
||||||
|
<div className='absolute rounded-t-md w-full h-12 bg-white/5'></div>
|
||||||
|
<table className='w-full my-1'>
|
||||||
|
<thead className='text-bunker-300'>
|
||||||
|
<tr>
|
||||||
|
<th className='text-left pl-6 pt-2.5 pb-2'>Name</th>
|
||||||
|
<th className='text-left pl-6 pt-2.5 pb-2'>Slug</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data?.length > 0 ? (
|
||||||
|
data.map(({ name, slug }) => {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={name}
|
||||||
|
className='bg-bunker-800 hover:bg-bunker-800/5 duration-100'
|
||||||
|
>
|
||||||
|
<td className='pl-6 py-2 border-mineshaft-700 border-t text-gray-300 capitalize'>
|
||||||
|
{name}
|
||||||
|
</td>
|
||||||
|
<td className='pl-6 py-2 border-mineshaft-700 border-t text-gray-300'>
|
||||||
|
{slug}
|
||||||
|
</td>
|
||||||
|
<td className='py-2 border-mineshaft-700 border-t flex'>
|
||||||
|
<div className='opacity-50 hover:opacity-100 duration-200 flex items-center mr-8'>
|
||||||
|
<Button
|
||||||
|
onButtonPressed={() =>
|
||||||
|
handlePopUpOpen('createUpdateEnv', { name, slug })
|
||||||
|
}
|
||||||
|
color='red'
|
||||||
|
size='icon-sm'
|
||||||
|
icon={faPencil}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='opacity-50 hover:opacity-100 duration-200 flex items-center'>
|
||||||
|
<Button
|
||||||
|
onButtonPressed={() =>
|
||||||
|
handlePopUpOpen('deleteEnv', { name, slug })
|
||||||
|
}
|
||||||
|
color='red'
|
||||||
|
size='icon-sm'
|
||||||
|
icon={faX}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={4}
|
||||||
|
className='text-center pt-7 pb-4 text-bunker-400'
|
||||||
|
>
|
||||||
|
No environmants found
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp['deleteEnv'].isOpen}
|
||||||
|
title={`Are you sure want to delete ${
|
||||||
|
(popUp?.deleteEnv?.data as { name: string })?.name || ' '
|
||||||
|
}?`}
|
||||||
|
deleteKey={(popUp?.deleteEnv?.data as { slug: string })?.slug || ''}
|
||||||
|
onClose={() => handlePopUpClose('deleteEnv')}
|
||||||
|
onSubmit={onEnvDeleteCB}
|
||||||
|
/>
|
||||||
|
<AddUpdateEnvironmentDialog
|
||||||
|
isOpen={popUp.createUpdateEnv.isOpen}
|
||||||
|
isEditMode={Boolean(popUp.createUpdateEnv?.data)}
|
||||||
|
initialValues={popUp?.createUpdateEnv?.data as any}
|
||||||
|
onClose={() => handlePopUpClose('createUpdateEnv')}
|
||||||
|
onCreateSubmit={onEnvCreateCB}
|
||||||
|
onEditSubmit={onEnvUpdateCB}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EnvironmentTable;
|
||||||
@@ -3,7 +3,6 @@ import { faX } from '@fortawesome/free-solid-svg-icons';
|
|||||||
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
|
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
|
||||||
|
|
||||||
import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken";
|
import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken";
|
||||||
import { reverseEnvMapping } from '../../../public/data/frequentConstants';
|
|
||||||
import guidGenerator from '../../utilities/randomId';
|
import guidGenerator from '../../utilities/randomId';
|
||||||
import Button from '../buttons/Button';
|
import Button from '../buttons/Button';
|
||||||
|
|
||||||
@@ -60,7 +59,7 @@ const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTok
|
|||||||
{workspaceName}
|
{workspaceName}
|
||||||
</td>
|
</td>
|
||||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||||
{reverseEnvMapping[row.environment]}
|
{row.environment}
|
||||||
</td>
|
</td>
|
||||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||||
{new Date(row.expiresAt).toUTCString()}
|
{new Date(row.expiresAt).toUTCString()}
|
||||||
|
|||||||
@@ -117,13 +117,13 @@ const UserTable = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
|
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
|
||||||
<div className="absolute rounded-t-md w-full h-14 bg-white/5"></div>
|
<div className="absolute rounded-t-md w-full h-[3.25rem] bg-white/5"></div>
|
||||||
<table className="w-full my-1">
|
<table className="w-full my-0.5">
|
||||||
<thead className="text-gray-400">
|
<thead className="text-gray-400 text-sm font-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left pl-6 py-3.5">First Name</th>
|
<th className="text-left pl-6 py-3.5">FIRST NAME</th>
|
||||||
<th className="text-left pl-6 py-3.5">Last Name</th>
|
<th className="text-left pl-6 py-3.5">LAST NAME</th>
|
||||||
<th className="text-left pl-6 py-3.5">Email</th>
|
<th className="text-left pl-6 py-3.5">EMAIL</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import { useTranslation } from "next-i18next";
|
|||||||
const CommentField = ({ comment, modifyComment, position }: { comment: string; modifyComment: (value: string, posistion: number) => void; position: number;}) => {
|
const CommentField = ({ comment, modifyComment, position }: { comment: string; modifyComment: (value: string, posistion: number) => void; position: number;}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return <div className={`relative mt-4 px-4 pt-4`}>
|
return <div className={`relative mt-4 px-4 pt-6`}>
|
||||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.comments")}</p>
|
<p className='text-sm text-bunker-300 pl-0.5'>{t("dashboard:sidebar.comments")}</p>
|
||||||
<textarea
|
<textarea
|
||||||
className="bg-bunker-800 h-32 w-full bg-bunker-800 p-2 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
|
className="bg-bunker-800 placeholder:text-bunker-400 h-32 w-full bg-bunker-800 px-2 py-1.5 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
|
||||||
value={comment}
|
value={comment}
|
||||||
onChange={(e) => modifyComment(e.target.value, position)}
|
onChange={(e) => modifyComment(e.target.value, position)}
|
||||||
placeholder="Leave any comments here..."
|
placeholder="Leave any comments here..."
|
||||||
|
|||||||