mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 15:27:46 +00:00
Fix merge conflicts
This commit is contained in:
@@ -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:
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
name: Push frontend and backend to Dockerhub
|
name: Build, Publish and Deploy to Gamma
|
||||||
|
|
||||||
on: [workflow_dispatch]
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -99,4 +98,41 @@ jobs:
|
|||||||
infisical/frontend:latest
|
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
|
||||||
|
if [[ $(helm status infisical) == *"FAILED"* ]]; then
|
||||||
|
echo "Helm upgrade failed"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Helm upgrade was successful"
|
||||||
|
fi
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<img width="300" src="/img/logoname-white.svg#gh-dark-mode-only" alt="infisical">
|
<img width="300" src="/img/logoname-white.svg#gh-dark-mode-only" alt="infisical">
|
||||||
</h1>
|
</h1>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<p align="center">Open-source, E2EE, simple tool to manage and sync environment variables across your team and infrastructure.</p>
|
<p align="center">Open-source, E2EE, simple tool to manage secrets and configs across your team and infrastructure.</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h4 align="center">
|
<h4 align="center">
|
||||||
@@ -34,17 +34,17 @@
|
|||||||
|
|
||||||
<img src="/img/infisical_github_repo.png" width="100%" alt="Dashboard" />
|
<img src="/img/infisical_github_repo.png" width="100%" alt="Dashboard" />
|
||||||
|
|
||||||
**[Infisical](https://infisical.com)** is an open source, E2EE tool to help teams manage and sync environment variables across their development workflow and infrastructure. It's designed to be simple and take minutes to get going.
|
**[Infisical](https://infisical.com)** is an open source, E2EE tool to help teams manage and sync secrets and configs across their development workflow and infrastructure. It's designed to be simple and take minutes to get going.
|
||||||
|
|
||||||
- **[User-Friendly Dashboard](https://infisical.com/docs/getting-started/dashboard/project)** to manage your team's environment variables within projects
|
- **[User-Friendly Dashboard](https://infisical.com/docs/getting-started/dashboard/project)** to manage your team's secrets and configs within projects
|
||||||
- **[Language-Agnostic CLI](https://infisical.com/docs/cli/overview)** that pulls and injects environment variables into your local workflow
|
- **[Language-Agnostic CLI](https://infisical.com/docs/cli/overview)** that pulls and injects esecrets and configs into your local workflow
|
||||||
- **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure
|
- **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure
|
||||||
- **Navigate Multiple Environments** per project (e.g. development, staging, production, etc.)
|
- **Navigate Multiple Environments** per project (e.g. development, staging, production, etc.)
|
||||||
- **Personal overrides** for environment variables
|
- **Personal overrides** for secrets and configs
|
||||||
- **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure
|
- **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure
|
||||||
- **[Secret Versioning](https://infisical.com/docs/getting-started/dashboard/versioning)** - check the history of change for any secret
|
- **[Secret Versioning](https://infisical.com/docs/getting-started/dashboard/versioning)** to view the change history for any secret
|
||||||
- **[Activity Logs](https://infisical.com/docs/getting-started/dashboard/audit-logs)** - check what user in the project is performing what actions with secrets
|
- **[Activity Logs](https://infisical.com/docs/getting-started/dashboard/audit-logs)** to record every action taken in a project.
|
||||||
- **[Point-in-time Secrets Recovery](https://infisical.com/docs/getting-started/dashboard/pit-recovery)** - roll back to any snapshot of you secrets
|
- **[Point-in-time Secrets Recovery](https://infisical.com/docs/getting-started/dashboard/pit-recovery)** for rolling back to any snapshot of your secrets
|
||||||
- 🔜 **1-Click Deploy** to Digital Ocean and Heroku
|
- 🔜 **1-Click Deploy** to Digital Ocean and Heroku
|
||||||
- 🔜 **Authentication/Authorization** for projects (read/write controls soon)
|
- 🔜 **Authentication/Authorization** for projects (read/write controls soon)
|
||||||
- 🔜 **Automatic Secret Rotation**
|
- 🔜 **Automatic Secret Rotation**
|
||||||
@@ -333,10 +333,15 @@ 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/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>
|
<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/Grraahaam"><img src="https://avatars.githubusercontent.com/u/72856427?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Gabriellopes232"><img src="https://avatars.githubusercontent.com/u/74881862?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/cerrussell"><img src="https://avatars.githubusercontent.com/u/80227828?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/imakecodes"><img src="https://avatars.githubusercontent.com/u/35536648?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
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
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!
|
Infisical is currently available in English and Korean. Help us translate Infisical to your language!
|
||||||
|
>>>>>>> 9ce4a52b8da0057c2450cd7af93a8c5758c2476b
|
||||||
|
|
||||||
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).
|
||||||
|
|||||||
Generated
+3
-6
@@ -28,6 +28,7 @@
|
|||||||
"express-validator": "^6.14.2",
|
"express-validator": "^6.14.2",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"helmet": "^5.1.1",
|
"helmet": "^5.1.1",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"jsrp": "^0.2.4",
|
"jsrp": "^0.2.4",
|
||||||
"libsodium-wrappers": "^0.7.10",
|
"libsodium-wrappers": "^0.7.10",
|
||||||
@@ -3698,8 +3699,7 @@
|
|||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/array-flatten": {
|
"node_modules/array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -6638,7 +6638,6 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
},
|
},
|
||||||
@@ -14980,8 +14979,7 @@
|
|||||||
"argparse": {
|
"argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"array-flatten": {
|
"array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -17197,7 +17195,6 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"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",
|
"swagger-autogen": "node ./swagger/index.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",
|
||||||
@@ -94,6 +94,7 @@
|
|||||||
"express-validator": "^6.14.2",
|
"express-validator": "^6.14.2",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"helmet": "^5.1.1",
|
"helmet": "^5.1.1",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"jsrp": "^0.2.4",
|
"jsrp": "^0.2.4",
|
||||||
"libsodium-wrappers": "^0.7.10",
|
"libsodium-wrappers": "^0.7.10",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+6
-2
@@ -8,7 +8,7 @@ import cookieParser from 'cookie-parser';
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import swaggerUi = require('swagger-ui-express');
|
import swaggerUi = require('swagger-ui-express');
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const swaggerFile = require('../api-documentation.json')
|
const swaggerFile = require('../spec.json')
|
||||||
|
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
@@ -41,11 +41,13 @@ import {
|
|||||||
integrationAuth as v1IntegrationAuthRouter
|
integrationAuth as v1IntegrationAuthRouter
|
||||||
} from './routes/v1';
|
} from './routes/v1';
|
||||||
import {
|
import {
|
||||||
secret as v2SecretRouter,
|
users as v2UsersRouter,
|
||||||
|
secret as v2SecretRouter, // begin to phase out
|
||||||
secrets as v2SecretsRouter,
|
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 { healthCheck } from './routes/status';
|
||||||
@@ -103,6 +105,8 @@ 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/users', v2UsersRouter);
|
||||||
|
app.use('/api/v2/workspace', v2EnvironmentRouter);
|
||||||
app.use('/api/v2/workspace', v2WorkspaceRouter); // TODO: turn into plural route
|
app.use('/api/v2/workspace', v2WorkspaceRouter); // TODO: turn into plural route
|
||||||
app.use('/api/v2/secret', v2SecretRouter); // stop supporting, TODO: revise
|
app.use('/api/v2/secret', v2SecretRouter); // stop supporting, TODO: revise
|
||||||
app.use('/api/v2/secrets', v2SecretsRouter);
|
app.use('/api/v2/secrets', v2SecretsRouter);
|
||||||
|
|||||||
@@ -170,10 +170,11 @@ export const logout = async (req: Request, res: Response) => {
|
|||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const checkAuth = async (req: Request, res: Response) =>
|
export const checkAuth = async (req: Request, res: Response) => {
|
||||||
res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Authenticated'
|
message: 'Authenticated'
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return new token by redeeming refresh token
|
* Return new token by redeeming refresh token
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
@@ -31,11 +31,17 @@ 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);
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
|
import * as usersController from './usersController';
|
||||||
import * as workspaceController from './workspaceController';
|
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 secretsController from './secretsController';
|
||||||
|
import * as environmentController from './environmentController';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
usersController,
|
||||||
workspaceController,
|
workspaceController,
|
||||||
serviceTokenDataController,
|
serviceTokenDataController,
|
||||||
apiKeyDataController,
|
apiKeyDataController,
|
||||||
secretController,
|
secretController,
|
||||||
secretsController
|
secretsController,
|
||||||
|
environmentController
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ 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';
|
import { postHogClient } from '../../services';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create secret for workspace with id [workspaceId] and environment [environment]
|
* Create secret for workspace with id [workspaceId] and environment [environment]
|
||||||
@@ -42,19 +42,19 @@ export const createSecret = async (req: Request, res: Response) => {
|
|||||||
throw RouteValidationError({ message: error.message, stack: error.stack })
|
throw RouteValidationError({ message: error.message, stack: error.stack })
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (postHogClient) {
|
if (postHogClient) {
|
||||||
// postHogClient.capture({
|
postHogClient.capture({
|
||||||
// event: 'secrets added',
|
event: 'secrets added',
|
||||||
// distinctId: req.user.email,
|
distinctId: req.user.email,
|
||||||
// properties: {
|
properties: {
|
||||||
// numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
// workspaceId,
|
workspaceId,
|
||||||
// environment,
|
environment,
|
||||||
// channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
// userAgent: req.headers?.['user-agent']
|
userAgent: req.headers?.['user-agent']
|
||||||
// }
|
}
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
|
|
||||||
res.status(200).send({
|
res.status(200).send({
|
||||||
secret
|
secret
|
||||||
@@ -103,19 +103,19 @@ export const createSecrets = 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 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (postHogClient) {
|
if (postHogClient) {
|
||||||
// postHogClient.capture({
|
postHogClient.capture({
|
||||||
// event: 'secrets added',
|
event: 'secrets added',
|
||||||
// distinctId: req.user.email,
|
distinctId: req.user.email,
|
||||||
// properties: {
|
properties: {
|
||||||
// numberOfSecrets: (secretsToCreate ?? []).length,
|
numberOfSecrets: (secretsToCreate ?? []).length,
|
||||||
// workspaceId,
|
workspaceId,
|
||||||
// environment,
|
environment,
|
||||||
// channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
// userAgent: req.headers?.['user-agent']
|
userAgent: req.headers?.['user-agent']
|
||||||
// }
|
}
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
|
|
||||||
res.status(200).send({
|
res.status(200).send({
|
||||||
secrets
|
secrets
|
||||||
@@ -158,19 +158,19 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
|||||||
throw InternalServerError()
|
throw InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (postHogClient) {
|
if (postHogClient) {
|
||||||
// postHogClient.capture({
|
postHogClient.capture({
|
||||||
// event: 'secrets deleted',
|
event: 'secrets deleted',
|
||||||
// distinctId: req.user.email,
|
distinctId: req.user.email,
|
||||||
// properties: {
|
properties: {
|
||||||
// numberOfSecrets: numSecretsDeleted,
|
numberOfSecrets: numSecretsDeleted,
|
||||||
// environment: environmentName,
|
environment: environmentName,
|
||||||
// workspaceId,
|
workspaceId,
|
||||||
// channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
// userAgent: req.headers?.['user-agent']
|
userAgent: req.headers?.['user-agent']
|
||||||
// }
|
}
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
|
|
||||||
res.status(200).send()
|
res.status(200).send()
|
||||||
}
|
}
|
||||||
@@ -183,19 +183,19 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
|||||||
export const deleteSecret = async (req: Request, res: Response) => {
|
export const deleteSecret = async (req: Request, res: Response) => {
|
||||||
await Secret.findByIdAndDelete(req._secret._id)
|
await Secret.findByIdAndDelete(req._secret._id)
|
||||||
|
|
||||||
// if (postHogClient) {
|
if (postHogClient) {
|
||||||
// postHogClient.capture({
|
postHogClient.capture({
|
||||||
// event: 'secrets deleted',
|
event: 'secrets deleted',
|
||||||
// distinctId: req.user.email,
|
distinctId: req.user.email,
|
||||||
// properties: {
|
properties: {
|
||||||
// numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
// workspaceId: req._secret.workspace.toString(),
|
workspaceId: req._secret.workspace.toString(),
|
||||||
// environment: req._secret.environment,
|
environment: req._secret.environment,
|
||||||
// channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
// userAgent: req.headers?.['user-agent']
|
userAgent: req.headers?.['user-agent']
|
||||||
// }
|
}
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
|
|
||||||
res.status(200).send({
|
res.status(200).send({
|
||||||
secret: req._secret
|
secret: req._secret
|
||||||
@@ -252,19 +252,19 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
throw InternalServerError()
|
throw InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (postHogClient) {
|
if (postHogClient) {
|
||||||
// postHogClient.capture({
|
postHogClient.capture({
|
||||||
// event: 'secrets modified',
|
event: 'secrets modified',
|
||||||
// distinctId: req.user.email,
|
distinctId: req.user.email,
|
||||||
// properties: {
|
properties: {
|
||||||
// numberOfSecrets: (secretsModificationsRequested ?? []).length,
|
numberOfSecrets: (secretsModificationsRequested ?? []).length,
|
||||||
// environment: environmentName,
|
environment: environmentName,
|
||||||
// workspaceId,
|
workspaceId,
|
||||||
// channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
// userAgent: req.headers?.['user-agent']
|
userAgent: req.headers?.['user-agent']
|
||||||
// }
|
}
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
|
|
||||||
return res.status(200).send()
|
return res.status(200).send()
|
||||||
}
|
}
|
||||||
@@ -304,19 +304,19 @@ export const updateSecret = async (req: Request, res: Response) => {
|
|||||||
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: error.stack })
|
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: error.stack })
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (postHogClient) {
|
if (postHogClient) {
|
||||||
// postHogClient.capture({
|
postHogClient.capture({
|
||||||
// event: 'secrets modified',
|
event: 'secrets modified',
|
||||||
// distinctId: req.user.email,
|
distinctId: req.user.email,
|
||||||
// properties: {
|
properties: {
|
||||||
// numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
// environment: environmentName,
|
environment: environmentName,
|
||||||
// workspaceId,
|
workspaceId,
|
||||||
// channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
// userAgent: req.headers?.['user-agent']
|
userAgent: req.headers?.['user-agent']
|
||||||
// }
|
}
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
|
|
||||||
return res.status(200).send(singleModificationUpdate)
|
return res.status(200).send(singleModificationUpdate)
|
||||||
}
|
}
|
||||||
@@ -332,13 +332,16 @@ 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 getting personal secrets for user
|
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 [err, secrets] = await to(Secret.find(
|
const [err, secrets] = await to(Secret.find(
|
||||||
@@ -354,19 +357,19 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
throw RouteValidationError({ message: "Failed to get secrets, please try again", stack: err.stack })
|
throw RouteValidationError({ message: "Failed to get secrets, please try again", stack: err.stack })
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (postHogClient) {
|
if (postHogClient) {
|
||||||
// postHogClient.capture({
|
postHogClient.capture({
|
||||||
// event: 'secrets pulled',
|
event: 'secrets pulled',
|
||||||
// distinctId: req.user.email,
|
distinctId: userEmail,
|
||||||
// properties: {
|
properties: {
|
||||||
// numberOfSecrets: (secrets ?? []).length,
|
numberOfSecrets: (secrets ?? []).length,
|
||||||
// environment,
|
environment,
|
||||||
// workspaceId,
|
workspaceId,
|
||||||
// channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
// userAgent: req.headers?.['user-agent']
|
userAgent: req.headers?.['user-agent']
|
||||||
// }
|
}
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
|
|
||||||
return res.json(secrets)
|
return res.json(secrets)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,58 @@ import { BadRequestError } from '../../utils/errors';
|
|||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const createSecrets = async (req: Request, res: Response) => {
|
export const createSecrets = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Create new secret(s)'
|
||||||
|
#swagger.description = 'Create one or many secrets for a given project and environment.'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.requestBody = {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of project",
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Environment within project"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
$ref: "#/components/schemas/CreateSecret",
|
||||||
|
"description": "Secret(s) to create - object or array of objects"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secrets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/Secret"
|
||||||
|
},
|
||||||
|
"description": "Newly-created secrets for the given project and environment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
||||||
const { workspaceId, environment } = req.body;
|
const { workspaceId, environment } = req.body;
|
||||||
|
|
||||||
@@ -67,8 +119,17 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
// trigger event - push secrets
|
||||||
|
await EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
// (EE) add secret versions for new secrets
|
// (EE) add secret versions for new secrets
|
||||||
EESecretService.addSecretVersions({
|
await EESecretService.addSecretVersions({
|
||||||
secretVersions: newSecrets.map(({
|
secretVersions: newSecrets.map(({
|
||||||
_id,
|
_id,
|
||||||
version,
|
version,
|
||||||
@@ -104,13 +165,6 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
// trigger event - push secrets
|
|
||||||
await EventService.handleEvent({
|
|
||||||
event: eventPushSecrets({
|
|
||||||
workspaceId
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const addAction = await EELogService.createActionSecret({
|
const addAction = await EELogService.createActionSecret({
|
||||||
name: ACTION_ADD_SECRETS,
|
name: ACTION_ADD_SECRETS,
|
||||||
userId: req.user._id.toString(),
|
userId: req.user._id.toString(),
|
||||||
@@ -159,15 +213,57 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getSecrets = async (req: Request, res: Response) => {
|
export const getSecrets = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Read secrets'
|
||||||
|
#swagger.description = 'Read secrets from a project and environment'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['workspaceId'] = {
|
||||||
|
"description": "ID of project",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['environment'] = {
|
||||||
|
"description": "Environment within project",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secrets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/Secret"
|
||||||
|
},
|
||||||
|
"description": "Secrets for the given project and environment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
const { workspaceId, environment } = req.query;
|
const { workspaceId, environment } = req.query;
|
||||||
|
|
||||||
let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user
|
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;
|
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 [err, secrets] = await to(Secret.find(
|
const [err, secrets] = await to(Secret.find(
|
||||||
@@ -204,7 +300,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: 'secrets pulled',
|
event: 'secrets pulled',
|
||||||
distinctId: req.user.email,
|
distinctId: userEmail,
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: secrets.length,
|
numberOfSecrets: secrets.length,
|
||||||
environment,
|
environment,
|
||||||
@@ -226,6 +322,50 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const updateSecrets = async (req: Request, res: Response) => {
|
export const updateSecrets = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Update secret(s)'
|
||||||
|
#swagger.description = 'Update secret(s)'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.requestBody = {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secrets": {
|
||||||
|
$ref: "#/components/schemas/UpdateSecret",
|
||||||
|
"description": "Secret(s) to update - object or array of objects"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secrets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/Secret"
|
||||||
|
},
|
||||||
|
"description": "Updated secrets"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
||||||
|
|
||||||
// TODO: move type
|
// TODO: move type
|
||||||
@@ -339,11 +479,13 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
Object.keys(workspaceSecretObj).forEach(async (key) => {
|
Object.keys(workspaceSecretObj).forEach(async (key) => {
|
||||||
// trigger event - push secrets
|
// trigger event - push secrets
|
||||||
await EventService.handleEvent({
|
setTimeout(async () => {
|
||||||
event: eventPushSecrets({
|
await EventService.handleEvent({
|
||||||
workspaceId: key
|
event: eventPushSecrets({
|
||||||
})
|
workspaceId: key
|
||||||
});
|
})
|
||||||
|
});
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
const updateAction = await EELogService.createActionSecret({
|
const updateAction = await EELogService.createActionSecret({
|
||||||
name: ACTION_UPDATE_SECRETS,
|
name: ACTION_UPDATE_SECRETS,
|
||||||
@@ -396,6 +538,50 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const deleteSecrets = async (req: Request, res: Response) => {
|
export const deleteSecrets = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Delete secret(s)'
|
||||||
|
#swagger.description = 'Delete one or many secrets by their ID(s)'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.requestBody = {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secretIds": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID(s) of secrets - string or array of strings"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secrets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/Secret"
|
||||||
|
},
|
||||||
|
"description": "Deleted secrets"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
||||||
const toDelete = req.secrets.map((s: any) => s._id);
|
const toDelete = req.secrets.map((s: any) => s._id);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
User
|
||||||
|
} from '../../models';
|
||||||
|
|
||||||
|
export const getMe = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = "Retrieve the current user on the request"
|
||||||
|
#swagger.description = "Retrieve the current user on the request"
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"user": {
|
||||||
|
"type": "object",
|
||||||
|
$ref: "#/components/schemas/CurrentUser",
|
||||||
|
"description": "Current user on request"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let user;
|
||||||
|
try {
|
||||||
|
user = await User
|
||||||
|
.findById(req.user._id)
|
||||||
|
.select('+publicKey +encryptedPrivateKey +iv +tag');
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get user'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
user
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { 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 {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
|
Secret,
|
||||||
Membership,
|
Membership,
|
||||||
MembershipOrg,
|
MembershipOrg,
|
||||||
Integration,
|
Integration,
|
||||||
@@ -19,7 +21,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 +53,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 +131,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,
|
||||||
@@ -169,6 +176,34 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getWorkspaceKey = async (req: Request, res: Response) => {
|
export const getWorkspaceKey = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Return encrypted project key'
|
||||||
|
#swagger.description = 'Return encrypted project key'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['workspaceId'] = {
|
||||||
|
"description": "ID of project",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/ProjectKey"
|
||||||
|
},
|
||||||
|
"description": "Encrypted project key for the given project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
let key;
|
let key;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
@@ -214,4 +249,222 @@ export const getWorkspaceServiceTokenData = async (
|
|||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
serviceTokenData
|
serviceTokenData
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return memberships for workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Return project memberships'
|
||||||
|
#swagger.description = 'Return project memberships'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['workspaceId'] = {
|
||||||
|
"description": "ID of project",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"memberships": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/Membership"
|
||||||
|
},
|
||||||
|
"description": "Memberships of project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let memberships;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
memberships = await Membership.find({
|
||||||
|
workspace: workspaceId
|
||||||
|
}).populate('user', '+publicKey');
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get workspace memberships'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
memberships
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete workspace membership with id [membershipId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteWorkspaceMembership = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Delete project membership'
|
||||||
|
#swagger.description = 'Delete project membership'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['workspaceId'] = {
|
||||||
|
"description": "ID of project",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['membershipId'] = {
|
||||||
|
"description": "ID of membership",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"membership": {
|
||||||
|
$ref: "#/components/schemas/Membership",
|
||||||
|
"description": "Deleted membership"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let membership;
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
membershipId
|
||||||
|
} = req.params;
|
||||||
|
|
||||||
|
membership = await Membership.findByIdAndDelete(membershipId);
|
||||||
|
|
||||||
|
if (!membership) throw new Error('Failed to delete workspace membership');
|
||||||
|
|
||||||
|
await Key.deleteMany({
|
||||||
|
receiver: membership.user,
|
||||||
|
workspace: membership.workspace
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to delete workspace membership'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
membership
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update role of membership with id [membershipId] to role [role]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const updateWorkspaceMembership = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Update project membership'
|
||||||
|
#swagger.description = 'Update project membership'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['workspaceId'] = {
|
||||||
|
"description": "ID of project",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['membershipId'] = {
|
||||||
|
"description": "ID of membership",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.requestBody = {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"role": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Role of membership - either admin or member",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"membership": {
|
||||||
|
$ref: "#/components/schemas/Membership",
|
||||||
|
"description": "Updated membership"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let membership;
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
membershipId
|
||||||
|
} = req.params;
|
||||||
|
const { role } = req.body;
|
||||||
|
|
||||||
|
membership = await Membership.findByIdAndUpdate(
|
||||||
|
membershipId,
|
||||||
|
{
|
||||||
|
role
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to update workspace membership'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
membership
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,51 @@ import { EESecretService } from '../../services';
|
|||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const getSecretVersions = async (req: Request, res: Response) => {
|
export const getSecretVersions = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Return secret versions'
|
||||||
|
#swagger.description = 'Return secret versions'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['secretId'] = {
|
||||||
|
"description": "ID of secret",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['offset'] = {
|
||||||
|
"description": "Number of versions to skip",
|
||||||
|
"required": false,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['limit'] = {
|
||||||
|
"description": "Maximum number of versions to return",
|
||||||
|
"required": false,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secretVersions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/SecretVersion"
|
||||||
|
},
|
||||||
|
"description": "Secret versions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
let secretVersions;
|
let secretVersions;
|
||||||
try {
|
try {
|
||||||
const { secretId } = req.params;
|
const { secretId } = req.params;
|
||||||
@@ -44,6 +89,54 @@ import { EESecretService } from '../../services';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Roll back secret to a version.'
|
||||||
|
#swagger.description = 'Roll back secret to a version.'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['secretId'] = {
|
||||||
|
"description": "ID of secret",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.requestBody = {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"version": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Version of secret to roll back to"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secret": {
|
||||||
|
"type": "object",
|
||||||
|
$ref: "#/components/schemas/Secret",
|
||||||
|
"description": "Secret rolled back to"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
let secret;
|
let secret;
|
||||||
try {
|
try {
|
||||||
const { secretId } = req.params;
|
const { secretId } = req.params;
|
||||||
|
|||||||
@@ -19,6 +19,51 @@ import { getLatestSecretVersionIds } from '../../helpers/secretVersion';
|
|||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => {
|
export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Return project secret snapshot ids'
|
||||||
|
#swagger.description = 'Return project secret snapshots ids'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['workspaceId'] = {
|
||||||
|
"description": "ID of project",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['offset'] = {
|
||||||
|
"description": "Number of secret snapshots to skip",
|
||||||
|
"required": false,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['limit'] = {
|
||||||
|
"description": "Maximum number of secret snapshots to return",
|
||||||
|
"required": false,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secretSnapshots": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/SecretSnapshot"
|
||||||
|
},
|
||||||
|
"description": "Project secret snapshots"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
let secretSnapshots;
|
let secretSnapshots;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
@@ -78,16 +123,66 @@ export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Respon
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Response) => {
|
export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Roll back project secrets to those captured in a secret snapshot version.'
|
||||||
|
#swagger.description = 'Roll back project secrets to those captured in a secret snapshot version.'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['workspaceId'] = {
|
||||||
|
"description": "ID of project",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.requestBody = {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"version": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Version of secret snapshot to roll back to",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secrets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/Secret"
|
||||||
|
},
|
||||||
|
"description": "Secrets rolled back to"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
let secrets;
|
let secrets;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
const { version } = req.body;
|
const { version } = req.body;
|
||||||
|
|
||||||
// validate secret snapshot
|
// validate secret snapshot
|
||||||
const secretSnapshot = await SecretSnapshot.findOne({
|
const secretSnapshot = await SecretSnapshot.findOne({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
version
|
version
|
||||||
}).populate<{ secretVersions: ISecretVersion[]}>('secretVersions');
|
}).populate<{ secretVersions: ISecretVersion[]}>('secretVersions');
|
||||||
|
|
||||||
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
|
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
|
||||||
|
|
||||||
@@ -231,6 +326,72 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspaceLogs = async (req: Request, res: Response) => {
|
export const getWorkspaceLogs = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Return project (audit) logs'
|
||||||
|
#swagger.description = 'Return project (audit) logs'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['workspaceId'] = {
|
||||||
|
"description": "ID of project",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['userId'] = {
|
||||||
|
"description": "ID of project member",
|
||||||
|
"required": false,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['offset'] = {
|
||||||
|
"description": "Number of logs to skip",
|
||||||
|
"required": false,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['limit'] = {
|
||||||
|
"description": "Maximum number of logs to return",
|
||||||
|
"required": false,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['sortBy'] = {
|
||||||
|
"description": "Order to sort the logs by",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"@enum": ["oldest", "recent"]
|
||||||
|
},
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['actionNames'] = {
|
||||||
|
"description": "Names of log actions (comma-separated)",
|
||||||
|
"required": false,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"logs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/Log"
|
||||||
|
},
|
||||||
|
"description": "Project logs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
let logs
|
let logs
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|||||||
@@ -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 ISecretVersion {
|
export interface ISecretVersion {
|
||||||
@@ -56,7 +52,6 @@ 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: { // consider removing field
|
isDeleted: { // consider removing field
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const validateSecrets = async ({
|
|||||||
try {
|
try {
|
||||||
secrets = await Secret.find({
|
secrets = await Secret.find({
|
||||||
_id: {
|
_id: {
|
||||||
$in: secretIds
|
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+291
-292
@@ -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';
|
||||||
@@ -145,7 +145,6 @@ const syncSecretsVercel = async ({
|
|||||||
secrets: any;
|
secrets: any;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
interface VercelSecret {
|
interface VercelSecret {
|
||||||
id?: string;
|
id?: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -155,131 +154,131 @@ 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: { [key: string]: string } = {
|
const params: { [key: string]: string } = {
|
||||||
decrypt: 'true',
|
decrypt: 'true',
|
||||||
...( integrationAuth?.teamId ? {
|
...( integrationAuth?.teamId ? {
|
||||||
teamId: 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[] = [];
|
||||||
params,
|
const deleteSecrets: VercelSecret[] = [];
|
||||||
headers: {
|
const newSecrets: VercelSecret[] = [];
|
||||||
Authorization: `Bearer ${accessToken}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)).data)
|
|
||||||
)).reduce((obj: any, secret: any) => ({
|
|
||||||
...obj,
|
|
||||||
[secret.key]: secret
|
|
||||||
}), {});
|
|
||||||
|
|
||||||
const updateSecrets: VercelSecret[] = [];
|
|
||||||
const deleteSecrets: VercelSecret[] = [];
|
|
||||||
const newSecrets: VercelSecret[] = [];
|
|
||||||
|
|
||||||
// Identify secrets to create
|
// Identify secrets to create
|
||||||
Object.keys(secrets).map((key) => {
|
Object.keys(secrets).map((key) => {
|
||||||
if (!(key in res)) {
|
if (!(key in res)) {
|
||||||
// case: secret has been created
|
// case: secret has been created
|
||||||
newSecrets.push({
|
newSecrets.push({
|
||||||
key: key,
|
key: key,
|
||||||
value: secrets[key],
|
value: secrets[key],
|
||||||
type: 'encrypted',
|
type: 'encrypted',
|
||||||
target: [integration.target]
|
target: [integration.target]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Identify secrets to update and delete
|
// Identify secrets to update and delete
|
||||||
Object.keys(res).map((key) => {
|
Object.keys(res).map((key) => {
|
||||||
if (key in secrets) {
|
if (key in secrets) {
|
||||||
if (res[key].value !== secrets[key]) {
|
if (res[key].value !== secrets[key]) {
|
||||||
// case: secret value has changed
|
// case: secret value has changed
|
||||||
updateSecrets.push({
|
updateSecrets.push({
|
||||||
id: res[key].id,
|
id: res[key].id,
|
||||||
key: key,
|
key: key,
|
||||||
value: secrets[key],
|
value: secrets[key],
|
||||||
type: 'encrypted',
|
type: 'encrypted',
|
||||||
target: [integration.target]
|
target: [integration.target]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// case: secret has been deleted
|
// case: secret has been deleted
|
||||||
deleteSecrets.push({
|
deleteSecrets.push({
|
||||||
id: res[key].id,
|
id: res[key].id,
|
||||||
key: key,
|
key: key,
|
||||||
value: res[key].value,
|
value: res[key].value,
|
||||||
type: 'encrypted',
|
type: 'encrypted',
|
||||||
target: [integration.target],
|
target: [integration.target],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sync/push new secrets
|
// Sync/push new secrets
|
||||||
if (newSecrets.length > 0) {
|
if (newSecrets.length > 0) {
|
||||||
await axios.post(
|
await axios.post(
|
||||||
`${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`,
|
`${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`,
|
||||||
newSecrets,
|
newSecrets,
|
||||||
{
|
{
|
||||||
params,
|
params,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Sync/push updated secrets
|
// Sync/push updated secrets
|
||||||
if (updateSecrets.length > 0) {
|
if (updateSecrets.length > 0) {
|
||||||
updateSecrets.forEach(async (secret: VercelSecret) => {
|
updateSecrets.forEach(async (secret: VercelSecret) => {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
...updatedSecret
|
...updatedSecret
|
||||||
} = secret;
|
} = secret;
|
||||||
await axios.patch(
|
await axios.patch(
|
||||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||||
updatedSecret,
|
updatedSecret,
|
||||||
{
|
{
|
||||||
params,
|
params,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
});
|
);
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Delete secrets
|
// Delete secrets
|
||||||
if (deleteSecrets.length > 0) {
|
if (deleteSecrets.length > 0) {
|
||||||
deleteSecrets.forEach(async (secret: VercelSecret) => {
|
deleteSecrets.forEach(async (secret: VercelSecret) => {
|
||||||
await axios.delete(
|
await axios.delete(
|
||||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||||
{
|
{
|
||||||
params,
|
params,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
});
|
);
|
||||||
}
|
});
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
@@ -307,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
|
||||||
|
Object.keys(secrets).map((key) => {
|
||||||
|
if (!(key in res)) {
|
||||||
|
// case: Infisical secret does not exist in Netlify -> create secret
|
||||||
|
newSecrets.push({
|
||||||
|
key,
|
||||||
|
values: [{
|
||||||
|
value: secrets[key],
|
||||||
|
context: integration.context
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// case: Infisical secret exists in Netlify
|
||||||
|
const contexts = res[key].values
|
||||||
|
.reduce((obj: any, value: NetlifyValue) => ({
|
||||||
|
...obj,
|
||||||
|
[value.context]: value
|
||||||
|
}), {});
|
||||||
|
|
||||||
|
if (integration.context in contexts) {
|
||||||
|
// case: Netlify secret value exists in integration context
|
||||||
|
if (secrets[key] !== contexts[integration.context].value) {
|
||||||
|
// case: Infisical and Netlify secret values are different
|
||||||
|
// -> update Netlify secret context and value
|
||||||
|
updateSecrets.push({
|
||||||
|
key,
|
||||||
|
values: [{
|
||||||
|
context: integration.context,
|
||||||
|
value: secrets[key]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// case: Netlify secret value does not exist in integration context
|
||||||
|
// -> add the new Netlify secret context and value
|
||||||
|
updateSecrets.push({
|
||||||
|
key,
|
||||||
|
values: [{
|
||||||
|
context: integration.context,
|
||||||
|
value: secrets[key]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// identify secrets to delete
|
||||||
|
// TODO: revise (patch case where 1 context was deleted but others still there
|
||||||
|
Object.keys(res).map((key) => {
|
||||||
|
// loop through each key's context
|
||||||
|
if (!(key in secrets)) {
|
||||||
|
// case: Netlify secret does not exist in Infisical
|
||||||
|
|
||||||
|
const numberOfValues = res[key].values.length;
|
||||||
|
|
||||||
|
res[key].values.forEach((value: NetlifyValue) => {
|
||||||
|
if (value.context === integration.context) {
|
||||||
|
if (numberOfValues <= 1) {
|
||||||
|
// case: Netlify secret value has less than 1 context -> delete secret
|
||||||
|
deleteSecrets.push(key);
|
||||||
|
} else {
|
||||||
|
// case: Netlify secret value has more than 1 context -> delete secret value context
|
||||||
|
deleteSecretValues.push({
|
||||||
|
key,
|
||||||
|
values: [{
|
||||||
|
id: value.id,
|
||||||
|
context: integration.context,
|
||||||
|
value: value.value
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// identify secrets to create and update
|
const syncParams = new URLSearchParams({
|
||||||
Object.keys(secrets).map((key) => {
|
site_id: integration.siteId
|
||||||
if (!(key in res)) {
|
});
|
||||||
// case: Infisical secret does not exist in Netlify -> create secret
|
|
||||||
newSecrets.push({
|
|
||||||
key,
|
|
||||||
values: [{
|
|
||||||
value: secrets[key],
|
|
||||||
context: integration.context
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// case: Infisical secret exists in Netlify
|
|
||||||
const contexts = res[key].values
|
|
||||||
.reduce((obj: any, value: NetlifyValue) => ({
|
|
||||||
...obj,
|
|
||||||
[value.context]: value
|
|
||||||
}), {});
|
|
||||||
|
|
||||||
if (integration.context in contexts) {
|
|
||||||
// case: Netlify secret value exists in integration context
|
|
||||||
if (secrets[key] !== contexts[integration.context].value) {
|
|
||||||
// case: Infisical and Netlify secret values are different
|
|
||||||
// -> update Netlify secret context and value
|
|
||||||
updateSecrets.push({
|
|
||||||
key,
|
|
||||||
values: [{
|
|
||||||
context: integration.context,
|
|
||||||
value: secrets[key]
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// case: Netlify secret value does not exist in integration context
|
|
||||||
// -> add the new Netlify secret context and value
|
|
||||||
updateSecrets.push({
|
|
||||||
key,
|
|
||||||
values: [{
|
|
||||||
context: integration.context,
|
|
||||||
value: secrets[key]
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// identify secrets to delete
|
|
||||||
// TODO: revise (patch case where 1 context was deleted but others still there
|
|
||||||
Object.keys(res).map((key) => {
|
|
||||||
// loop through each key's context
|
|
||||||
if (!(key in secrets)) {
|
|
||||||
// case: Netlify secret does not exist in Infisical
|
|
||||||
|
|
||||||
const numberOfValues = res[key].values.length;
|
|
||||||
|
|
||||||
res[key].values.forEach((value: NetlifyValue) => {
|
|
||||||
if (value.context === integration.context) {
|
|
||||||
if (numberOfValues <= 1) {
|
|
||||||
// case: Netlify secret value has less than 1 context -> delete secret
|
|
||||||
deleteSecrets.push(key);
|
|
||||||
} else {
|
|
||||||
// case: Netlify secret value has more than 1 context -> delete secret value context
|
|
||||||
deleteSecretValues.push({
|
|
||||||
key,
|
|
||||||
values: [{
|
|
||||||
id: value.id,
|
|
||||||
context: integration.context,
|
|
||||||
value: value.value
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const syncParams = new URLSearchParams({
|
if (newSecrets.length > 0) {
|
||||||
site_id: integration.siteId
|
await axios.post(
|
||||||
});
|
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||||
|
newSecrets,
|
||||||
|
{
|
||||||
|
params: syncParams,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (newSecrets.length > 0) {
|
if (updateSecrets.length > 0) {
|
||||||
await axios.post(
|
updateSecrets.forEach(async (secret: NetlifySecret) => {
|
||||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
await axios.patch(
|
||||||
newSecrets,
|
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
|
||||||
{
|
{
|
||||||
params: syncParams,
|
context: secret.values[0].context,
|
||||||
headers: {
|
value: secret.values[0].value
|
||||||
Authorization: `Bearer ${accessToken}`
|
},
|
||||||
}
|
{
|
||||||
}
|
params: syncParams,
|
||||||
);
|
headers: {
|
||||||
}
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (updateSecrets.length > 0) {
|
if (deleteSecrets.length > 0) {
|
||||||
updateSecrets.forEach(async (secret: NetlifySecret) => {
|
deleteSecrets.forEach(async (key: string) => {
|
||||||
await axios.patch(
|
await axios.delete(
|
||||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
|
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`,
|
||||||
{
|
{
|
||||||
context: secret.values[0].context,
|
params: syncParams,
|
||||||
value: secret.values[0].value
|
headers: {
|
||||||
},
|
Authorization: `Bearer ${accessToken}`
|
||||||
{
|
}
|
||||||
params: syncParams,
|
}
|
||||||
headers: {
|
);
|
||||||
Authorization: `Bearer ${accessToken}`
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deleteSecrets.length > 0) {
|
if (deleteSecretValues.length > 0) {
|
||||||
deleteSecrets.forEach(async (key: string) => {
|
deleteSecretValues.forEach(async (secret: NetlifySecret) => {
|
||||||
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/${secret.key}/value/${secret.values[0].id}`,
|
||||||
{
|
{
|
||||||
params: syncParams,
|
params: syncParams,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deleteSecretValues.length > 0) {
|
|
||||||
deleteSecretValues.forEach(async (secret: NetlifySecret) => {
|
|
||||||
await axios.delete(
|
|
||||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`,
|
|
||||||
{
|
|
||||||
params: syncParams,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} 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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import requireAuth from './requireAuth';
|
|||||||
import requireBotAuth from './requireBotAuth';
|
import requireBotAuth from './requireBotAuth';
|
||||||
import requireSignupAuth from './requireSignupAuth';
|
import requireSignupAuth from './requireSignupAuth';
|
||||||
import requireWorkspaceAuth from './requireWorkspaceAuth';
|
import requireWorkspaceAuth from './requireWorkspaceAuth';
|
||||||
|
import requireMembershipAuth from './requireMembershipAuth';
|
||||||
import requireOrganizationAuth from './requireOrganizationAuth';
|
import requireOrganizationAuth from './requireOrganizationAuth';
|
||||||
import requireIntegrationAuth from './requireIntegrationAuth';
|
import requireIntegrationAuth from './requireIntegrationAuth';
|
||||||
import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth';
|
import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth';
|
||||||
@@ -16,6 +17,7 @@ export {
|
|||||||
requireBotAuth,
|
requireBotAuth,
|
||||||
requireSignupAuth,
|
requireSignupAuth,
|
||||||
requireWorkspaceAuth,
|
requireWorkspaceAuth,
|
||||||
|
requireMembershipAuth,
|
||||||
requireOrganizationAuth,
|
requireOrganizationAuth,
|
||||||
requireIntegrationAuth,
|
requireIntegrationAuth,
|
||||||
requireIntegrationAuthorizationAuth,
|
requireIntegrationAuthorizationAuth,
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { UnauthorizedRequestError } from '../utils/errors';
|
||||||
|
import {
|
||||||
|
Membership,
|
||||||
|
} from '../models';
|
||||||
|
import { validateMembership } from '../helpers/membership';
|
||||||
|
|
||||||
|
type req = 'params' | 'body' | 'query';
|
||||||
|
/**
|
||||||
|
* Validate membership with id [membershipId] and that user with id
|
||||||
|
* [req.user._id] can modify that membership.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String[]} obj.acceptedRoles - accepted workspace roles for JWT auth
|
||||||
|
* @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing
|
||||||
|
*/
|
||||||
|
const requireMembershipAuth = ({
|
||||||
|
acceptedRoles,
|
||||||
|
location = 'params'
|
||||||
|
}: {
|
||||||
|
acceptedRoles: string[];
|
||||||
|
location?: req;
|
||||||
|
}) => {
|
||||||
|
return async (
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const { membershipId } = req[location];
|
||||||
|
|
||||||
|
const membership = await Membership.findById(membershipId);
|
||||||
|
|
||||||
|
if (!membership) throw new Error('Failed to find target membership');
|
||||||
|
|
||||||
|
const userMembership = await Membership.findOne({
|
||||||
|
workspace: membership.workspace
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userMembership) throw new Error('Failed to validate own membership')
|
||||||
|
|
||||||
|
const targetMembership = await validateMembership({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: membership.workspace.toString(),
|
||||||
|
acceptedRoles
|
||||||
|
});
|
||||||
|
|
||||||
|
req.targetMembership = targetMembership;
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (err) {
|
||||||
|
return next(UnauthorizedRequestError({
|
||||||
|
message: 'Unable to validate workspace membership'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default requireMembershipAuth;
|
||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { body, param } from 'express-validator';
|
|||||||
import { requireAuth, validateRequest } from '../../middleware';
|
import { requireAuth, validateRequest } from '../../middleware';
|
||||||
import { membershipController } from '../../controllers/v1';
|
import { membershipController } from '../../controllers/v1';
|
||||||
|
|
||||||
router.get( // used for CLI (deprecate)
|
router.get( // used for old CLI (deprecate)
|
||||||
'/:workspaceId/connect',
|
'/:workspaceId/connect',
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt']
|
acceptedAuthModes: ['jwt']
|
||||||
|
|||||||
@@ -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,13 +1,17 @@
|
|||||||
|
import users from './users';
|
||||||
import secret from './secret'; // stop-supporting
|
import secret from './secret'; // stop-supporting
|
||||||
import secrets from './secrets';
|
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 {
|
||||||
|
users,
|
||||||
secret,
|
secret,
|
||||||
secrets,
|
secrets,
|
||||||
workspace,
|
workspace,
|
||||||
serviceTokenData,
|
serviceTokenData,
|
||||||
apiKeyData
|
apiKeyData,
|
||||||
}
|
environment
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
router.post(
|
router.post(
|
||||||
'/',
|
'/',
|
||||||
body('workspaceId').exists().isString().trim(),
|
body('workspaceId').exists().isString().trim(),
|
||||||
body('environment').exists().isString().trim().isIn(['dev', 'staging', 'prod', 'test']),
|
body('environment').exists().isString().trim(),
|
||||||
body('secrets')
|
body('secrets')
|
||||||
.exists()
|
.exists()
|
||||||
.custom((value) => {
|
.custom((value) => {
|
||||||
@@ -73,7 +73,7 @@ router.post(
|
|||||||
router.get(
|
router.get(
|
||||||
'/',
|
'/',
|
||||||
query('workspaceId').exists().trim(),
|
query('workspaceId').exists().trim(),
|
||||||
query('environment').exists().trim().isIn(['dev', 'staging', 'prod', 'test']),
|
query('environment').exists().trim(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt', 'serviceToken']
|
acceptedAuthModes: ['jwt', 'serviceToken']
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import {
|
||||||
|
requireAuth
|
||||||
|
} from '../../middleware';
|
||||||
|
import { usersController } from '../../controllers/v2';
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/me',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
usersController.getMe
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -3,6 +3,7 @@ const router = express.Router();
|
|||||||
import { body, param, query } from 'express-validator';
|
import { body, param, query } from 'express-validator';
|
||||||
import {
|
import {
|
||||||
requireAuth,
|
requireAuth,
|
||||||
|
requireMembershipAuth,
|
||||||
requireWorkspaceAuth,
|
requireWorkspaceAuth,
|
||||||
validateRequest
|
validateRequest
|
||||||
} from '../../middleware';
|
} from '../../middleware';
|
||||||
@@ -67,4 +68,54 @@ router.get(
|
|||||||
workspaceController.getWorkspaceServiceTokenData
|
workspaceController.getWorkspaceServiceTokenData
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: /POST to create membership and re-route inviting user to workspace there
|
||||||
|
|
||||||
|
router.get( // new - TODO: rewire dashboard to this route
|
||||||
|
'/:workspaceId/memberships',
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
}),
|
||||||
|
workspaceController.getWorkspaceMemberships
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete( // TODO - rewire dashboard to this route
|
||||||
|
'/:workspaceId/memberships/:membershipId',
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
param('membershipId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN],
|
||||||
|
}),
|
||||||
|
requireMembershipAuth({
|
||||||
|
acceptedRoles: [ADMIN]
|
||||||
|
}),
|
||||||
|
workspaceController.deleteWorkspaceMembership
|
||||||
|
);
|
||||||
|
|
||||||
|
router.patch( // TODO - rewire dashboard to this route
|
||||||
|
'/:workspaceId/memberships/:membershipId',
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
param('membershipId').exists().trim(),
|
||||||
|
body('role').exists().isString().trim().isIn([ADMIN, MEMBER]),
|
||||||
|
validateRequest,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN],
|
||||||
|
}),
|
||||||
|
requireMembershipAuth({
|
||||||
|
acceptedRoles: [ADMIN]
|
||||||
|
}),
|
||||||
|
workspaceController.updateWorkspaceMembership
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
@@ -8,6 +8,7 @@ declare global {
|
|||||||
user: any;
|
user: any;
|
||||||
workspace: any;
|
workspace: any;
|
||||||
membership: any;
|
membership: any;
|
||||||
|
targetMembership: any;
|
||||||
organization: any;
|
organization: any;
|
||||||
membershipOrg: any;
|
membershipOrg: any;
|
||||||
integration: any;
|
integration: any;
|
||||||
|
|||||||
@@ -47,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,
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
// 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);
|
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' });
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
const { secretSchema } = require('./schemas/index.ts');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates OpenAPI specs for all Infisical API endpoints:
|
||||||
|
* - spec.json in /backend for api-serving
|
||||||
|
* - spec.yaml in /docs for API reference
|
||||||
|
*/
|
||||||
|
const generateOpenAPISpec = async () => {
|
||||||
|
const doc = {
|
||||||
|
info: {
|
||||||
|
title: 'Infisical API',
|
||||||
|
description: 'List of all available APIs that can be consumed',
|
||||||
|
},
|
||||||
|
host: ['https://infisical.com'],
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: 'https://infisical.com',
|
||||||
|
description: 'Production server'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'http://localhost:8080',
|
||||||
|
description: 'Local server'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
securityDefinitions: {
|
||||||
|
bearerAuth: {
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
bearerFormat: 'JWT',
|
||||||
|
description: "This security definition uses the HTTP 'bearer' scheme, which allows the client to authenticate using a JSON Web Token (JWT) that is passed in the Authorization header of the request."
|
||||||
|
},
|
||||||
|
apiKeyAuth: {
|
||||||
|
type: 'apiKey',
|
||||||
|
in: 'header',
|
||||||
|
name: 'X-API-Key',
|
||||||
|
description: 'This security definition uses an API key, which is passed in the header of the request as the value of the "X-API-Key" header. The client must provide a valid key in order to access the API.'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
definitions: {
|
||||||
|
CurrentUser: {
|
||||||
|
_id: '',
|
||||||
|
email: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
publicKey: '',
|
||||||
|
encryptedPrivateKey: '',
|
||||||
|
iv: '',
|
||||||
|
tag: '',
|
||||||
|
updatedAt: '',
|
||||||
|
createdAt: ''
|
||||||
|
},
|
||||||
|
Membership: {
|
||||||
|
user: {
|
||||||
|
_id: '',
|
||||||
|
email: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
publicKey: '',
|
||||||
|
updatedAt: '',
|
||||||
|
createdAt: ''
|
||||||
|
},
|
||||||
|
workspace: '',
|
||||||
|
role: 'admin'
|
||||||
|
},
|
||||||
|
ProjectKey: {
|
||||||
|
encryptedkey: '',
|
||||||
|
nonce: '',
|
||||||
|
sender: {
|
||||||
|
publicKey: ''
|
||||||
|
},
|
||||||
|
receiver: '',
|
||||||
|
workspace: ''
|
||||||
|
},
|
||||||
|
CreateSecret: {
|
||||||
|
type: 'shared',
|
||||||
|
secretKeyCiphertext: '',
|
||||||
|
secretKeyIV: '',
|
||||||
|
secretKeyTag: '',
|
||||||
|
secretValueCiphertext: '',
|
||||||
|
secretValueIV: '',
|
||||||
|
secretValueTag: '',
|
||||||
|
secretCommentCiphertext: '',
|
||||||
|
secretCommentIV: '',
|
||||||
|
secretCommentTag: ''
|
||||||
|
},
|
||||||
|
UpdateSecret: {
|
||||||
|
id: '',
|
||||||
|
secretKeyCiphertext: '',
|
||||||
|
secretKeyIV: '',
|
||||||
|
secretKeyTag: '',
|
||||||
|
secretValueCiphertext: '',
|
||||||
|
secretValueIV: '',
|
||||||
|
secretValueTag: '',
|
||||||
|
secretCommentCiphertext: '',
|
||||||
|
secretCommentIV: '',
|
||||||
|
secretCommentTag: ''
|
||||||
|
},
|
||||||
|
Secret: {
|
||||||
|
_id: '',
|
||||||
|
version: 1,
|
||||||
|
workspace : '',
|
||||||
|
type: 'shared',
|
||||||
|
user: null,
|
||||||
|
secretKeyCiphertext: '',
|
||||||
|
secretKeyIV: '',
|
||||||
|
secretKeyTag: '',
|
||||||
|
secretValueCiphertext: '',
|
||||||
|
secretValueIV: '',
|
||||||
|
secretValueTag: '',
|
||||||
|
secretCommentCiphertext: '',
|
||||||
|
secretCommentIV: '',
|
||||||
|
secretCommentTag: '',
|
||||||
|
updatedAt: '',
|
||||||
|
createdAt: ''
|
||||||
|
},
|
||||||
|
Log: {
|
||||||
|
_id: '',
|
||||||
|
user: {
|
||||||
|
_id: '',
|
||||||
|
email: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: ''
|
||||||
|
},
|
||||||
|
workspace: '',
|
||||||
|
actionNames: [
|
||||||
|
'addSecrets'
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'addSecrets',
|
||||||
|
user: '',
|
||||||
|
workspace: '',
|
||||||
|
payload: [
|
||||||
|
{
|
||||||
|
oldSecretVersion: '',
|
||||||
|
newSecretVersion: ''
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
channel: 'cli',
|
||||||
|
ipAddress: '192.168.0.1',
|
||||||
|
updatedAt: '',
|
||||||
|
createdAt: ''
|
||||||
|
},
|
||||||
|
SecretSnapshot: {
|
||||||
|
workspace: '',
|
||||||
|
version: 1,
|
||||||
|
secretVersions: [
|
||||||
|
{
|
||||||
|
_id: ''
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
SecretVersion: {
|
||||||
|
_id: '',
|
||||||
|
secret: '',
|
||||||
|
version: 1,
|
||||||
|
workspace: '',
|
||||||
|
type: '',
|
||||||
|
user: '',
|
||||||
|
environment: '',
|
||||||
|
isDeleted: '',
|
||||||
|
secretKeyCiphertext: '',
|
||||||
|
secretKeyIV: '',
|
||||||
|
secretKeyTag: '',
|
||||||
|
secretValueCiphertext: '',
|
||||||
|
secretValueIV: '',
|
||||||
|
secretValueTag: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const outputJSONFile = '../spec.json';
|
||||||
|
const outputYAMLFile = '../docs/spec.yaml';
|
||||||
|
const endpointsFiles = ['../src/app.ts'];
|
||||||
|
|
||||||
|
const spec = await swaggerAutogen(outputJSONFile, endpointsFiles, doc);
|
||||||
|
await fs.writeFile(outputYAMLFile, yaml.dump(spec.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
generateOpenAPISpec();
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const secretSchema = require('./secretSchema.ts');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
secretSchema
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const secretSchema = {
|
||||||
|
_id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'objectId'
|
||||||
|
},
|
||||||
|
version: {
|
||||||
|
type: 'number'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = secretSchema;
|
||||||
@@ -16,10 +16,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FormatDotenv string = "dotenv"
|
FormatDotenv string = "dotenv"
|
||||||
FormatJson string = "json"
|
FormatJson string = "json"
|
||||||
FormatCSV string = "csv"
|
FormatCSV string = "csv"
|
||||||
FormatYaml string = "yaml"
|
FormatYaml string = "yaml"
|
||||||
|
FormatDotEnvExport string = "dotenv-export"
|
||||||
)
|
)
|
||||||
|
|
||||||
// exportCmd represents the export command
|
// exportCmd represents the export command
|
||||||
@@ -85,6 +86,8 @@ func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string,
|
|||||||
switch strings.ToLower(format) {
|
switch strings.ToLower(format) {
|
||||||
case FormatDotenv:
|
case FormatDotenv:
|
||||||
return formatAsDotEnv(envs), nil
|
return formatAsDotEnv(envs), nil
|
||||||
|
case FormatDotEnvExport:
|
||||||
|
return formatAsDotEnvExport(envs), nil
|
||||||
case FormatJson:
|
case FormatJson:
|
||||||
return formatAsJson(envs), nil
|
return formatAsJson(envs), nil
|
||||||
case FormatCSV:
|
case FormatCSV:
|
||||||
@@ -92,7 +95,7 @@ func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string,
|
|||||||
case FormatYaml:
|
case FormatYaml:
|
||||||
return formatAsYaml(envs), nil
|
return formatAsYaml(envs), nil
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("invalid format type: %s. Available format types are [%s]", format, []string{FormatDotenv, FormatJson, FormatCSV, FormatYaml})
|
return "", fmt.Errorf("invalid format type: %s. Available format types are [%s]", format, []string{FormatDotenv, FormatJson, FormatCSV, FormatYaml, FormatDotEnvExport})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +120,15 @@ func formatAsDotEnv(envs []models.SingleEnvironmentVariable) string {
|
|||||||
return dotenv
|
return dotenv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format environment variables as a dotenv file with export at the beginning
|
||||||
|
func formatAsDotEnvExport(envs []models.SingleEnvironmentVariable) string {
|
||||||
|
var dotenv string
|
||||||
|
for _, env := range envs {
|
||||||
|
dotenv += fmt.Sprintf("export %s='%s'\n", env.Key, env.Value)
|
||||||
|
}
|
||||||
|
return dotenv
|
||||||
|
}
|
||||||
|
|
||||||
func formatAsYaml(envs []models.SingleEnvironmentVariable) string {
|
func formatAsYaml(envs []models.SingleEnvironmentVariable) string {
|
||||||
var dotenv string
|
var dotenv string
|
||||||
for _, env := range envs {
|
for _, env := range envs {
|
||||||
|
|||||||
@@ -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.2",
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
|
|||||||
@@ -117,11 +117,9 @@ func GetAllEnvironmentVariables(envName string) ([]models.SingleEnvironmentVaria
|
|||||||
secrets, err := GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, envName)
|
secrets, err := GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, envName)
|
||||||
return secrets, err
|
return secrets, err
|
||||||
|
|
||||||
} else if infisicalToken != "" {
|
} else {
|
||||||
log.Debug("Trying to fetch secrets using service token")
|
log.Debug("Trying to fetch secrets using service token")
|
||||||
return GetPlainTextSecretsViaServiceToken(infisicalToken)
|
return GetPlainTextSecretsViaServiceToken(infisicalToken)
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("unable to fetch secrets because we could not find a service token or a logged in user")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
---
|
---
|
||||||
title: "Read"
|
title: "Retrieve"
|
||||||
openapi: "GET /api/v2/secrets/"
|
openapi: "GET /api/v2/secrets/"
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Roll Back to Version"
|
||||||
|
openapi: "POST /api/v1/secret/{secretId}/secret-versions/rollback"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get Versions"
|
||||||
|
openapi: "GET /api/v1/secret/{secretId}/secret-versions"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get Current User"
|
||||||
|
openapi: "GET /api/v2/users/me"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete Membership"
|
||||||
|
openapi: "DELETE /api/v2/workspace/{workspaceId}/memberships/{membershipId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get Logs"
|
||||||
|
openapi: "GET /api/v1/workspace/{workspaceId}/logs"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get Memberships"
|
||||||
|
openapi: "GET /api/v2/workspace/{workspaceId}/memberships"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Roll Back to Snapshot"
|
||||||
|
openapi: "POST /api/v1/workspace/{workspaceId}/secret-snapshots/rollback"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get Snapshots"
|
||||||
|
openapi: "GET /api/v1/workspace/{workspaceId}/secret-snapshots"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Update Membership"
|
||||||
|
openapi: "PATCH /api/v2/workspace/{workspaceId}/memberships/{membershipId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get Key"
|
||||||
|
openapi: "GET /api/v2/workspace/{workspaceId}/encrypted-key"
|
||||||
|
---
|
||||||
@@ -1,3 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: "Authentication"
|
title: "Authentication"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
To authenticate requests with Infisical, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform. You can obtain an API key from your user settings.
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
It's important to keep your API key secure, as it grants access to your
|
||||||
|
secrets in Infisical. For added security, consider rotating your API key on a
|
||||||
|
regular basis.
|
||||||
|
</Info>
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
---
|
||||||
|
title: "Create secrets"
|
||||||
|
---
|
||||||
|
|
||||||
|
In this example, we demonstrate how to add secrets to a project and environment.
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||||
|
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
|
||||||
|
|
||||||
|
## Flow
|
||||||
|
|
||||||
|
1. Get your (encrypted) private key.
|
||||||
|
2. Decrypt your (encrypted) private key with your password.
|
||||||
|
3. Get the (encrypted) project key for the project.
|
||||||
|
4. Decrypt the (encrypted) project key with your private key.
|
||||||
|
5. Encrypt your secret(s) with the project key.
|
||||||
|
6. Send (encrypted) secret(s) to the Infical API
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const ALGORITHM = 'aes-256-gcm';
|
||||||
|
const BLOCK_SIZE_BYTES = 16;
|
||||||
|
|
||||||
|
const encrypt = (
|
||||||
|
text,
|
||||||
|
secret
|
||||||
|
) => {
|
||||||
|
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
|
||||||
|
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
|
||||||
|
|
||||||
|
let ciphertext = cipher.update(text, 'utf8', 'base64');
|
||||||
|
ciphertext += cipher.final('base64');
|
||||||
|
return {
|
||||||
|
ciphertext,
|
||||||
|
iv: iv.toString('base64'),
|
||||||
|
tag: cipher.getAuthTag().toString('base64')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const decrypt = (ciphertext, iv, tag, secret) => {
|
||||||
|
const decipher = crypto.createDecipheriv(
|
||||||
|
ALGORITHM,
|
||||||
|
secret,
|
||||||
|
Buffer.from(iv, 'base64')
|
||||||
|
);
|
||||||
|
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
||||||
|
|
||||||
|
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
|
||||||
|
cleartext += decipher.final('utf8');
|
||||||
|
|
||||||
|
return cleartext;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSecrets = async () => {
|
||||||
|
const API_KEY = 'your_api_key';
|
||||||
|
const PSWD = 'your_pswd';
|
||||||
|
const WORKSPACE_ID = 'your_workspace_id';
|
||||||
|
|
||||||
|
const SECRET_KEY = 'SOME_KEY';
|
||||||
|
const SECRET_VALUE = 'SOME_VALUE';
|
||||||
|
|
||||||
|
// 1. get (encrypted) private key
|
||||||
|
const user = await axios.get(
|
||||||
|
'https://api.infisical.com/api/v2/users/me', {
|
||||||
|
headers: {
|
||||||
|
'X-API-KEY': API_KEY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. decrypt your (encrypted) private key with your password
|
||||||
|
const privateKey = decrypt({
|
||||||
|
ciphertext: user.encryptedPrivateKey,
|
||||||
|
iv: user.iv,
|
||||||
|
tag: user.tag,
|
||||||
|
secret: PSWD.slice(0, 32).padStart(32, '0');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. get the (encrypted) project key for the project
|
||||||
|
const encryptedProjectKey = await axios.get(
|
||||||
|
`https://api.infisical.com/api/v2/workspace/${WORKSPACE_ID}`, {
|
||||||
|
headers: {
|
||||||
|
'X-API-KEY': API_KEY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. decrypt the project key with your private key
|
||||||
|
const projectKey = nacl.box.open(
|
||||||
|
util.decodeBase64(encryptedProjectKey),
|
||||||
|
util.decodeBase64(projectKey.nonce),
|
||||||
|
util.decodeBase64(projectKey.sender.publicKey),
|
||||||
|
util.decodeBase64(privateKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. encrypt your secret(s) with the project key
|
||||||
|
const {
|
||||||
|
ciphertext: secretKeyCiphertext,
|
||||||
|
iv: secretKeyIV,
|
||||||
|
tag: secretKeyTag
|
||||||
|
} = encrypt(SECRET_KEY, projectKey);
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: secretValueCiphertext,
|
||||||
|
iv: secretValueIV,
|
||||||
|
tag: secretValueTag
|
||||||
|
} = encrypt(SECRET_VALUE, projectKey);
|
||||||
|
|
||||||
|
const secret = {
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Send (encrypted) secret(s) to the Infisical API
|
||||||
|
await axios.post(
|
||||||
|
`https://api.infisical.com/api/v2/secrets`,
|
||||||
|
{
|
||||||
|
workspaceId: WORKSPACE_ID,
|
||||||
|
environment: 'dev',
|
||||||
|
secrets: secret
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'X-API-KEY': API_KEY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
createSecrets();
|
||||||
|
```
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
This example uses [TweetNaCl.js](https://tweetnacl.js.org/#/), a port of
|
||||||
|
TweetNacl/Nacl, to perform asymmeric decryption of the project key but there
|
||||||
|
are ports of NaCl available in every major language.
|
||||||
|
</Info>
|
||||||
|
<Tip>
|
||||||
|
It can be useful to perform steps 1-4 ahead of time and store away your
|
||||||
|
private key (and even project key) for later use. The Infisical CLI works by
|
||||||
|
securely storing your private key via your OS keyring.
|
||||||
|
</Tip>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
title: "Delete secrets"
|
||||||
|
---
|
||||||
|
|
||||||
|
In this example, we demonstrate how to delete secrets
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||||
|
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
const deleteSecrets = async () => {
|
||||||
|
const API_KEY = "your_api_key";
|
||||||
|
const SECRET_ID = "ID"; // ID of secret to delete
|
||||||
|
|
||||||
|
// 6. Send ID(s) of secret(s) to delete to the Infisical API
|
||||||
|
await axios.delete(
|
||||||
|
`https://api.infisical.com/api/v2/secrets`,
|
||||||
|
{
|
||||||
|
secretIds: SECRET_ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"X-API-KEY": API_KEY,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteSecrets();
|
||||||
|
```
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
---
|
||||||
|
title: "Retrieve secrets"
|
||||||
|
---
|
||||||
|
|
||||||
|
In this example, we demonstrate how to retrieve secrets from a project and environment.
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||||
|
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
|
||||||
|
|
||||||
|
## Flow
|
||||||
|
|
||||||
|
1. Get your (encrypted) private key.
|
||||||
|
2. Decrypt your (encrypted) private key with your password.
|
||||||
|
3. Get the (encrypted) project key for the project.
|
||||||
|
4. Decrypt the (encrypted) project key with your private key.
|
||||||
|
5. Get secrets for a project and environment.
|
||||||
|
6. Decrypt the (encrypted) secrets
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const ALGORITHM = 'aes-256-gcm';
|
||||||
|
const BLOCK_SIZE_BYTES = 16;
|
||||||
|
|
||||||
|
const encrypt = (
|
||||||
|
text,
|
||||||
|
secret
|
||||||
|
) => {
|
||||||
|
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
|
||||||
|
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
|
||||||
|
|
||||||
|
let ciphertext = cipher.update(text, 'utf8', 'base64');
|
||||||
|
ciphertext += cipher.final('base64');
|
||||||
|
return {
|
||||||
|
ciphertext,
|
||||||
|
iv: iv.toString('base64'),
|
||||||
|
tag: cipher.getAuthTag().toString('base64')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const decrypt = (ciphertext, iv, tag, secret) => {
|
||||||
|
const decipher = crypto.createDecipheriv(
|
||||||
|
ALGORITHM,
|
||||||
|
secret,
|
||||||
|
Buffer.from(iv, 'base64')
|
||||||
|
);
|
||||||
|
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
||||||
|
|
||||||
|
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
|
||||||
|
cleartext += decipher.final('utf8');
|
||||||
|
|
||||||
|
return cleartext;
|
||||||
|
}
|
||||||
|
|
||||||
|
const retrieveSecrets = async () => {
|
||||||
|
const API_KEY = 'your_api_key';
|
||||||
|
const PSWD = 'your_pswd';
|
||||||
|
const WORKSPACE_ID = 'your_workspace_id';
|
||||||
|
|
||||||
|
// 1. get (encrypted) private key
|
||||||
|
const user = await axios.get(
|
||||||
|
'https://api.infisical.com/api/v2/users/me', {
|
||||||
|
headers: {
|
||||||
|
'X-API-KEY': API_KEY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. decrypt your (encrypted) private key with your password
|
||||||
|
const privateKey = decrypt({
|
||||||
|
ciphertext: user.encryptedPrivateKey,
|
||||||
|
iv: user.iv,
|
||||||
|
tag: user.tag,
|
||||||
|
secret: PSWD.slice(0, 32).padStart(32, '0');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. get the (encrypted) project key for the project
|
||||||
|
const encryptedProjectKey = await axios.get(
|
||||||
|
`https://api.infisical.com/api/v2/workspace/${WORKSPACE_ID}`, {
|
||||||
|
headers: {
|
||||||
|
'X-API-KEY': API_KEY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. decrypt the project key with your private key
|
||||||
|
const projectKey = nacl.box.open(
|
||||||
|
util.decodeBase64(encryptedProjectKey),
|
||||||
|
util.decodeBase64(projectKey.nonce),
|
||||||
|
util.decodeBase64(projectKey.sender.publicKey),
|
||||||
|
util.decodeBase64(privateKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. get (encrypted) secrets for a project and environment.
|
||||||
|
const encryptedSecrets = await axios.get(
|
||||||
|
'https://api.infisical.com/api/v2/secrets', {
|
||||||
|
headers: {
|
||||||
|
'X-API-KEY': API_KEY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 6. decrypt the (encrypted) secrets
|
||||||
|
const secrets = encryptedSecrets.map((encryptedSecret) => {
|
||||||
|
const secretKey = decrypt({
|
||||||
|
ciphertext: encryptedSecret.secretKeyCiphertext,
|
||||||
|
iv: encryptedSecret.secretKeyIV,
|
||||||
|
tag: encryptedSecret.secretKeyTag
|
||||||
|
secret: projectKey
|
||||||
|
});
|
||||||
|
const secretValue = decrypt({
|
||||||
|
ciphertext: encryptedSecret.secretValueCiphertext,
|
||||||
|
iv: encryptedSecret.secretValueIV,
|
||||||
|
tag: encryptedSecret.secretValueTag
|
||||||
|
secret: projectKey
|
||||||
|
});
|
||||||
|
|
||||||
|
return ({
|
||||||
|
secretKey,
|
||||||
|
secretValue
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
retrieveSecrets();
|
||||||
|
```
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
This example uses [TweetNaCl.js](https://tweetnacl.js.org/#/), a port of
|
||||||
|
TweetNacl/Nacl, to perform asymmeric decryption of the project key but there
|
||||||
|
are ports of NaCl available in every major language.
|
||||||
|
</Info>
|
||||||
|
<Tip>
|
||||||
|
It can be useful to perform steps 1-4 ahead of time and store away your
|
||||||
|
private key (and even project key) for later use. The Infisical CLI works by
|
||||||
|
securely storing your private key via your OS keyring.
|
||||||
|
</Tip>
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
---
|
||||||
|
title: "Update secrets"
|
||||||
|
---
|
||||||
|
|
||||||
|
In this example, we demonstrate how to update secrets
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||||
|
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
|
||||||
|
|
||||||
|
## Flow
|
||||||
|
|
||||||
|
1. Get your (encrypted) private key.
|
||||||
|
2. Decrypt your (encrypted) private key with your password.
|
||||||
|
3. Get the project key for the project.
|
||||||
|
4. Decrypt the project key with your private key.
|
||||||
|
5. Encrypt your secret(s) with the project key.
|
||||||
|
6. Send (encrypted) updated secret(s) to the Infical API
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const ALGORITHM = 'aes-256-gcm';
|
||||||
|
const BLOCK_SIZE_BYTES = 16;
|
||||||
|
|
||||||
|
const encrypt = (
|
||||||
|
text,
|
||||||
|
secret
|
||||||
|
) => {
|
||||||
|
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
|
||||||
|
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
|
||||||
|
|
||||||
|
let ciphertext = cipher.update(text, 'utf8', 'base64');
|
||||||
|
ciphertext += cipher.final('base64');
|
||||||
|
return {
|
||||||
|
ciphertext,
|
||||||
|
iv: iv.toString('base64'),
|
||||||
|
tag: cipher.getAuthTag().toString('base64')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const decrypt = (ciphertext, iv, tag, secret) => {
|
||||||
|
const decipher = crypto.createDecipheriv(
|
||||||
|
ALGORITHM,
|
||||||
|
secret,
|
||||||
|
Buffer.from(iv, 'base64')
|
||||||
|
);
|
||||||
|
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
||||||
|
|
||||||
|
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
|
||||||
|
cleartext += decipher.final('utf8');
|
||||||
|
|
||||||
|
return cleartext;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSecrets = async () => {
|
||||||
|
const API_KEY = 'your_api_key';
|
||||||
|
const PSWD = 'your_pswd';
|
||||||
|
const WORKSPACE_ID = 'your_workspace_id';
|
||||||
|
|
||||||
|
const SECRET_ID = 'ID' // ID of secret to update
|
||||||
|
const SECRET_KEY = 'SOME_KEY';
|
||||||
|
const SECRET_VALUE = 'SOME_VALUE';
|
||||||
|
|
||||||
|
// 1. get (encrypted) private key
|
||||||
|
const user = await axios.get(
|
||||||
|
'https://api.infisical.com/api/v2/users/me', {
|
||||||
|
headers: {
|
||||||
|
'X-API-KEY': API_KEY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. decrypt your (encrypted) private key with your password
|
||||||
|
const privateKey = decrypt({
|
||||||
|
ciphertext: user.encryptedPrivateKey,
|
||||||
|
iv: user.iv,
|
||||||
|
tag: user.tag,
|
||||||
|
secret: PSWD.slice(0, 32).padStart(32, '0');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. get the (encrypted) project key for the project
|
||||||
|
const encryptedProjectKey = await axios.get(
|
||||||
|
`https://api.infisical.com/api/v2/workspace/${WORKSPACE_ID}`, {
|
||||||
|
headers: {
|
||||||
|
'X-API-KEY': API_KEY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. decrypt the project key with your private key
|
||||||
|
const projectKey = nacl.box.open(
|
||||||
|
util.decodeBase64(encryptedProjectKey),
|
||||||
|
util.decodeBase64(projectKey.nonce),
|
||||||
|
util.decodeBase64(projectKey.sender.publicKey),
|
||||||
|
util.decodeBase64(privateKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. encrypt your secret(s) with the project key
|
||||||
|
const {
|
||||||
|
ciphertext: secretKeyCiphertext,
|
||||||
|
iv: secretKeyIV,
|
||||||
|
tag: secretKeyTag
|
||||||
|
} = encrypt(SECRET_KEY, projectKey);
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: secretValueCiphertext,
|
||||||
|
iv: secretValueIV,
|
||||||
|
tag: secretValueTag
|
||||||
|
} = encrypt(SECRET_VALUE, projectKey);
|
||||||
|
|
||||||
|
const secret = {
|
||||||
|
id: SECRET_ID,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Send (encrypted) secret(s) to the Infisical API
|
||||||
|
await axios.patch(
|
||||||
|
`https://api.infisical.com/api/v2/secrets`,
|
||||||
|
{
|
||||||
|
secrets: secret
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'X-API-KEY': API_KEY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSecrets();
|
||||||
|
```
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
This example uses [TweetNaCl.js](https://tweetnacl.js.org/#/), a port of
|
||||||
|
TweetNacl/Nacl, to perform asymmeric decryption of the project key but there
|
||||||
|
are ports of NaCl available in every major language.
|
||||||
|
</Info>
|
||||||
|
<Tip>
|
||||||
|
It can be useful to perform steps 1-4 ahead of time and store away your
|
||||||
|
private key (and even project key) for later use. The Infisical CLI works by
|
||||||
|
securely storing your private key via your OS keyring.
|
||||||
|
</Tip>
|
||||||
@@ -1,3 +1,31 @@
|
|||||||
---
|
---
|
||||||
title: "Introduction"
|
title: "Introduction"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
Infisical's REST API is currently unavailable and scheduled to go live on Jan
|
||||||
|
16!
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
Infisical's REST API provides users an alternative way to programmatically access and manage
|
||||||
|
secrets via HTTPS requests. This can be useful for automating tasks, such as
|
||||||
|
rotating credentials, or for integrating secret management into a larger system.
|
||||||
|
|
||||||
|
With the REST API, users can create, read, update, and delete secrets, as well as manage access control, query audit logs, and more.
|
||||||
|
|
||||||
|
## Concepts
|
||||||
|
|
||||||
|
Using Infisical's API to manage secrets requires a basic understanding of the system and its underlying cryptography detailed [here](/security/overview).
|
||||||
|
|
||||||
|
- Each user has a public/private key pair that is stored with the platform; private keys are encrypted locally by the user's password before being sent off to the server during the account signup process.
|
||||||
|
- Each (encrypted) secret belongs to a project and environment.
|
||||||
|
- Each project has an (encrypted) project key used to encrypt the secrets within that project; Infisical stores copies of the project key, for each member of that project, encrypted under each member's public key.
|
||||||
|
- Secrets are encrypted symmetrically by your copy of the project key belonging to the project containing.
|
||||||
|
- Infisical uses AES256-GCM and [TweetNaCl.js](https://tweetnacl.js.org/#/) for symmetric and asymmetric encryption/decryption operations.
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
Infisical's system ensures greater security such that secrets are
|
||||||
|
encrypted/decrypted on the client-side but requires users to properly
|
||||||
|
implement cryptographic operations to maintain end-to-end encryption (E2EE).
|
||||||
|
We're
|
||||||
|
</Info>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
title: "Usage"
|
||||||
|
---
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- Set up and add envars to [Infisical Cloud](https://app.infisical.com) or your self-hosted instance.
|
||||||
|
- Obtain an API Key in your user settings to be included in requests to the Infisical API.
|
||||||
|
|
||||||
|
Using Infisical's API to manage secrets requires a basic understanding of the system and its underlying cryptography detailed [here](/security/overview).
|
||||||
|
|
||||||
|
## Concepts
|
||||||
|
|
||||||
|
- Each user has a public/private key pair that is stored with the platform; private keys are encrypted locally by the user's password before being sent off to the server during the account signup process.
|
||||||
|
- Each (encrypted) secret belongs to a project and environment.
|
||||||
|
- Each project has an (encrypted) project key used to encrypt the secrets within that project; Infisical stores copies of the project key, for each member of that project, encrypted under each member's public key.
|
||||||
|
- Secrets are encrypted symmetrically by your copy of the project key belonging to the project containing.
|
||||||
|
- Infisical uses AES256-GCM and [TweetNaCl.js](https://tweetnacl.js.org/#/) for symmetric and asymmetric encryption/decryption operations.
|
||||||
@@ -12,12 +12,12 @@ Export environment variables from the platform into a file format.
|
|||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
| Option | Description | Default value |
|
| Option | Description | Default value |
|
||||||
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
||||||
| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` |
|
| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` |
|
||||||
| `--projectId` | Only required if injecting via the [service token method](../token). If you are not using service token, the project id will be automatically retrieved from the `.infisical.json` located at the root of your local project. | `None` |
|
| `--projectId` | Only required if injecting via the [service token method](../token). If you are not using service token, the project id will be automatically retrieved from the `.infisical.json` located at the root of your local project. | `None` |
|
||||||
| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` |
|
| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` |
|
||||||
| `--format` | Format of the output file. Accepted values: `dotenv`, `csv` and `json` | `dotenv` |
|
| `--format` | Format of the output file. Accepted values: `dotenv`, `dotenv-export`, `csv` and `json` | `dotenv` |
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -25,6 +25,9 @@ Export environment variables from the platform into a file format.
|
|||||||
# Export variables to a .env file
|
# Export variables to a .env file
|
||||||
infisical export > .env
|
infisical export > .env
|
||||||
|
|
||||||
|
# Export variables to a .env file (with export keyword)
|
||||||
|
infisical export --format=dotenv-export > .env
|
||||||
|
|
||||||
# Export variables to a CSV file
|
# Export variables to a CSV file
|
||||||
infisical export --format=csv > secrets.csv
|
infisical export --format=csv > secrets.csv
|
||||||
|
|
||||||
@@ -33,4 +36,5 @@ infisical export --format=json > secrets.json
|
|||||||
|
|
||||||
# Export variables to a YAML file
|
# Export variables to a YAML file
|
||||||
infisical export --format=yaml > secrets.yaml
|
infisical export --format=yaml > secrets.yaml
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 370 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
@@ -21,6 +21,16 @@
|
|||||||
"to": "#F8B7BD"
|
"to": "#F8B7BD"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"api": {
|
||||||
|
"baseUrl": [
|
||||||
|
"https://app.infisical.com",
|
||||||
|
"http://localhost:8080"
|
||||||
|
],
|
||||||
|
"auth": {
|
||||||
|
"method": "api-key",
|
||||||
|
"name": "X-API-KEY"
|
||||||
|
}
|
||||||
|
},
|
||||||
"topbarLinks": [
|
"topbarLinks": [
|
||||||
{ "name": "Log In", "url": "https://app.infisical.com/login" }
|
{ "name": "Log In", "url": "https://app.infisical.com/login" }
|
||||||
],
|
],
|
||||||
@@ -39,6 +49,11 @@
|
|||||||
"icon": "server",
|
"icon": "server",
|
||||||
"url": "self-hosting"
|
"url": "self-hosting"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "API Reference",
|
||||||
|
"icon": "cloud",
|
||||||
|
"url": "api-reference"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Integrations",
|
"name": "Integrations",
|
||||||
"icon": "plug",
|
"icon": "plug",
|
||||||
@@ -125,6 +140,56 @@
|
|||||||
"self-hosting/configuration/email"
|
"self-hosting/configuration/email"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "Overview",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/overview/introduction",
|
||||||
|
"api-reference/overview/authentication",
|
||||||
|
{
|
||||||
|
"group": "Examples",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/overview/examples/create-secrets",
|
||||||
|
"api-reference/overview/examples/retrieve-secrets",
|
||||||
|
"api-reference/overview/examples/update-secrets",
|
||||||
|
"api-reference/overview/examples/delete-secrets"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Endpoints",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"group": "Users",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/endpoints/users/me"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Projects",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/endpoints/workspaces/memberships",
|
||||||
|
"api-reference/endpoints/workspaces/update-membership",
|
||||||
|
"api-reference/endpoints/workspaces/delete-membership",
|
||||||
|
"api-reference/endpoints/workspaces/workspace-key",
|
||||||
|
"api-reference/endpoints/workspaces/logs",
|
||||||
|
"api-reference/endpoints/workspaces/secret-snapshots",
|
||||||
|
"api-reference/endpoints/workspaces/rollback-snapshot"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Secrets",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/endpoints/secrets/create",
|
||||||
|
"api-reference/endpoints/secrets/read",
|
||||||
|
"api-reference/endpoints/secrets/update",
|
||||||
|
"api-reference/endpoints/secrets/delete",
|
||||||
|
"api-reference/endpoints/secrets/versions",
|
||||||
|
"api-reference/endpoints/secrets/rollback-version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"group": "Integrations",
|
"group": "Integrations",
|
||||||
"pages": ["integrations/overview"]
|
"pages": ["integrations/overview"]
|
||||||
|
|||||||
@@ -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 @@ [email protected] # 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
|
||||||
|
[email protected] # 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>
|
||||||
|
|||||||
+2327
File diff suppressed because it is too large
Load Diff
@@ -68,18 +68,5 @@ export default function RouteGuard({ children }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authorized) {
|
return children;
|
||||||
return children;
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div className="w-screen h-screen bg-bunker-800 flex items-center justify-center">
|
|
||||||
<Image
|
|
||||||
src="/images/loading/loading.gif"
|
|
||||||
height={70}
|
|
||||||
width={120}
|
|
||||||
alt="google logo"
|
|
||||||
></Image>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,7 +66,7 @@ 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,
|
||||||
@@ -101,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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -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()}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ const CloudIntegration = ({
|
|||||||
integrationAuths
|
integrationAuths
|
||||||
.map((authorization) => authorization.integration)
|
.map((authorization) => authorization.integration)
|
||||||
.includes(cloudIntegrationOption.name.toLowerCase()) && (
|
.includes(cloudIntegrationOption.name.toLowerCase()) && (
|
||||||
<div className="absolute group z-50 top-0 right-0 flex flex-row">
|
<div className="absolute group z-40 top-0 right-0 flex flex-row">
|
||||||
<div
|
<div
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ import getIntegrationApps from "../../pages/api/integrations/GetIntegrationApps"
|
|||||||
import updateIntegration from "../../pages/api/integrations/updateIntegration"
|
import updateIntegration from "../../pages/api/integrations/updateIntegration"
|
||||||
import {
|
import {
|
||||||
contextNetlifyMapping,
|
contextNetlifyMapping,
|
||||||
envMapping,
|
|
||||||
reverseContextNetlifyMapping,
|
reverseContextNetlifyMapping,
|
||||||
reverseEnvMapping,
|
|
||||||
} from "../../public/data/frequentConstants";
|
} from "../../public/data/frequentConstants";
|
||||||
|
|
||||||
interface Integration {
|
interface Integration {
|
||||||
@@ -36,13 +34,23 @@ interface IntegrationApp {
|
|||||||
siteId: string;
|
siteId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Integration = ({
|
type Props = {
|
||||||
integration
|
|
||||||
}: {
|
|
||||||
integration: Integration;
|
integration: Integration;
|
||||||
}) => {
|
environments: Array<{ name: string; slug: string }>;
|
||||||
const [integrationEnvironment, setIntegrationEnvironment] = useState(
|
};
|
||||||
reverseEnvMapping[integration.environment]
|
|
||||||
|
const Integration = ({
|
||||||
|
integration,
|
||||||
|
environments = []
|
||||||
|
}:Props ) => {
|
||||||
|
// set initial environment. This find will only execute when component is mounting
|
||||||
|
const [integrationEnvironment, setIntegrationEnvironment] = useState<
|
||||||
|
Props['environments'][0]
|
||||||
|
>(
|
||||||
|
environments.find(({ slug }) => slug === integration.environment) || {
|
||||||
|
name: '',
|
||||||
|
slug: '',
|
||||||
|
}
|
||||||
);
|
);
|
||||||
const [fileState, setFileState] = useState([]);
|
const [fileState, setFileState] = useState([]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -93,7 +101,7 @@ const Integration = ({
|
|||||||
case "vercel":
|
case "vercel":
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-gray-400 text-xs font-semibold mb-2">
|
<div className="text-gray-400 text-xs font-semibold mb-2 w-60">
|
||||||
ENVIRONMENT
|
ENVIRONMENT
|
||||||
</div>
|
</div>
|
||||||
<ListBox
|
<ListBox
|
||||||
@@ -104,6 +112,7 @@ const Integration = ({
|
|||||||
] : null}
|
] : null}
|
||||||
selected={integrationTarget}
|
selected={integrationTarget}
|
||||||
onChange={setIntegrationTarget}
|
onChange={setIntegrationTarget}
|
||||||
|
isFull={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -136,42 +145,47 @@ const Integration = ({
|
|||||||
if (!integrationApp || apps.length === 0) return <div></div>
|
if (!integrationApp || apps.length === 0) return <div></div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between">
|
<div className='max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between'>
|
||||||
<div className="flex">
|
<div className='flex'>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-400 text-xs font-semibold mb-2">ENVIRONMENT</p>
|
<p className='text-gray-400 text-xs font-semibold mb-2'>
|
||||||
<ListBox data={!integration.isActive ? [
|
ENVIRONMENT
|
||||||
"Development",
|
</p>
|
||||||
"Staging",
|
<ListBox
|
||||||
"Testing",
|
data={
|
||||||
"Production",
|
!integration.isActive
|
||||||
] : null}
|
? environments.map(({ name }) => name)
|
||||||
selected={integrationEnvironment}
|
: null
|
||||||
onChange={(environment) => {
|
}
|
||||||
setIntegrationEnvironment(environment);
|
selected={integrationEnvironment.name}
|
||||||
}}
|
onChange={(envName) =>
|
||||||
|
setIntegrationEnvironment(
|
||||||
|
environments.find(({ name }) => envName === name) || {
|
||||||
|
name: 'unknown',
|
||||||
|
slug: 'unknown',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
isFull={true}
|
isFull={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-2">
|
<div className='pt-2'>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faArrowRight}
|
icon={faArrowRight}
|
||||||
className="mx-4 text-gray-400 mt-8"
|
className='mx-4 text-gray-400 mt-8'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-2">
|
<div className='mr-2'>
|
||||||
<p className="text-gray-400 text-xs font-semibold mb-2">
|
<p className='text-gray-400 text-xs font-semibold mb-2'>
|
||||||
INTEGRATION
|
INTEGRATION
|
||||||
</p>
|
</p>
|
||||||
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300">
|
<div className='py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300'>
|
||||||
{integration.integration.charAt(0).toUpperCase() +
|
{integration.integration.charAt(0).toUpperCase() +
|
||||||
integration.integration.slice(1)}
|
integration.integration.slice(1)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-2">
|
<div className='mr-2'>
|
||||||
<div className="text-gray-400 text-xs font-semibold mb-2">
|
<div className='text-gray-400 text-xs font-semibold mb-2'>APP</div>
|
||||||
APP
|
|
||||||
</div>
|
|
||||||
<ListBox
|
<ListBox
|
||||||
data={!integration.isActive ? apps.map((app) => app.name) : null}
|
data={!integration.isActive ? apps.map((app) => app.name) : null}
|
||||||
selected={integrationApp}
|
selected={integrationApp}
|
||||||
@@ -182,52 +196,55 @@ const Integration = ({
|
|||||||
</div>
|
</div>
|
||||||
{renderIntegrationSpecificParams(integration)}
|
{renderIntegrationSpecificParams(integration)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end">
|
<div className='flex items-end'>
|
||||||
{integration.isActive ? (
|
{integration.isActive ? (
|
||||||
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4">
|
<div className='max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4'>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faRotate}
|
icon={faRotate}
|
||||||
className="text-lg mr-2.5 text-primary animate-spin"
|
className='text-lg mr-2.5 text-primary animate-spin'
|
||||||
/>
|
|
||||||
<div className="text-gray-300 font-semibold">In Sync</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
text="Start Integration"
|
|
||||||
onButtonPressed={async () => {
|
|
||||||
|
|
||||||
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
|
|
||||||
const siteId = siteApp?.siteId ? siteApp.siteId : null;
|
|
||||||
|
|
||||||
const result = await updateIntegration({
|
|
||||||
integrationId: integration._id,
|
|
||||||
environment: envMapping[integrationEnvironment],
|
|
||||||
app: integrationApp,
|
|
||||||
isActive: true,
|
|
||||||
target: integrationTarget ? integrationTarget.toLowerCase() : null,
|
|
||||||
context: integrationContext ? reverseContextNetlifyMapping[integrationContext] : null,
|
|
||||||
siteId
|
|
||||||
});
|
|
||||||
|
|
||||||
router.reload();
|
|
||||||
}}
|
|
||||||
color="mineshaft"
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
|
|
||||||
<Button
|
|
||||||
onButtonPressed={async () => {
|
|
||||||
await deleteIntegration({
|
|
||||||
integrationId: integration._id,
|
|
||||||
});
|
|
||||||
router.reload();
|
|
||||||
}}
|
|
||||||
color="red"
|
|
||||||
size="icon-md"
|
|
||||||
icon={faX}
|
|
||||||
/>
|
/>
|
||||||
|
<div className='text-gray-300 font-semibold'>In Sync</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
text='Start Integration'
|
||||||
|
onButtonPressed={async () => {
|
||||||
|
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
|
||||||
|
const siteId = siteApp?.siteId ? siteApp.siteId : null;
|
||||||
|
|
||||||
|
await updateIntegration({
|
||||||
|
integrationId: integration._id,
|
||||||
|
environment: integrationEnvironment.slug,
|
||||||
|
app: integrationApp,
|
||||||
|
isActive: true,
|
||||||
|
target: integrationTarget
|
||||||
|
? integrationTarget.toLowerCase()
|
||||||
|
: null,
|
||||||
|
context: integrationContext
|
||||||
|
? reverseContextNetlifyMapping[integrationContext]
|
||||||
|
: null,
|
||||||
|
siteId,
|
||||||
|
});
|
||||||
|
|
||||||
|
router.reload();
|
||||||
|
}}
|
||||||
|
color='mineshaft'
|
||||||
|
size='md'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className='opacity-50 hover:opacity-100 duration-200 ml-2'>
|
||||||
|
<Button
|
||||||
|
onButtonPressed={async () => {
|
||||||
|
await deleteIntegration({
|
||||||
|
integrationId: integration._id,
|
||||||
|
});
|
||||||
|
router.reload();
|
||||||
|
}}
|
||||||
|
color='red'
|
||||||
|
size='icon-md'
|
||||||
|
icon={faX}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import guidGenerator from "~/utilities/randomId";
|
|||||||
import Integration from "./Integration";
|
import Integration from "./Integration";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
integrations: any
|
integrations: any;
|
||||||
|
environments: Array<{ name: string; slug: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IntegrationType {
|
interface IntegrationType {
|
||||||
@@ -19,7 +20,8 @@ interface IntegrationType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ProjectIntegrationSection = ({
|
const ProjectIntegrationSection = ({
|
||||||
integrations
|
integrations,
|
||||||
|
environments = [],
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
return integrations.length > 0 ? (
|
return integrations.length > 0 ? (
|
||||||
<div className="mb-12">
|
<div className="mb-12">
|
||||||
@@ -33,6 +35,7 @@ const ProjectIntegrationSection = ({
|
|||||||
<Integration
|
<Integration
|
||||||
key={guidGenerator()}
|
key={guidGenerator()}
|
||||||
integration={integration}
|
integration={integration}
|
||||||
|
environments={environments}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const attemptLogin = async (
|
|||||||
// if everything works, go the main dashboard page.
|
// if everything works, go the main dashboard page.
|
||||||
const { token, publicKey, encryptedPrivateKey, iv, tag } =
|
const { token, publicKey, encryptedPrivateKey, iv, tag } =
|
||||||
await login2(email, clientProof);
|
await login2(email, clientProof);
|
||||||
|
|
||||||
SecurityClient.setToken(token);
|
SecurityClient.setToken(token);
|
||||||
|
|
||||||
const privateKey = Aes256Gcm.decrypt({
|
const privateKey = Aes256Gcm.decrypt({
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: str
|
|||||||
const fileDownloadUrl = URL.createObjectURL(blob);
|
const fileDownloadUrl = URL.createObjectURL(blob);
|
||||||
const alink = document.createElement('a');
|
const alink = document.createElement('a');
|
||||||
alink.href = fileDownloadUrl;
|
alink.href = fileDownloadUrl;
|
||||||
alink.download = envMapping[env] + '.env';
|
alink.download = env + '.env';
|
||||||
alink.click();
|
alink.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import getSecrets from '~/pages/api/files/GetSecrets';
|
import getSecrets from '~/pages/api/files/GetSecrets';
|
||||||
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
|
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
|
||||||
|
|
||||||
import { envMapping } from '../../../public/data/frequentConstants';
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
decryptAssymmetric,
|
decryptAssymmetric,
|
||||||
decryptSymmetric
|
decryptSymmetric
|
||||||
@@ -35,7 +33,7 @@ interface SecretProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface FunctionProps {
|
interface FunctionProps {
|
||||||
env: keyof typeof envMapping;
|
env: string;
|
||||||
setIsKeyAvailable: any;
|
setIsKeyAvailable: any;
|
||||||
setData: any;
|
setData: any;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
@@ -58,7 +56,7 @@ const getSecretsForProject = async ({
|
|||||||
try {
|
try {
|
||||||
let encryptedSecrets;
|
let encryptedSecrets;
|
||||||
try {
|
try {
|
||||||
encryptedSecrets = await getSecrets(workspaceId, envMapping[env]);
|
encryptedSecrets = await getSecrets(workspaceId, env);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('ERROR: Not able to access the latest version of secrets');
|
console.log('ERROR: Not able to access the latest version of secrets');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { usePopUp } from './usePopUp';
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
interface usePopUpProps {
|
||||||
|
name: Readonly<string>;
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* to provide better intellisense
|
||||||
|
* checks which type of inputProps were given and converts them into key-names
|
||||||
|
* SIDENOTE: On inputting give it as const and not string with (as const)
|
||||||
|
*/
|
||||||
|
type usePopUpState<T extends Readonly<string[]> | usePopUpProps[]> = {
|
||||||
|
[P in T extends usePopUpProps[] ? T[number]['name'] : T[number]]: {
|
||||||
|
isOpen: boolean;
|
||||||
|
data?: unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface usePopUpReturn<T extends Readonly<string[]> | usePopUpProps[]> {
|
||||||
|
popUp: usePopUpState<T>;
|
||||||
|
handlePopUpOpen: (popUpName: keyof usePopUpState<T>, data?: unknown) => void;
|
||||||
|
handlePopUpClose: (popUpName: keyof usePopUpState<T>) => void;
|
||||||
|
handlePopUpToggle: (popUpName: keyof usePopUpState<T>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This hook is used to manage multiple popUps/modal/dialog in a page
|
||||||
|
* Provides api to open,close,toggle and also store temporary data for the popUp
|
||||||
|
* @param popUpNames: the names of popUp containers eg: ["popUp1","second"] or [{name:"popUp2",isOpen:bool}]
|
||||||
|
*/
|
||||||
|
export const usePopUp = <T extends Readonly<string[]> | usePopUpProps[]>(
|
||||||
|
popUpNames: T
|
||||||
|
): usePopUpReturn<T> => {
|
||||||
|
const [popUp, setPopUp] = useState<usePopUpState<T>>(
|
||||||
|
Object.fromEntries(
|
||||||
|
popUpNames.map((popUpName) =>
|
||||||
|
typeof popUpName === 'string'
|
||||||
|
? [popUpName, { isOpen: false }]
|
||||||
|
: [popUpName.name, { isOpen: popUpName.isOpen }]
|
||||||
|
) // convert into an array of [[popUpName,state]] then into Object
|
||||||
|
) as usePopUpState<T> // to override generic string return type of the function
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePopUpOpen = useCallback(
|
||||||
|
(popUpName: keyof usePopUpState<T>, data?: unknown) => {
|
||||||
|
setPopUp((popUp) => ({ ...popUp, [popUpName]: { isOpen: true, data } }));
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePopUpClose = useCallback((popUpName: keyof usePopUpState<T>) => {
|
||||||
|
setPopUp((popUp) => ({ ...popUp, [popUpName]: { isOpen: false } }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePopUpToggle = useCallback((popUpName: keyof usePopUpState<T>) => {
|
||||||
|
setPopUp((popUp) => ({
|
||||||
|
...popUp,
|
||||||
|
[popUpName]: { isOpen: !popUp[popUpName].isOpen },
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
popUp,
|
||||||
|
handlePopUpOpen,
|
||||||
|
handlePopUpClose,
|
||||||
|
handlePopUpToggle,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -8,7 +8,7 @@ module.exports = {
|
|||||||
debug: false,
|
debug: false,
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: "en",
|
defaultLocale: "en",
|
||||||
locales: ["en", "ko", "fr"],
|
locales: ["en", "ko", "fr", "pt-BR"],
|
||||||
},
|
},
|
||||||
fallbackLng: {
|
fallbackLng: {
|
||||||
default: ["en"],
|
default: ["en"],
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import SecurityClient from '~/utilities/SecurityClient';
|
||||||
|
|
||||||
|
type NewEnvironmentInfo = {
|
||||||
|
environmentSlug: string;
|
||||||
|
environmentName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This route deletes a specified workspace.
|
||||||
|
* @param {*} workspaceId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const createEnvironment = (workspaceId:string, newEnv: NewEnvironmentInfo) => {
|
||||||
|
return SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/environments`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(newEnv)
|
||||||
|
}).then(async (res) => {
|
||||||
|
if (res && res.status == 200) {
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
console.log('Failed to create environment');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createEnvironment;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import SecurityClient from '~/utilities/SecurityClient';
|
||||||
|
/**
|
||||||
|
* This route deletes a specified env.
|
||||||
|
* @param {*} workspaceId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const deleteEnvironment = (workspaceId: string, environmentSlug: string) => {
|
||||||
|
return SecurityClient.fetchCall(
|
||||||
|
`/api/v2/workspace/${workspaceId}/environments`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ environmentSlug }),
|
||||||
|
}
|
||||||
|
).then(async (res) => {
|
||||||
|
if (res && res.status == 200) {
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
console.log('Failed to delete environment');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default deleteEnvironment;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import SecurityClient from '~/utilities/SecurityClient';
|
||||||
|
|
||||||
|
type EnvironmentInfo = {
|
||||||
|
oldEnvironmentSlug: string;
|
||||||
|
environmentSlug: string;
|
||||||
|
environmentName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This route updates a specified environment.
|
||||||
|
* @param {*} workspaceId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const updateEnvironment = (workspaceId: string, env: EnvironmentInfo) => {
|
||||||
|
return SecurityClient.fetchCall(
|
||||||
|
`/api/v2/workspace/${workspaceId}/environments`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(env),
|
||||||
|
}
|
||||||
|
).then(async (res) => {
|
||||||
|
if (res && res.status == 200) {
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
console.log('Failed to update environment');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default updateEnvironment;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import SecurityClient from '~/utilities/SecurityClient';
|
||||||
|
|
||||||
|
interface Workspace {
|
||||||
|
__v: number;
|
||||||
|
_id: string;
|
||||||
|
name: string;
|
||||||
|
organization: string;
|
||||||
|
environments: Array<{ name: string; slug: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This route lets us get the workspaces of a certain user
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getAWorkspace = (workspaceID:string) => {
|
||||||
|
return SecurityClient.fetchCall(`/api/v1/workspace/${workspaceID}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}).then(async (res) => {
|
||||||
|
if (res?.status == 200) {
|
||||||
|
const data = (await res.json()) as unknown as { workspace: Workspace };
|
||||||
|
return data.workspace;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Failed to get workspace');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getAWorkspace;
|
||||||
@@ -5,6 +5,7 @@ interface Workspace {
|
|||||||
_id: string;
|
_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
|
environments: Array<{name:string, slug:string}>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+263
-201
@@ -2,7 +2,7 @@ import { Fragment, useCallback, useEffect, useState } from 'react';
|
|||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
import {
|
import {
|
||||||
faArrowDownAZ,
|
faArrowDownAZ,
|
||||||
faArrowDownZA,
|
faArrowDownZA,
|
||||||
@@ -34,7 +34,6 @@ import getSecretsForProject from '~/components/utilities/secrets/getSecretsForPr
|
|||||||
import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps';
|
import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps';
|
||||||
import guidGenerator from '~/utilities/randomId';
|
import guidGenerator from '~/utilities/randomId';
|
||||||
|
|
||||||
import { envMapping, reverseEnvMapping } from '../../public/data/frequentConstants';
|
|
||||||
import addSecrets from '../api/files/AddSecrets';
|
import addSecrets from '../api/files/AddSecrets';
|
||||||
import deleteSecrets from '../api/files/DeleteSecrets';
|
import deleteSecrets from '../api/files/DeleteSecrets';
|
||||||
import updateSecrets from '../api/files/UpdateSecrets';
|
import updateSecrets from '../api/files/UpdateSecrets';
|
||||||
@@ -43,6 +42,10 @@ import checkUserAction from '../api/userActions/checkUserAction';
|
|||||||
import registerUserAction from '../api/userActions/registerUserAction';
|
import registerUserAction from '../api/userActions/registerUserAction';
|
||||||
import getWorkspaces from '../api/workspace/getWorkspaces';
|
import getWorkspaces from '../api/workspace/getWorkspaces';
|
||||||
|
|
||||||
|
type WorkspaceEnv = {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface SecretDataProps {
|
interface SecretDataProps {
|
||||||
pos: number;
|
pos: number;
|
||||||
@@ -104,11 +107,8 @@ export default function Dashboard() {
|
|||||||
const [initialData, setInitialData] = useState<SecretDataProps[] | null | undefined>([]);
|
const [initialData, setInitialData] = useState<SecretDataProps[] | null | undefined>([]);
|
||||||
const [buttonReady, setButtonReady] = useState(false);
|
const [buttonReady, setButtonReady] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [workspaceId, setWorkspaceId] = useState('');
|
|
||||||
const [blurred, setBlurred] = useState(true);
|
const [blurred, setBlurred] = useState(true);
|
||||||
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
|
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
|
||||||
const [env, setEnv] = useState('Development');
|
|
||||||
const [snapshotEnv, setSnapshotEnv] = useState('Development');
|
|
||||||
const [isNew, setIsNew] = useState(false);
|
const [isNew, setIsNew] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [searchKeys, setSearchKeys] = useState('');
|
const [searchKeys, setSearchKeys] = useState('');
|
||||||
@@ -116,7 +116,7 @@ export default function Dashboard() {
|
|||||||
const [sortMethod, setSortMethod] = useState('alphabetical');
|
const [sortMethod, setSortMethod] = useState('alphabetical');
|
||||||
const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false);
|
const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false);
|
||||||
const [hasUserEverPushed, setHasUserEverPushed] = useState(false);
|
const [hasUserEverPushed, setHasUserEverPushed] = useState(false);
|
||||||
const [sidebarSecretId, toggleSidebar] = useState("None");
|
const [sidebarSecretId, toggleSidebar] = useState('None');
|
||||||
const [PITSidebarOpen, togglePITSidebar] = useState(false);
|
const [PITSidebarOpen, togglePITSidebar] = useState(false);
|
||||||
const [sharedToHide, setSharedToHide] = useState<string[]>([]);
|
const [sharedToHide, setSharedToHide] = useState<string[]>([]);
|
||||||
const [snapshotData, setSnapshotData] = useState<SnapshotProps>();
|
const [snapshotData, setSnapshotData] = useState<SnapshotProps>();
|
||||||
@@ -126,6 +126,16 @@ export default function Dashboard() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
|
|
||||||
|
const workspaceId = router.query.id as string;
|
||||||
|
const [workspaceEnvs, setWorkspaceEnvs] = useState<WorkspaceEnv[]>([]);
|
||||||
|
|
||||||
|
const [selectedSnapshotEnv, setSelectedSnapshotEnv] =
|
||||||
|
useState<WorkspaceEnv>();
|
||||||
|
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv>({
|
||||||
|
name: '',
|
||||||
|
slug: '',
|
||||||
|
});
|
||||||
|
|
||||||
// #TODO: fix save message for changing reroutes
|
// #TODO: fix save message for changing reroutes
|
||||||
// const beforeRouteHandler = (url) => {
|
// const beforeRouteHandler = (url) => {
|
||||||
// const warningText =
|
// const warningText =
|
||||||
@@ -172,25 +182,37 @@ export default function Dashboard() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const tempNumSnapshots = await getProjectSercetSnapshotsCount({ workspaceId: String(router.query.id) })
|
const tempNumSnapshots = await getProjectSercetSnapshotsCount({
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
setNumSnapshots(tempNumSnapshots);
|
setNumSnapshots(tempNumSnapshots);
|
||||||
const userWorkspaces = await getWorkspaces();
|
const userWorkspaces = await getWorkspaces();
|
||||||
const listWorkspaces = userWorkspaces.map((workspace) => workspace._id);
|
const workspace = userWorkspaces.find(
|
||||||
if (
|
(workspace) => workspace._id === workspaceId
|
||||||
!listWorkspaces.includes(router.asPath.split('/')[2])
|
);
|
||||||
) {
|
if (!workspace) {
|
||||||
router.push('/dashboard/' + listWorkspaces[0]);
|
router.push('/dashboard/' + userWorkspaces?.[0]?._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setWorkspaceEnvs(workspace?.environments || []);
|
||||||
|
// set env
|
||||||
|
const env = workspace?.environments?.[0] || {
|
||||||
|
name: 'unknown',
|
||||||
|
slug: 'unkown',
|
||||||
|
};
|
||||||
|
setSelectedEnv(env);
|
||||||
|
setSelectedSnapshotEnv(env);
|
||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
setIsNew(
|
setIsNew(
|
||||||
(Date.parse(String(new Date())) - Date.parse(user.createdAt)) / 60000 < 3
|
(Date.parse(String(new Date())) - Date.parse(user.createdAt)) /
|
||||||
|
60000 <
|
||||||
|
3
|
||||||
? true
|
? true
|
||||||
: false
|
: false
|
||||||
);
|
);
|
||||||
|
|
||||||
const userAction = await checkUserAction({
|
const userAction = await checkUserAction({
|
||||||
action: 'first_time_secrets_pushed'
|
action: 'first_time_secrets_pushed',
|
||||||
});
|
});
|
||||||
setHasUserEverPushed(userAction ? true : false);
|
setHasUserEverPushed(userAction ? true : false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -198,32 +220,33 @@ export default function Dashboard() {
|
|||||||
setData(undefined);
|
setData(undefined);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, [workspaceId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setBlurred(true);
|
setBlurred(true);
|
||||||
setWorkspaceId(String(router.query.id));
|
// ENV
|
||||||
|
|
||||||
const dataToSort = await getSecretsForProject({
|
const dataToSort = await getSecretsForProject({
|
||||||
env,
|
env: selectedEnv.slug,
|
||||||
setIsKeyAvailable,
|
setIsKeyAvailable,
|
||||||
setData,
|
setData,
|
||||||
workspaceId: String(router.query.id)
|
workspaceId,
|
||||||
});
|
});
|
||||||
setInitialData(dataToSort);
|
setInitialData(dataToSort);
|
||||||
reorderRows(dataToSort);
|
reorderRows(dataToSort);
|
||||||
|
|
||||||
setIsLoading(false);
|
setTimeout(
|
||||||
|
() => setIsLoading(false)
|
||||||
|
, 700);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Error', error);
|
console.log('Error', error);
|
||||||
setData(undefined);
|
setData(undefined);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [env]);
|
}, [selectedEnv]);
|
||||||
|
|
||||||
const addRow = () => {
|
const addRow = () => {
|
||||||
setIsNew(false);
|
setIsNew(false);
|
||||||
@@ -237,37 +260,22 @@ export default function Dashboard() {
|
|||||||
value: '',
|
value: '',
|
||||||
valueOverride: undefined,
|
valueOverride: undefined,
|
||||||
comment: '',
|
comment: '',
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => {
|
const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => {
|
||||||
setButtonReady(true);
|
setButtonReady(true);
|
||||||
toggleSidebar("None");
|
toggleSidebar('None');
|
||||||
createNotification({
|
createNotification({
|
||||||
text: `${secretName} has been deleted. Remember to save changes.`,
|
text: `${secretName} has been deleted. Remember to save changes.`,
|
||||||
type: 'error'
|
type: 'error',
|
||||||
});
|
});
|
||||||
sortValuesHandler(data!.filter((row: SecretDataProps) => !ids.includes(row.id)), sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical");
|
sortValuesHandler(
|
||||||
};
|
data!.filter((row: SecretDataProps) => !ids.includes(row.id)),
|
||||||
|
sortMethod == 'alhpabetical' ? '-alphabetical' : 'alphabetical'
|
||||||
/**
|
);
|
||||||
* This function deleted the override of a certain secrer
|
|
||||||
* @param {string} id - id of a shared secret; the override with the same key should be deleted
|
|
||||||
*/
|
|
||||||
const deleteOverride = (id: string) => {
|
|
||||||
setButtonReady(true);
|
|
||||||
|
|
||||||
// find which shared secret corresponds to the overriden version
|
|
||||||
// const sharedVersionOfOverride = data!.filter(secret => secret.type == "shared" && secret.key == data!.filter(row => row.id == id)[0]?.key)[0]?.id;
|
|
||||||
|
|
||||||
// change the sidebar to this shared secret; and unhide it
|
|
||||||
// toggleSidebar(sharedVersionOfOverride)
|
|
||||||
// setSharedToHide(sharedToHide!.filter(tempId => tempId != sharedVersionOfOverride))
|
|
||||||
|
|
||||||
// resort secrets
|
|
||||||
// const tempData = data!.filter((row: SecretDataProps) => !(row.key == data!.filter(row => row.id == id)[0]?.key && row.type == 'personal'))
|
|
||||||
// sortValuesHandler(tempData, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical")
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyValue = (value: string, pos: number) => {
|
const modifyValue = (value: string, pos: number) => {
|
||||||
@@ -341,14 +349,14 @@ export default function Dashboard() {
|
|||||||
if (nameErrors) {
|
if (nameErrors) {
|
||||||
return createNotification({
|
return createNotification({
|
||||||
text: 'Solve all name errors before saving secrets.',
|
text: 'Solve all name errors before saving secrets.',
|
||||||
type: 'error'
|
type: 'error',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (duplicatesExist) {
|
if (duplicatesExist) {
|
||||||
return createNotification({
|
return createNotification({
|
||||||
text: 'Remove duplicated secret names before saving.',
|
text: 'Remove duplicated secret names before saving.',
|
||||||
type: 'error'
|
type: 'error',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,15 +412,15 @@ export default function Dashboard() {
|
|||||||
await deleteSecrets({ secretIds: secretsToBeDeleted.concat(overridesToBeDeleted) });
|
await deleteSecrets({ secretIds: secretsToBeDeleted.concat(overridesToBeDeleted) });
|
||||||
}
|
}
|
||||||
if (secretsToBeAdded.concat(overridesToBeAdded).length > 0) {
|
if (secretsToBeAdded.concat(overridesToBeAdded).length > 0) {
|
||||||
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded.concat(overridesToBeAdded), workspaceId, env: envMapping[env] });
|
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded.concat(overridesToBeAdded), workspaceId, env: selectedEnv.slug });
|
||||||
secrets && await addSecrets({ secrets, env: envMapping[env], workspaceId });
|
secrets && await addSecrets({ secrets, env: selectedEnv.slug, workspaceId });
|
||||||
}
|
}
|
||||||
if (secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
|
if (secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
|
||||||
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated), workspaceId, env: envMapping[env] });
|
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated), workspaceId, env: selectedEnv.slug });
|
||||||
secrets && await updateSecrets({ secrets });
|
secrets && await updateSecrets({ secrets });
|
||||||
}
|
}
|
||||||
|
|
||||||
setInitialData(newData);
|
setInitialData(structuredClone(newData));
|
||||||
|
|
||||||
// If this user has never saved environment variables before, show them a prompt to read docs
|
// If this user has never saved environment variables before, show them a prompt to read docs
|
||||||
if (!hasUserEverPushed) {
|
if (!hasUserEverPushed) {
|
||||||
@@ -434,36 +442,49 @@ export default function Dashboard() {
|
|||||||
setBlurred(!blurred);
|
setBlurred(!blurred);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortValuesHandler = (dataToSort: SecretDataProps[] | 1, specificSortMethod?: 'alphabetical' | '-alphabetical') => {
|
const sortValuesHandler = (
|
||||||
const howToSort = specificSortMethod == undefined ? sortMethod : specificSortMethod;
|
dataToSort: SecretDataProps[] | 1,
|
||||||
|
specificSortMethod?: 'alphabetical' | '-alphabetical'
|
||||||
|
) => {
|
||||||
|
const howToSort =
|
||||||
|
specificSortMethod == undefined ? sortMethod : specificSortMethod;
|
||||||
const sortedData = (dataToSort != 1 ? dataToSort : data)!
|
const sortedData = (dataToSort != 1 ? dataToSort : data)!
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
howToSort == 'alphabetical'
|
howToSort == 'alphabetical'
|
||||||
? a.key.localeCompare(b.key)
|
? a.key.localeCompare(b.key)
|
||||||
: b.key.localeCompare(a.key)
|
: b.key.localeCompare(a.key)
|
||||||
)
|
)
|
||||||
.map((item: SecretDataProps, index: number) => {
|
.map((item: SecretDataProps, index: number) => {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
pos: index
|
pos: index,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
setData(sortedData);
|
setData(sortedData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteCertainRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => {
|
const deleteCertainRow = ({
|
||||||
deleteRow({ids, secretName});
|
ids,
|
||||||
|
secretName,
|
||||||
|
}: {
|
||||||
|
ids: string[];
|
||||||
|
secretName: string;
|
||||||
|
}) => {
|
||||||
|
deleteRow({ ids, secretName });
|
||||||
};
|
};
|
||||||
|
|
||||||
return data ? (
|
return data ? (
|
||||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
<div className='bg-bunker-800 max-h-screen flex flex-col justify-between text-white'>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{t("common:head-title", { title: t("dashboard:title") })}</title>
|
<title>{t('common:head-title', { title: t('dashboard:title') })}</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel='icon' href='/infisical.ico' />
|
||||||
<meta property="og:image" content="/images/message.png" />
|
<meta property='og:image' content='/images/message.png' />
|
||||||
<meta property="og:title" content={String(t("dashboard:og-title"))} />
|
<meta property='og:title' content={String(t('dashboard:og-title'))} />
|
||||||
<meta name="og:description" content={String(t("dashboard:og-description"))} />
|
<meta
|
||||||
|
name='og:description'
|
||||||
|
content={String(t('dashboard:og-description'))}
|
||||||
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
{sidebarSecretId != "None" && <SideBar
|
{sidebarSecretId != "None" && <SideBar
|
||||||
@@ -488,56 +509,68 @@ export default function Dashboard() {
|
|||||||
<NavHeader pageName={t("dashboard:title")} isProjectRelated={true} />
|
<NavHeader pageName={t("dashboard:title")} isProjectRelated={true} />
|
||||||
{checkDocsPopUpVisible && (
|
{checkDocsPopUpVisible && (
|
||||||
<BottonRightPopup
|
<BottonRightPopup
|
||||||
buttonText={t("dashboard:check-docs.button")}
|
buttonText={t('dashboard:check-docs.button')}
|
||||||
buttonLink="https://infisical.com/docs/getting-started/introduction"
|
buttonLink='https://infisical.com/docs/getting-started/introduction'
|
||||||
titleText={t("dashboard:check-docs.title")}
|
titleText={t('dashboard:check-docs.title')}
|
||||||
emoji="🎉"
|
emoji='🎉'
|
||||||
textLine1={t("dashboard:check-docs.line1")}
|
textLine1={t('dashboard:check-docs.line1')}
|
||||||
textLine2={t("dashboard:check-docs.line2")}
|
textLine2={t('dashboard:check-docs.line2')}
|
||||||
setCheckDocsPopUpVisible={setCheckDocsPopUpVisible}
|
setCheckDocsPopUpVisible={setCheckDocsPopUpVisible}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-row justify-between items-center mx-6 mt-6 mb-3 text-xl max-w-5xl">
|
<div className='flex flex-row justify-between items-center mx-6 mt-6 mb-3 text-xl max-w-5xl'>
|
||||||
{snapshotData &&
|
{snapshotData && (
|
||||||
<div className={`flex justify-start max-w-sm mt-1 mr-2`}>
|
<div className={`flex justify-start max-w-sm mt-1 mr-2`}>
|
||||||
<Button
|
<Button
|
||||||
text={String(t("Go back to current"))}
|
text={String(t('Go back to current'))}
|
||||||
onButtonPressed={() => setSnapshotData(undefined)}
|
onButtonPressed={() => setSnapshotData(undefined)}
|
||||||
color="mineshaft"
|
color='mineshaft'
|
||||||
size="md"
|
size='md'
|
||||||
icon={faArrowLeft}
|
icon={faArrowLeft}
|
||||||
/>
|
/>
|
||||||
</div>}
|
</div>
|
||||||
<div className="flex flex-row justify-start items-center text-3xl">
|
)}
|
||||||
<div className="font-semibold mr-4 mt-1 flex flex-row items-center">
|
<div className='flex flex-row justify-start items-center text-3xl'>
|
||||||
<p>{snapshotData ? "Secret Snapshot" : t("dashboard:title")}</p>
|
<div className='font-semibold mr-4 mt-1 flex flex-row items-center'>
|
||||||
{snapshotData && <span className='bg-primary-800 text-sm ml-4 mt-1 px-1.5 rounded-md'>{new Date(snapshotData.createdAt).toLocaleString()}</span>}
|
<p>{snapshotData ? 'Secret Snapshot' : t('dashboard:title')}</p>
|
||||||
|
{snapshotData && (
|
||||||
|
<span className='bg-primary-800 text-sm ml-4 mt-1 px-1.5 rounded-md'>
|
||||||
|
{new Date(snapshotData.createdAt).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!snapshotData && data?.length == 0 && (
|
{!snapshotData && data?.length == 0 && (
|
||||||
<ListBox
|
<ListBox
|
||||||
selected={env}
|
selected={selectedEnv.name}
|
||||||
data={['Development', 'Staging', 'Production', 'Testing']}
|
data={workspaceEnvs.map(({ name }) => name)}
|
||||||
onChange={setEnv}
|
onChange={(envName) =>
|
||||||
|
setSelectedEnv(
|
||||||
|
workspaceEnvs.find(({ name }) => envName === name) || {
|
||||||
|
name: 'unknown',
|
||||||
|
slug: 'unknown',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row">
|
<div className='flex flex-row'>
|
||||||
<div className={`flex justify-start max-w-sm mt-1 mr-2`}>
|
<div className={`flex justify-start max-w-sm mt-1 mr-2`}>
|
||||||
<Button
|
<Button
|
||||||
text={String(numSnapshots + " " + t("Commits"))}
|
text={String(numSnapshots + ' ' + t('Commits'))}
|
||||||
onButtonPressed={() => togglePITSidebar(true)}
|
onButtonPressed={() => togglePITSidebar(true)}
|
||||||
color="mineshaft"
|
color='mineshaft'
|
||||||
size="md"
|
size='md'
|
||||||
icon={faClockRotateLeft}
|
icon={faClockRotateLeft}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{(data?.length !== 0 || buttonReady) && !snapshotData && (
|
{(data?.length !== 0 || buttonReady) && !snapshotData && (
|
||||||
<div className={`flex justify-start max-w-sm mt-1`}>
|
<div className={`flex justify-start max-w-sm mt-1`}>
|
||||||
<Button
|
<Button
|
||||||
text={String(t("common:save-changes"))}
|
text={String(t('common:save-changes'))}
|
||||||
onButtonPressed={savePush}
|
onButtonPressed={savePush}
|
||||||
color="primary"
|
color='primary'
|
||||||
size="md"
|
size='md'
|
||||||
active={buttonReady}
|
active={buttonReady}
|
||||||
iconDisabled={faCheck}
|
iconDisabled={faCheck}
|
||||||
textDisabled={String(t("common:saved"))}
|
textDisabled={String(t("common:saved"))}
|
||||||
@@ -551,7 +584,7 @@ export default function Dashboard() {
|
|||||||
onButtonPressed={async () => {
|
onButtonPressed={async () => {
|
||||||
// Update secrets in the state only for the current environment
|
// Update secrets in the state only for the current environment
|
||||||
const rolledBackSecrets = snapshotData.secretVersions
|
const rolledBackSecrets = snapshotData.secretVersions
|
||||||
.filter(row => reverseEnvMapping[row.environment] == env)
|
.filter(row => row.environment == selectedEnv.slug)
|
||||||
.map((sv, position) => {
|
.map((sv, position) => {
|
||||||
return {
|
return {
|
||||||
id: sv.id, idOverride: sv.id, pos: position, valueOverride: sv.valueOverride, key: sv.key, value: sv.value, comment: ''
|
id: sv.id, idOverride: sv.id, pos: position, valueOverride: sv.valueOverride, key: sv.key, value: sv.value, comment: ''
|
||||||
@@ -575,88 +608,116 @@ export default function Dashboard() {
|
|||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-6 w-full pr-12">
|
<div className='mx-6 w-full pr-12'>
|
||||||
<div className="flex flex-col max-w-5xl pb-1">
|
<div className='flex flex-col max-w-5xl pb-1'>
|
||||||
<div className="w-full flex flex-row items-start">
|
<div className='w-full flex flex-row items-start'>
|
||||||
{(snapshotData || data?.length !== 0) && (
|
{(snapshotData || data?.length !== 0) && (
|
||||||
<>
|
<>
|
||||||
{!snapshotData
|
{!snapshotData ? (
|
||||||
? <ListBox
|
<ListBox
|
||||||
selected={env}
|
selected={selectedEnv.name}
|
||||||
data={['Development', 'Staging', 'Production', 'Testing']}
|
data={workspaceEnvs.map(({ name }) => name)}
|
||||||
onChange={setEnv}
|
onChange={(envName) =>
|
||||||
/>
|
setSelectedEnv(
|
||||||
: <ListBox
|
workspaceEnvs.find(
|
||||||
selected={snapshotEnv}
|
({ name }) => envName === name
|
||||||
data={['Development', 'Staging', 'Production', 'Testing']}
|
) || {
|
||||||
onChange={setSnapshotEnv}
|
name: 'unknown',
|
||||||
/>}
|
slug: 'unknown',
|
||||||
<div className="h-10 w-full bg-white/5 hover:bg-white/10 ml-2 flex items-center rounded-md flex flex-row items-center">
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ListBox
|
||||||
|
selected={selectedSnapshotEnv?.name || ''}
|
||||||
|
data={workspaceEnvs.map(({ name }) => name)}
|
||||||
|
onChange={(envName) =>
|
||||||
|
setSelectedSnapshotEnv(
|
||||||
|
workspaceEnvs.find(
|
||||||
|
({ name }) => envName === name
|
||||||
|
) || {
|
||||||
|
name: 'unknown',
|
||||||
|
slug: 'unknown',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className='h-10 w-full bg-white/5 hover:bg-white/10 ml-2 flex items-center rounded-md flex flex-row items-center'>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className="bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400"
|
className='bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400'
|
||||||
icon={faMagnifyingGlass}
|
icon={faMagnifyingGlass}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
className="pl-2 text-gray-400 rounded-r-md bg-white/5 w-full h-full outline-none"
|
className='pl-2 text-gray-400 rounded-r-md bg-white/5 w-full h-full outline-none'
|
||||||
value={searchKeys}
|
value={searchKeys}
|
||||||
onChange={(e) => setSearchKeys(e.target.value)}
|
onChange={(e) => setSearchKeys(e.target.value)}
|
||||||
placeholder={String(t("dashboard:search-keys"))}
|
placeholder={String(t('dashboard:search-keys'))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!snapshotData && <div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
{!snapshotData && (
|
||||||
<Button
|
<div className='ml-2 min-w-max flex flex-row items-start justify-start'>
|
||||||
onButtonPressed={() => reorderRows(1)}
|
<Button
|
||||||
color="mineshaft"
|
onButtonPressed={() => reorderRows(1)}
|
||||||
size="icon-md"
|
color='mineshaft'
|
||||||
icon={
|
size='icon-md'
|
||||||
sortMethod == 'alphabetical'
|
icon={
|
||||||
? faArrowDownAZ
|
sortMethod == 'alphabetical'
|
||||||
: faArrowDownZA
|
? faArrowDownAZ
|
||||||
}
|
: faArrowDownZA
|
||||||
/>
|
}
|
||||||
</div>}
|
/>
|
||||||
{!snapshotData && <div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
</div>
|
||||||
<DownloadSecretMenu data={data} env={env} />
|
)}
|
||||||
</div>}
|
{!snapshotData && (
|
||||||
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
<div className='ml-2 min-w-max flex flex-row items-start justify-start'>
|
||||||
|
<DownloadSecretMenu
|
||||||
|
data={data}
|
||||||
|
env={selectedEnv.slug}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='ml-2 min-w-max flex flex-row items-start justify-start'>
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={changeBlurred}
|
onButtonPressed={changeBlurred}
|
||||||
color="mineshaft"
|
color='mineshaft'
|
||||||
size="icon-md"
|
size='icon-md'
|
||||||
icon={blurred ? faEye : faEyeSlash}
|
icon={blurred ? faEye : faEyeSlash}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!snapshotData && <div className="relative ml-2 min-w-max flex flex-row items-start justify-end">
|
{!snapshotData && (
|
||||||
<Button
|
<div className='relative ml-2 min-w-max flex flex-row items-start justify-end'>
|
||||||
text={String(t("dashboard:add-key"))}
|
<Button
|
||||||
onButtonPressed={addRow}
|
text={String(t('dashboard:add-key'))}
|
||||||
color="mineshaft"
|
onButtonPressed={addRow}
|
||||||
icon={faPlus}
|
color='mineshaft'
|
||||||
size="md"
|
icon={faPlus}
|
||||||
/>
|
size='md'
|
||||||
{isNew && (
|
/>
|
||||||
<span className="absolute right-0 flex h-3 w-3 items-center justify-center ml-4 mb-4">
|
{isNew && (
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary/50 opacity-75 h-4 w-4"></span>
|
<span className='absolute right-0 flex h-3 w-3 items-center justify-center ml-4 mb-4'>
|
||||||
<span className="relative inline-flex rounded-full h-3 w-3 bg-primary"></span>
|
<span className='animate-ping absolute inline-flex h-full w-full rounded-full bg-primary/50 opacity-75 h-4 w-4'></span>
|
||||||
</span>
|
<span className='relative inline-flex rounded-full h-3 w-3 bg-primary'></span>
|
||||||
)}
|
</span>
|
||||||
</div>}
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center h-full my-48">
|
<div className='flex items-center justify-center h-full my-48'>
|
||||||
<Image
|
<Image
|
||||||
src="/images/loading/loading.gif"
|
src='/images/loading/loading.gif'
|
||||||
height={60}
|
height={60}
|
||||||
width={100}
|
width={100}
|
||||||
alt="infisical loading indicator"
|
alt='infisical loading indicator'
|
||||||
></Image>
|
></Image>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : data?.length !== 0 ? (
|
||||||
data?.length !== 0 ? (
|
<div className='flex flex-col w-full mt-1 mb-2'>
|
||||||
<div className="flex flex-col w-full mt-1 mb-2">
|
|
||||||
<div
|
<div
|
||||||
className={`max-w-5xl mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
className={`max-w-5xl mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||||
>
|
>
|
||||||
@@ -679,7 +740,7 @@ export default function Dashboard() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{snapshotData && snapshotData.secretVersions?.sort((a, b) => a.key.localeCompare(b.key))
|
{snapshotData && snapshotData.secretVersions?.sort((a, b) => a.key.localeCompare(b.key))
|
||||||
.filter(row => reverseEnvMapping[row.environment] == snapshotEnv)
|
.filter(row => row.environment == selectedSnapshotEnv?.slug)
|
||||||
.filter(row => row.key.toUpperCase().includes(searchKeys.toUpperCase()))
|
.filter(row => row.key.toUpperCase().includes(searchKeys.toUpperCase()))
|
||||||
.filter(
|
.filter(
|
||||||
row => !(snapshotData.secretVersions?.filter(row => (snapshotData.secretVersions
|
row => !(snapshotData.secretVersions?.filter(row => (snapshotData.secretVersions
|
||||||
@@ -707,21 +768,23 @@ export default function Dashboard() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{!snapshotData && <div className="w-full max-w-5xl px-2 pt-3">
|
{!snapshotData && (
|
||||||
<DropZone
|
<div className='w-full max-w-5xl px-2 pt-3'>
|
||||||
setData={addData}
|
<DropZone
|
||||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
setData={addData}
|
||||||
createNewFile={addRow}
|
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||||
errorDragAndDrop={errorDragAndDrop}
|
createNewFile={addRow}
|
||||||
setButtonReady={setButtonReady}
|
errorDragAndDrop={errorDragAndDrop}
|
||||||
keysExist={true}
|
setButtonReady={setButtonReady}
|
||||||
numCurrentRows={data.length}
|
keysExist={true}
|
||||||
/>
|
numCurrentRows={data.length}
|
||||||
</div>}
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center h-full text-xl text-gray-400 max-w-5xl mt-28">
|
<div className='flex flex-col items-center justify-center h-full text-xl text-gray-400 max-w-5xl mt-28'>
|
||||||
{isKeyAvailable && !snapshotData && (
|
{isKeyAvailable && !snapshotData && (
|
||||||
<DropZone
|
<DropZone
|
||||||
setData={setData}
|
setData={setData}
|
||||||
@@ -733,36 +796,35 @@ export default function Dashboard() {
|
|||||||
keysExist={false}
|
keysExist={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{
|
{!isKeyAvailable && (
|
||||||
(!isKeyAvailable && (
|
<>
|
||||||
<>
|
<FontAwesomeIcon
|
||||||
<FontAwesomeIcon
|
className='text-7xl mt-20 mb-8'
|
||||||
className="text-7xl mt-20 mb-8"
|
icon={faFolderOpen}
|
||||||
icon={faFolderOpen}
|
/>
|
||||||
/>
|
<p>
|
||||||
<p>
|
To view this file, contact your administrator for
|
||||||
To view this file, contact your administrator for
|
permission.
|
||||||
permission.
|
</p>
|
||||||
</p>
|
<p className='mt-1'>
|
||||||
<p className="mt-1">
|
They need to grant you access in the team tab.
|
||||||
They need to grant you access in the team tab.
|
</p>
|
||||||
</p>
|
</>
|
||||||
</>
|
)}
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative z-10 w-10/12 mr-auto h-full ml-2 bg-bunker-800 flex flex-col items-center justify-center">
|
<div className='relative z-10 w-10/12 mr-auto h-full ml-2 bg-bunker-800 flex flex-col items-center justify-center'>
|
||||||
<div className="absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full"></div>
|
<div className='absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full'></div>
|
||||||
<Image
|
<Image
|
||||||
src="/images/loading/loading.gif"
|
src='/images/loading/loading.gif'
|
||||||
height={70}
|
height={70}
|
||||||
width={120}
|
width={120}
|
||||||
alt="loading animation"
|
alt='loading animation'
|
||||||
></Image>
|
></Image>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -770,4 +832,4 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
Dashboard.requireAuth = true;
|
Dashboard.requireAuth = true;
|
||||||
|
|
||||||
export const getServerSideProps = getTranslatedServerSideProps(["dashboard"]);
|
export const getServerSideProps = getTranslatedServerSideProps(['dashboard']);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import setBotActiveStatus from "../api/bot/setBotActiveStatus";
|
|||||||
import getIntegrationOptions from "../api/integrations/GetIntegrationOptions";
|
import getIntegrationOptions from "../api/integrations/GetIntegrationOptions";
|
||||||
import getWorkspaceAuthorizations from "../api/integrations/getWorkspaceAuthorizations";
|
import getWorkspaceAuthorizations from "../api/integrations/getWorkspaceAuthorizations";
|
||||||
import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations";
|
import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations";
|
||||||
|
import getAWorkspace from "../api/workspace/getAWorkspace";
|
||||||
import getLatestFileKey from "../api/workspace/getLatestFileKey";
|
import getLatestFileKey from "../api/workspace/getLatestFileKey";
|
||||||
const {
|
const {
|
||||||
decryptAssymmetric,
|
decryptAssymmetric,
|
||||||
@@ -34,6 +35,7 @@ const crypto = require("crypto");
|
|||||||
export default function Integrations() {
|
export default function Integrations() {
|
||||||
const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]);
|
const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]);
|
||||||
const [integrationAuths, setIntegrationAuths] = useState([]);
|
const [integrationAuths, setIntegrationAuths] = useState([]);
|
||||||
|
const [environments,setEnvironments] = useState([])
|
||||||
const [integrations, setIntegrations] = useState([]);
|
const [integrations, setIntegrations] = useState([]);
|
||||||
const [bot, setBot] = useState(null);
|
const [bot, setBot] = useState(null);
|
||||||
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
|
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
|
||||||
@@ -41,11 +43,15 @@ export default function Integrations() {
|
|||||||
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState(null);
|
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const workspaceId = router.query.id;
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(async () => {
|
useEffect(async () => {
|
||||||
try {
|
try {
|
||||||
|
const workspace = await getAWorkspace(workspaceId);
|
||||||
|
setEnvironments(workspace.environments);
|
||||||
|
|
||||||
// get cloud integration options
|
// get cloud integration options
|
||||||
setCloudIntegrationOptions(
|
setCloudIntegrationOptions(
|
||||||
await getIntegrationOptions()
|
await getIntegrationOptions()
|
||||||
@@ -54,23 +60,19 @@ export default function Integrations() {
|
|||||||
// get project integration authorizations
|
// get project integration authorizations
|
||||||
setIntegrationAuths(
|
setIntegrationAuths(
|
||||||
await getWorkspaceAuthorizations({
|
await getWorkspaceAuthorizations({
|
||||||
workspaceId: router.query.id,
|
workspaceId
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// get project integrations
|
// get project integrations
|
||||||
setIntegrations(
|
setIntegrations(
|
||||||
await getWorkspaceIntegrations({
|
await getWorkspaceIntegrations({
|
||||||
workspaceId: router.query.id,
|
workspaceId,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// get project bot
|
// get project bot
|
||||||
setBot(
|
setBot(await getBot({ workspaceId }));
|
||||||
await getBot({
|
|
||||||
workspaceId: router.query.id
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
@@ -90,7 +92,7 @@ export default function Integrations() {
|
|||||||
|
|
||||||
if (bot) {
|
if (bot) {
|
||||||
// case: there is a bot
|
// case: there is a bot
|
||||||
const key = await getLatestFileKey({ workspaceId: router.query.id });
|
const key = await getLatestFileKey({ workspaceId });
|
||||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||||
|
|
||||||
const WORKSPACE_KEY = decryptAssymmetric({
|
const WORKSPACE_KEY = decryptAssymmetric({
|
||||||
@@ -214,7 +216,7 @@ export default function Integrations() {
|
|||||||
handleBotActivate={handleBotActivate}
|
handleBotActivate={handleBotActivate}
|
||||||
handleIntegrationOption={handleIntegrationOption}
|
handleIntegrationOption={handleIntegrationOption}
|
||||||
/> */}
|
/> */}
|
||||||
<IntegrationSection integrations={integrations} />
|
<IntegrationSection integrations={integrations} environments={environments} />
|
||||||
{(cloudIntegrationOptions.length > 0 && bot) ? (
|
{(cloudIntegrationOptions.length > 0 && bot) ? (
|
||||||
<CloudIntegrationSection
|
<CloudIntegrationSection
|
||||||
cloudIntegrationOptions={cloudIntegrationOptions}
|
cloudIntegrationOptions={cloudIntegrationOptions}
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ export default function Login() {
|
|||||||
<ListBox
|
<ListBox
|
||||||
selected={lang}
|
selected={lang}
|
||||||
onChange={setLanguage}
|
onChange={setLanguage}
|
||||||
data={["en", "ko", "fr"]}
|
data={["en", "ko", "fr", "pt-BR"]}
|
||||||
isFull
|
isFull
|
||||||
text={`${t("common:language")}: `}
|
text={`${t("common:language")}: `}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,306 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import Head from "next/head";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useTranslation } from "next-i18next";
|
|
||||||
import { faCheck, faCopy, faPlus } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
|
|
||||||
import Button from "~/components/basic/buttons/Button";
|
|
||||||
import AddServiceTokenDialog from "~/components/basic/dialog/AddServiceTokenDialog";
|
|
||||||
import InputField from "~/components/basic/InputField";
|
|
||||||
import ServiceTokenTable from "~/components/basic/table/ServiceTokenTable.tsx";
|
|
||||||
import NavHeader from "~/components/navigation/NavHeader";
|
|
||||||
import { getTranslatedServerSideProps } from "~/utilities/withTranslateProps";
|
|
||||||
|
|
||||||
import getServiceTokens from "../../api/serviceToken/getServiceTokens";
|
|
||||||
import deleteWorkspace from "../../api/workspace/deleteWorkspace";
|
|
||||||
import getWorkspaces from "../../api/workspace/getWorkspaces";
|
|
||||||
import renameWorkspace from "../../api/workspace/renameWorkspace";
|
|
||||||
|
|
||||||
|
|
||||||
export default function SettingsBasic() {
|
|
||||||
const [buttonReady, setButtonReady] = useState(false);
|
|
||||||
const router = useRouter();
|
|
||||||
const [workspaceName, setWorkspaceName] = useState("");
|
|
||||||
const [serviceTokens, setServiceTokens] = useState([]);
|
|
||||||
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState("");
|
|
||||||
const [workspaceId, setWorkspaceId] = useState("");
|
|
||||||
const [isAddOpen, setIsAddOpen] = useState(false);
|
|
||||||
let [isAddServiceTokenDialogOpen, setIsAddServiceTokenDialogOpen] =
|
|
||||||
useState(false);
|
|
||||||
const [projectIdCopied, setProjectIdCopied] = useState(false);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function copies the project id to the clipboard
|
|
||||||
*/
|
|
||||||
function copyToClipboard() {
|
|
||||||
// const copyText = document.getElementById('myInput') as HTMLInputElement;
|
|
||||||
const copyText = document.getElementById('myInput')
|
|
||||||
|
|
||||||
if (copyText) {
|
|
||||||
copyText.select();
|
|
||||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
|
||||||
|
|
||||||
navigator.clipboard.writeText(copyText.value);
|
|
||||||
|
|
||||||
setProjectIdCopied(true);
|
|
||||||
setTimeout(() => setProjectIdCopied(false), 2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(async () => {
|
|
||||||
let userWorkspaces = await getWorkspaces();
|
|
||||||
userWorkspaces.map((userWorkspace) => {
|
|
||||||
if (userWorkspace._id == router.query.id) {
|
|
||||||
setWorkspaceName(userWorkspace.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let tempServiceTokens = await getServiceTokens({
|
|
||||||
workspaceId: router.query.id,
|
|
||||||
});
|
|
||||||
setServiceTokens(tempServiceTokens);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const modifyWorkspaceName = (newName) => {
|
|
||||||
setButtonReady(true);
|
|
||||||
setWorkspaceName(newName);
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitChanges = (newWorkspaceName) => {
|
|
||||||
renameWorkspace(router.query.id, newWorkspaceName);
|
|
||||||
setButtonReady(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(async () => {
|
|
||||||
setWorkspaceId(router.query.id);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
function closeAddModal() {
|
|
||||||
setIsAddOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openAddModal() {
|
|
||||||
setIsAddOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeAddServiceTokenModal = () => {
|
|
||||||
setIsAddServiceTokenDialogOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function deleted a workspace.
|
|
||||||
* It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
|
|
||||||
* It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
|
|
||||||
* It then deletes the workspace and forwards the user to another aviable workspace.
|
|
||||||
*/
|
|
||||||
const executeDeletingWorkspace = async () => {
|
|
||||||
let userWorkspaces = await getWorkspaces();
|
|
||||||
|
|
||||||
if (userWorkspaces.length > 1) {
|
|
||||||
if (
|
|
||||||
userWorkspaces.filter(
|
|
||||||
(workspace) => workspace._id == router.query.id
|
|
||||||
)[0].name == workspaceToBeDeletedName
|
|
||||||
) {
|
|
||||||
await deleteWorkspace(router.query.id);
|
|
||||||
let userWorkspaces = await getWorkspaces();
|
|
||||||
router.push("/dashboard/" + userWorkspaces[0]._id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
|
||||||
<Head>
|
|
||||||
<title>
|
|
||||||
{t("common:head-title", { title: t("settings-project:title") })}
|
|
||||||
</title>
|
|
||||||
<link rel="icon" href="/infisical.ico" />
|
|
||||||
</Head>
|
|
||||||
<AddServiceTokenDialog
|
|
||||||
isOpen={isAddServiceTokenDialogOpen}
|
|
||||||
workspaceId={router.query.id}
|
|
||||||
closeModal={closeAddServiceTokenModal}
|
|
||||||
workspaceName={workspaceName}
|
|
||||||
serviceTokens={serviceTokens}
|
|
||||||
setServiceTokens={setServiceTokens}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-row mr-6 max-w-5xl">
|
|
||||||
<div className="w-full max-h-screen pb-2 overflow-y-auto">
|
|
||||||
<NavHeader
|
|
||||||
pageName={t("settings-project:title")}
|
|
||||||
isProjectRelated={true}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-row justify-between items-center ml-6 my-8 text-xl max-w-5xl">
|
|
||||||
<div className="flex flex-col justify-start items-start text-3xl">
|
|
||||||
<p className="font-semibold mr-4 text-gray-200">
|
|
||||||
{t("settings-project:title")}
|
|
||||||
</p>
|
|
||||||
<p className="font-normal mr-4 text-gray-400 text-base">
|
|
||||||
{t("settings-project:description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col ml-6 text-mineshaft-50">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="min-w-md mt-2 flex flex-col items-start">
|
|
||||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-4 flex flex-col items-start flex flex-col items-start w-full mb-6 pt-2">
|
|
||||||
<p className="text-xl font-semibold mb-4 mt-2">
|
|
||||||
{t("common:display-name")}
|
|
||||||
</p>
|
|
||||||
<div className="max-h-28 w-full max-w-md mr-auto">
|
|
||||||
<InputField
|
|
||||||
onChangeHandler={modifyWorkspaceName}
|
|
||||||
type="varName"
|
|
||||||
value={workspaceName}
|
|
||||||
placeholder=""
|
|
||||||
isRequired
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-start w-full">
|
|
||||||
<div className={`flex justify-start max-w-sm mt-4 mb-2`}>
|
|
||||||
<Button
|
|
||||||
text={t("common:save-changes")}
|
|
||||||
onButtonPressed={() => submitChanges(workspaceName)}
|
|
||||||
color="mineshaft"
|
|
||||||
size="md"
|
|
||||||
active={buttonReady}
|
|
||||||
iconDisabled={faCheck}
|
|
||||||
textDisabled="Saved"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white/5 rounded-md px-6 pt-4 pb-2 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4">
|
|
||||||
<p className="text-xl font-semibold self-start">
|
|
||||||
{t("common:project-id")}
|
|
||||||
</p>
|
|
||||||
<p className="text-base text-gray-400 font-normal self-start mt-4">
|
|
||||||
{t("settings-project:project-id-description")}
|
|
||||||
</p>
|
|
||||||
<p className="text-base text-gray-400 font-normal self-start">
|
|
||||||
{t("settings-project:project-id-description2")}
|
|
||||||
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
|
||||||
<a
|
|
||||||
href="https://infisical.com/docs/getting-started/introduction"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
className="text-primary hover:opacity-80 duration-200"
|
|
||||||
>
|
|
||||||
{t("settings-project:docs")}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p className="mt-4 text-xs text-bunker-300">{t("settings-project:auto-generated")}</p>
|
|
||||||
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mb-3 mr-2 rounded-md text-gray-400">
|
|
||||||
<p className="mr-2 font-bold pl-4">{`${t(
|
|
||||||
"common:project-id"
|
|
||||||
)}:`}</p>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={workspaceId}
|
|
||||||
id="myInput"
|
|
||||||
className="bg-white/0 text-gray-400 py-2 w-60 px-2 min-w-md outline-none"
|
|
||||||
disabled
|
|
||||||
></input>
|
|
||||||
<div className="group font-normal group relative inline-block text-gray-400 underline hover:text-primary duration-200">
|
|
||||||
<button
|
|
||||||
onClick={copyToClipboard}
|
|
||||||
className="pl-4 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200"
|
|
||||||
>
|
|
||||||
{projectIdCopied ? (
|
|
||||||
<FontAwesomeIcon icon={faCheck} className="pr-0.5" />
|
|
||||||
) : (
|
|
||||||
<FontAwesomeIcon icon={faCopy} />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full pl-3 py-2 bg-bunker-800 rounded-md text-center text-gray-400 text-sm">
|
|
||||||
{t("common:click-to-copy")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white/5 rounded-md px-6 pt-4 flex flex-col items-start flex flex-col items-start w-full mt-4 mb-4 pt-2">
|
|
||||||
<div className="flex flex-row justify-between w-full">
|
|
||||||
<div className="flex flex-col w-full">
|
|
||||||
<p className="text-xl font-semibold mb-3">
|
|
||||||
{t("section-token:service-tokens")}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-400">
|
|
||||||
{t("section-token:service-tokens-description")}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-400 mb-4">
|
|
||||||
Please, make sure you are on the
|
|
||||||
<a
|
|
||||||
className="text-primary underline underline-offset-2 ml-1"
|
|
||||||
href="https://infisical.com/docs/cli/overview"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
latest version of CLI
|
|
||||||
</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="w-48 mt-2">
|
|
||||||
<Button
|
|
||||||
text={t("section-token:add-new")}
|
|
||||||
onButtonPressed={() => {
|
|
||||||
setIsAddServiceTokenDialogOpen(true);
|
|
||||||
}}
|
|
||||||
color="mineshaft"
|
|
||||||
icon={faPlus}
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ServiceTokenTable
|
|
||||||
data={serviceTokens}
|
|
||||||
workspaceName={workspaceName}
|
|
||||||
setServiceTokens={setServiceTokens}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white/5 rounded-md px-6 pt-4 pb-6 border-l border-red pl-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pb-4 pt-2">
|
|
||||||
<p className="text-xl font-bold text-red">
|
|
||||||
{t("settings-project:danger-zone")}
|
|
||||||
</p>
|
|
||||||
<p className="mt-2 text-md text-gray-400">
|
|
||||||
{t("settings-project:danger-zone-note")}
|
|
||||||
</p>
|
|
||||||
<div className="max-h-28 w-full max-w-md mr-auto mt-4">
|
|
||||||
<InputField
|
|
||||||
label={t("settings-project:project-to-delete")}
|
|
||||||
onChangeHandler={setWorkspaceToBeDeletedName}
|
|
||||||
type="varName"
|
|
||||||
value={workspaceToBeDeletedName}
|
|
||||||
placeholder=""
|
|
||||||
isRequired
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="max-w-md mt-6 w-full inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2.5 text-sm font-medium text-gray-400 hover:bg-red hover:text-white hover:font-semibold hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
|
||||||
onClick={executeDeletingWorkspace}
|
|
||||||
>
|
|
||||||
{t("settings-project:delete-project")}
|
|
||||||
</button>
|
|
||||||
<p className="mt-0.5 ml-1 text-xs text-gray-500">
|
|
||||||
{t("settings-project:delete-project-note")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsBasic.requireAuth = true;
|
|
||||||
|
|
||||||
export const getServerSideProps = getTranslatedServerSideProps([
|
|
||||||
"settings",
|
|
||||||
"settings-project",
|
|
||||||
"section-token",
|
|
||||||
]);
|
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { faCheck, faCopy, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
|
import Button from '~/components/basic/buttons/Button';
|
||||||
|
import AddServiceTokenDialog from '~/components/basic/dialog/AddServiceTokenDialog';
|
||||||
|
import InputField from '~/components/basic/InputField';
|
||||||
|
import EnvironmentTable from '~/components/basic/table/EnvironmentsTable';
|
||||||
|
import ServiceTokenTable from '~/components/basic/table/ServiceTokenTable';
|
||||||
|
import NavHeader from '~/components/navigation/NavHeader';
|
||||||
|
import deleteEnvironment from '~/pages/api/environments/deleteEnvironment';
|
||||||
|
import updateEnvironment from '~/pages/api/environments/updateEnvironment';
|
||||||
|
import { getTranslatedServerSideProps } from '~/utilities/withTranslateProps';
|
||||||
|
|
||||||
|
import createEnvironment from '../../api/environments/createEnvironment';
|
||||||
|
import getServiceTokens from '../../api/serviceToken/getServiceTokens';
|
||||||
|
import deleteWorkspace from '../../api/workspace/deleteWorkspace';
|
||||||
|
import getWorkspaces from '../../api/workspace/getWorkspaces';
|
||||||
|
import renameWorkspace from '../../api/workspace/renameWorkspace';
|
||||||
|
|
||||||
|
type EnvData = {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SettingsBasic() {
|
||||||
|
const [buttonReady, setButtonReady] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
const [workspaceName, setWorkspaceName] = useState('');
|
||||||
|
const [serviceTokens, setServiceTokens] = useState([]);
|
||||||
|
const [environments, setEnvironments] = useState<Array<EnvData>>([]);
|
||||||
|
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState('');
|
||||||
|
const [isAddOpen, setIsAddOpen] = useState(false);
|
||||||
|
const [isAddServiceTokenDialogOpen, setIsAddServiceTokenDialogOpen] =
|
||||||
|
useState(false);
|
||||||
|
const [projectIdCopied, setProjectIdCopied] = useState(false);
|
||||||
|
const workspaceId = router.query.id as string;
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function copies the project id to the clipboard
|
||||||
|
*/
|
||||||
|
function copyToClipboard() {
|
||||||
|
const copyText = document.getElementById('myInput') as HTMLInputElement;
|
||||||
|
|
||||||
|
if (copyText) {
|
||||||
|
copyText.select();
|
||||||
|
copyText.setSelectionRange(0, 99999); // For mobile devices
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(copyText.value);
|
||||||
|
|
||||||
|
setProjectIdCopied(true);
|
||||||
|
setTimeout(() => setProjectIdCopied(false), 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const load = async () => {
|
||||||
|
const userWorkspaces = await getWorkspaces();
|
||||||
|
userWorkspaces.forEach((userWorkspace) => {
|
||||||
|
if (userWorkspace._id == workspaceId) {
|
||||||
|
setWorkspaceName(userWorkspace.name);
|
||||||
|
setEnvironments(userWorkspace.environments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const tempServiceTokens = await getServiceTokens({
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
setServiceTokens(tempServiceTokens);
|
||||||
|
};
|
||||||
|
|
||||||
|
load();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const modifyWorkspaceName = (newName: string) => {
|
||||||
|
setButtonReady(true);
|
||||||
|
setWorkspaceName(newName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitChanges = (newWorkspaceName: string) => {
|
||||||
|
renameWorkspace(workspaceId, newWorkspaceName);
|
||||||
|
setButtonReady(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeAddServiceTokenModal = () => {
|
||||||
|
setIsAddServiceTokenDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function deleted a workspace.
|
||||||
|
* It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
|
||||||
|
* It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
|
||||||
|
* It then deletes the workspace and forwards the user to another aviable workspace.
|
||||||
|
*/
|
||||||
|
const executeDeletingWorkspace = async () => {
|
||||||
|
const userWorkspaces = await getWorkspaces();
|
||||||
|
|
||||||
|
if (userWorkspaces.length > 1) {
|
||||||
|
if (
|
||||||
|
userWorkspaces.filter(
|
||||||
|
(workspace) => workspace._id === workspaceId
|
||||||
|
)[0].name == workspaceToBeDeletedName
|
||||||
|
) {
|
||||||
|
await deleteWorkspace(workspaceId);
|
||||||
|
const userWorkspaces = await getWorkspaces();
|
||||||
|
router.push('/dashboard/' + userWorkspaces[0]._id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreateEnvironment = async ({ name, slug }: EnvData) => {
|
||||||
|
const res = await createEnvironment(workspaceId, {
|
||||||
|
environmentName: name,
|
||||||
|
environmentSlug: slug,
|
||||||
|
});
|
||||||
|
if (res) {
|
||||||
|
// TODO: on react-query migration do an api call to resync
|
||||||
|
setEnvironments((env) => [...env, { name, slug }]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateEnvironment = async (
|
||||||
|
oldSlug: string,
|
||||||
|
{ name, slug }: EnvData
|
||||||
|
) => {
|
||||||
|
const res = await updateEnvironment(workspaceId, {
|
||||||
|
oldEnvironmentSlug: oldSlug,
|
||||||
|
environmentName: name,
|
||||||
|
environmentSlug: slug,
|
||||||
|
});
|
||||||
|
// TODO: on react-query migration do an api call to resync
|
||||||
|
if (res) {
|
||||||
|
setEnvironments((env) =>
|
||||||
|
env.map((el) => (el.slug === oldSlug ? { name, slug } : el))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteEnvironment = async (slugToBeDelete: string) => {
|
||||||
|
const res = await deleteEnvironment(workspaceId, slugToBeDelete);
|
||||||
|
// TODO: on react-query migration do an api call to resync
|
||||||
|
if (res) {
|
||||||
|
setEnvironments((env) =>
|
||||||
|
env.filter(({ slug }) => slug !== slugToBeDelete)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='bg-bunker-800 max-h-screen flex flex-col justify-between text-white'>
|
||||||
|
<Head>
|
||||||
|
<title>
|
||||||
|
{t('common:head-title', { title: t('settings-project:title') })}
|
||||||
|
</title>
|
||||||
|
<link rel='icon' href='/infisical.ico' />
|
||||||
|
</Head>
|
||||||
|
<AddServiceTokenDialog
|
||||||
|
isOpen={isAddServiceTokenDialogOpen}
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
environments={environments}
|
||||||
|
closeModal={closeAddServiceTokenModal}
|
||||||
|
workspaceName={workspaceName}
|
||||||
|
serviceTokens={serviceTokens}
|
||||||
|
setServiceTokens={setServiceTokens}
|
||||||
|
/>
|
||||||
|
<div className='flex flex-row mr-6 max-w-5xl'>
|
||||||
|
<div className='w-full max-h-screen pb-2 overflow-y-auto'>
|
||||||
|
<NavHeader
|
||||||
|
pageName={t('settings-project:title')}
|
||||||
|
isProjectRelated={true}
|
||||||
|
/>
|
||||||
|
<div className='flex flex-row justify-between items-center ml-6 my-8 text-xl max-w-5xl'>
|
||||||
|
<div className='flex flex-col justify-start items-start text-3xl'>
|
||||||
|
<p className='font-semibold mr-4 text-gray-200'>
|
||||||
|
{t('settings-project:title')}
|
||||||
|
</p>
|
||||||
|
<p className='font-normal mr-4 text-gray-400 text-base'>
|
||||||
|
{t('settings-project:description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col ml-6 text-mineshaft-50'>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<div className='min-w-md mt-2 flex flex-col items-start'>
|
||||||
|
<div className='bg-white/5 rounded-md px-6 pt-6 pb-4 flex flex-col items-start flex flex-col items-start w-full mb-6 pt-2'>
|
||||||
|
<p className='text-xl font-semibold mb-4 mt-2'>
|
||||||
|
{t('common:display-name')}
|
||||||
|
</p>
|
||||||
|
<div className='max-h-28 w-full max-w-md mr-auto'>
|
||||||
|
<InputField
|
||||||
|
label=''
|
||||||
|
onChangeHandler={modifyWorkspaceName}
|
||||||
|
type='varName'
|
||||||
|
value={workspaceName}
|
||||||
|
placeholder=''
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-start w-full'>
|
||||||
|
<div className={`flex justify-start max-w-sm mt-4 mb-2`}>
|
||||||
|
<Button
|
||||||
|
text={t('common:save-changes') as string}
|
||||||
|
onButtonPressed={() => submitChanges(workspaceName)}
|
||||||
|
color='mineshaft'
|
||||||
|
size='md'
|
||||||
|
active={buttonReady}
|
||||||
|
iconDisabled={faCheck}
|
||||||
|
textDisabled='Saved'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='bg-white/5 rounded-md px-6 pt-4 pb-2 flex flex-col items-start w-full mb-6 mt-4'>
|
||||||
|
<p className='text-xl font-semibold self-start'>
|
||||||
|
{t('common:project-id')}
|
||||||
|
</p>
|
||||||
|
<p className='text-base text-gray-400 font-normal self-start mt-4'>
|
||||||
|
{t('settings-project:project-id-description')}
|
||||||
|
</p>
|
||||||
|
<p className='text-base text-gray-400 font-normal self-start'>
|
||||||
|
{t('settings-project:project-id-description2')}
|
||||||
|
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
||||||
|
<a
|
||||||
|
href='https://infisical.com/docs/getting-started/introduction'
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener'
|
||||||
|
className='text-primary hover:opacity-80 duration-200'
|
||||||
|
>
|
||||||
|
{t('settings-project:docs')}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p className='mt-4 text-xs text-bunker-300'>
|
||||||
|
{t('settings-project:auto-generated')}
|
||||||
|
</p>
|
||||||
|
<div className='flex justify-end items-center bg-white/[0.07] text-base mt-2 mb-3 mr-2 rounded-md text-gray-400'>
|
||||||
|
<p className='mr-2 font-bold pl-4'>{`${t(
|
||||||
|
'common:project-id'
|
||||||
|
)}:`}</p>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={workspaceId}
|
||||||
|
id='myInput'
|
||||||
|
className='bg-white/0 text-gray-400 py-2 w-60 px-2 min-w-md outline-none'
|
||||||
|
disabled
|
||||||
|
></input>
|
||||||
|
<div className='group font-normal group relative inline-block text-gray-400 underline hover:text-primary duration-200'>
|
||||||
|
<button
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
className='pl-4 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
|
||||||
|
>
|
||||||
|
{projectIdCopied ? (
|
||||||
|
<FontAwesomeIcon icon={faCheck} className='pr-0.5' />
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon icon={faCopy} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<span className='absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full pl-3 py-2 bg-bunker-800 rounded-md text-center text-gray-400 text-sm'>
|
||||||
|
{t('common:click-to-copy')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='bg-white/5 rounded-md px-6 pt-6 flex flex-col items-start w-full mt-4 mb-4'>
|
||||||
|
<EnvironmentTable
|
||||||
|
data={environments}
|
||||||
|
onCreateEnv={onCreateEnvironment}
|
||||||
|
onUpdateEnv={onUpdateEnvironment}
|
||||||
|
onDeleteEnv={onDeleteEnvironment}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='bg-white/5 rounded-md px-6 flex flex-col items-start w-full mt-4 mb-4 pt-2'>
|
||||||
|
<div className='flex flex-row justify-between w-full'>
|
||||||
|
<div className='flex flex-col w-full'>
|
||||||
|
<p className='text-xl font-semibold mb-3'>
|
||||||
|
{t('section-token:service-tokens')}
|
||||||
|
</p>
|
||||||
|
<p className='text-sm text-gray-400'>
|
||||||
|
{t('section-token:service-tokens-description')}
|
||||||
|
</p>
|
||||||
|
<p className='text-sm text-gray-400 mb-4'>
|
||||||
|
Please, make sure you are on the
|
||||||
|
<a
|
||||||
|
className='text-primary underline underline-offset-2 ml-1'
|
||||||
|
href='https://infisical.com/docs/cli/overview'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
latest version of CLI
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='w-48 mt-2'>
|
||||||
|
<Button
|
||||||
|
text={t('section-token:add-new') as string}
|
||||||
|
onButtonPressed={() => {
|
||||||
|
setIsAddServiceTokenDialogOpen(true);
|
||||||
|
}}
|
||||||
|
color='mineshaft'
|
||||||
|
icon={faPlus}
|
||||||
|
size='md'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ServiceTokenTable
|
||||||
|
data={serviceTokens}
|
||||||
|
workspaceName={workspaceName}
|
||||||
|
setServiceTokens={setServiceTokens as any}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='bg-white/5 rounded-md px-6 border-l border-red pl-6 flex flex-col items-start w-full mb-6 mt-4 pb-4 pt-2'>
|
||||||
|
<p className='text-xl font-bold text-red'>
|
||||||
|
{t('settings-project:danger-zone')}
|
||||||
|
</p>
|
||||||
|
<p className='mt-2 text-md text-gray-400'>
|
||||||
|
{t('settings-project:danger-zone-note')}
|
||||||
|
</p>
|
||||||
|
<div className='max-h-28 w-full max-w-md mr-auto mt-4'>
|
||||||
|
<InputField
|
||||||
|
label={t('settings-project:project-to-delete')}
|
||||||
|
onChangeHandler={setWorkspaceToBeDeletedName}
|
||||||
|
type='varName'
|
||||||
|
value={workspaceToBeDeletedName}
|
||||||
|
placeholder=''
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='max-w-md mt-6 w-full inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2.5 text-sm font-medium text-gray-400 hover:bg-red hover:text-white hover:font-semibold hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||||
|
onClick={executeDeletingWorkspace}
|
||||||
|
>
|
||||||
|
{t('settings-project:delete-project')}
|
||||||
|
</button>
|
||||||
|
<p className='mt-0.5 ml-1 text-xs text-gray-500'>
|
||||||
|
{t('settings-project:delete-project-note')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsBasic.requireAuth = true;
|
||||||
|
|
||||||
|
export const getServerSideProps = getTranslatedServerSideProps([
|
||||||
|
'settings',
|
||||||
|
'settings-project',
|
||||||
|
'section-token',
|
||||||
|
]);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"event": {
|
||||||
|
"readSecrets": "Segredos Visualizados",
|
||||||
|
"updateSecrets": "Segredos Atualizados",
|
||||||
|
"addSecrets": "Segredos Adicionados",
|
||||||
|
"deleteSecrets": "Segredos Excluídos"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"title": "Uso & Faturamento",
|
||||||
|
"description": "Visualize e gerencie a assinatura da sua organização aqui",
|
||||||
|
"subscription": "Inscrição",
|
||||||
|
"starter": {
|
||||||
|
"name": "Iniciante",
|
||||||
|
"price-explanation": "Até 5 membros da equipe",
|
||||||
|
"text": "Gerencie qualquer projeto com 5 membros gratuitamente!",
|
||||||
|
"subtext": "$5 por membro / mês depois."
|
||||||
|
},
|
||||||
|
"professional": {
|
||||||
|
"name": "Profissional",
|
||||||
|
"price-explanation": "/membro/mês",
|
||||||
|
"subtext": "Inclui projetos e membros ilimitados.",
|
||||||
|
"text": "Acompanhe o gerenciamento de chaves à medida que você cresce."
|
||||||
|
},
|
||||||
|
"enterprise": {
|
||||||
|
"name": "Empreendimento",
|
||||||
|
"text": "Acompanhe o gerenciamento de chaves à medida que você cresce."
|
||||||
|
},
|
||||||
|
"current-usage": "Uso atual",
|
||||||
|
"free": "Grátis",
|
||||||
|
"downgrade": "Reduzir",
|
||||||
|
"upgrade": "Melhoria",
|
||||||
|
"learn-more": "Saber Mais",
|
||||||
|
"custom-pricing": "Preço Personalizado",
|
||||||
|
"schedule-demo": "Agende uma Demonstração"
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"head-title": "{{title}} | Infiscal",
|
||||||
|
"error_project-already-exists": "Já exite um projeto com este nome.",
|
||||||
|
"no-mobile": "Para usar o Infisical, faça o login através de um dispositivo com dimensões maiores.",
|
||||||
|
"email": "Email",
|
||||||
|
"password": "Senha",
|
||||||
|
"first-name": "Primeiro Nome",
|
||||||
|
"last-name": "Ultimo Nome",
|
||||||
|
"logout": "Sair",
|
||||||
|
"validate-required": "Por favor insira o seu {{name}}",
|
||||||
|
"maintenance-alert": "Estamos passando por pequenas dificuldades técnicas. Estamos trabalhando para resolvê-lo agora. Por favor, volte em alguns minutos.",
|
||||||
|
"click-to-copy": "Clique para copiar",
|
||||||
|
"project-id": "ID do Projeto",
|
||||||
|
"save-changes": "Salvar Alterações",
|
||||||
|
"saved": "Salvou",
|
||||||
|
"drop-zone": "Arraste e solte seu arquivo .env aqui.",
|
||||||
|
"drop-zone-keys": "Arraste e solte seu arquivo .env aqui para adicionar mais chaves.",
|
||||||
|
"role": "Role",
|
||||||
|
"role_admin": "admin",
|
||||||
|
"display-name": "Nome de exibição",
|
||||||
|
"environment": "Ambiente",
|
||||||
|
"expired-in": "Expira em",
|
||||||
|
"language": "Linguagem",
|
||||||
|
"search": "Procurar...",
|
||||||
|
"note": "Note"
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user