1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-20 22:32:28 +00:00

Compare commits

..

198 Commits

Author SHA1 Message Date
3cd9241aee increase cli version 2023-01-13 19:52:29 -08:00
9ca544f680 Merge pull request from imakecodes/feature/adding-export-dotenv-export-format
feat(CLI): adding new export format (dotenv-export)
2023-01-13 19:50:10 -08:00
98d84b6717 add FormatDotEnvExport to list of available formats 2023-01-13 19:48:40 -08:00
3bc7f2aa7c Merge pull request from Gabriellopes232/language-support-ptbr
Language Support pt-BR
2023-01-13 17:03:43 -08:00
72b8dbda15 Merge branch 'main' into language-support-ptbr 2023-01-13 16:50:19 -08:00
439e86d763 Merge pull request from akhilmhdh/feat/#31
feat(): implemented api for environment crud operations
2023-01-13 16:45:35 -08:00
71fbf519ce Minor style changes - capitalization 2023-01-13 16:17:40 -08:00
d386f2702d Minor style changes to integrations 2023-01-13 16:05:50 -08:00
986434d66a Add infisical in Makefile for docker compose 2023-01-13 14:01:23 -08:00
30d84ede41 Merge pull request from Infisical/gamma-auto-deploy
Gamma auto deploy
2023-01-13 13:42:34 -08:00
87a3f9a03c delete gamma deploy workflow file 2023-01-13 13:41:56 -08:00
64d1f252e2 Rename workflow file 2023-01-13 13:39:30 -08:00
092e4a55bd enable auto deploy to gamma 2023-01-13 13:13:15 -08:00
a00e6df59f add manual approval setp 2023-01-13 13:09:57 -08:00
189d24589e correct needs fields in gha 2023-01-13 12:53:53 -08:00
17bae52830 gamma deployment after image build 2023-01-13 12:52:07 -08:00
323701d432 add gha upgrade helmchart 2023-01-13 12:38:56 -08:00
593765cb24 cat values file after downloading 2023-01-13 12:31:55 -08:00
fa60784a6b Add files via upload 2023-01-13 12:27:14 -08:00
eb9a8e0285 echo values file 2023-01-13 12:16:36 -08:00
d1f296b7e7 fix indent gamma deploy gha 2023-01-13 12:11:05 -08:00
dc6d036d86 write helm values to file form secret 2023-01-13 12:08:47 -08:00
58aee0239f docs: adding documentation about dotenv-export 2023-01-13 16:59:22 -03:00
799a839940 feat: adding new export format 2023-01-13 16:52:15 -03:00
0242707e33 gh action test kubectl 2023-01-13 11:49:38 -08:00
9974f889f3 Merge pull request from Infisical/gamma-auto-deploy
Add github action for gamma deploy
2023-01-13 11:45:01 -08:00
a8f38a5367 Add github action for gamma deploy 2023-01-13 11:40:29 -08:00
61318f28f7 Remove netlify and gh 2023-01-13 09:18:41 -08:00
036d32aeba feat(): added multi env support in integration 2023-01-13 21:09:59 +05:30
d03eff4f46 Merge pull request from Infisical/check-smtp-setup
Add SMTP support for AWS SES and docs for it
2023-01-13 20:26:12 +07:00
29592a1e9e Add SMTP support for AWS SES and docs for it 2023-01-13 20:25:13 +07:00
0f151fcd7a Merge pull request from Infisical/patch-integrations
Patch integrations
2023-01-13 18:19:08 +07:00
cbd8302afe Add temp patch for CRUD ops race conditions 2023-01-13 18:15:17 +07:00
6992c51e17 Working fixing integrations race condition 2023-01-13 16:36:34 +07:00
91f1090568 Merge branch 'main' of https://github.com/Infisical/infisical 2023-01-13 01:29:26 -08:00
6c61aef526 hotfix: fix the bug with pushing multiple envars in a sequence 2023-01-13 01:29:14 -08:00
b67abf94d4 Fixing minor bugs for custom environments 2023-01-13 01:02:43 -08:00
f57f3e6475 enable integ 2023-01-12 23:27:10 -08:00
d958341154 Corrected the telemetery event name 2023-01-12 22:15:17 -08:00
d579684d2f increase version 2023-01-12 22:00:06 -08:00
35466a7f4a Modify get secrets logic 2023-01-12 21:58:03 -08:00
95177074e3 Merge branch 'main' into feat/#31 2023-01-12 17:24:48 -08:00
1ac94ee940 selectively get user email from service toke/jwt 2023-01-12 16:38:12 -08:00
dc76be3d22 Merge pull request from Grraahaam/feat/translation-fr
feat: adding support for fr language 🌎🇫🇷
2023-01-12 16:10:02 -08:00
a707fe1498 disable integration 2023-01-12 15:56:49 -08:00
71f60f1589 Update modify secrets api v2 so that fields are optional 2023-01-12 15:31:58 -08:00
47fd48b7b0 Fixed the TS error during signup 2023-01-12 13:57:00 -08:00
07c65ded40 Refactored the logic for frontend dashboard 2023-01-12 01:05:13 -08:00
84700308f5 feat(): implemented ui for multi env and integrated api with backend
fix(): fixed all v2 release conflict
2023-01-11 23:12:05 +05:30
9116bf3344 feat(ui): implemented ui for env management table 2023-01-11 23:10:11 +05:30
3ad3e19bcf feat(): implemented api for environment crud operations 2023-01-11 23:10:11 +05:30
861639de27 Merge pull request from Infisical/patch-integrations
Patch Vercel API teamId requirement for team integrations
2023-01-11 17:04:21 +07:00
37ed27111a Patch Vercel API teamId requirement for team integrations 2023-01-11 16:53:53 +07:00
c527efad94 Revert "Disabled integrations for now"
This reverts commit 389f5c4f211de8f45b459d749ec18358a5263220.
2023-01-11 00:24:05 -05:00
389f5c4f21 Disabled integrations for now 2023-01-10 21:11:23 -08:00
acaae0b82c Merge pull request from Infisical/patch-integrations
Bring back sync integrations to CRUD secrets routes
2023-01-11 11:23:43 +07:00
b8f102493e Bring back sync integrations to CRUD secrets routes 2023-01-11 11:19:56 +07:00
286184ab48 Merge branch 'main' of https://github.com/Infisical/infisical 2023-01-11 10:20:34 +07:00
c0f0d699b4 Add files to api-docs branch 2023-01-11 10:18:44 +07:00
0b281a02d0 fix(i18n): add default empty string 2023-01-11 01:54:16 +01:00
d7b046236b Merge branch 'Infisical:main' into feat/translation-fr 2023-01-11 01:38:47 +01:00
d9b7f69838 fix(lang): add remaining translation on login + signin 2023-01-11 01:32:53 +01:00
16d2746749 fix(lang): add langugageMap fr 2023-01-11 01:30:15 +01:00
9ce4a52b8d Remove posthog for sev2 2023-01-10 19:07:50 -05:00
0fab5d32f2 Merge branch 'main' of https://github.com/Infisical/infisical 2023-01-10 14:08:12 -08:00
3fd5b521bb Removed service token logs 2023-01-10 14:08:02 -08:00
b8a750a31d Merge pull request from Infisical/depot-docker
Depot docker
2023-01-10 16:50:55 -05:00
e51046fe62 remove QEMU 2023-01-10 16:47:09 -05:00
7fde55414a add depot token 2023-01-10 16:36:06 -05:00
db639b1a89 add project id to depot 2023-01-10 16:31:02 -05:00
fbe2297ed6 Add depot 2023-01-10 16:28:48 -05:00
63a739d626 Removed posthog logs 2023-01-10 12:48:27 -08:00
2212c351ca Merge branch 'main' of https://github.com/Infisical/infisical 2023-01-10 11:39:43 -08:00
946fbe4716 Disabled integrations for now 2023-01-10 11:39:34 -08:00
1dbd121aa4 Try different keys for workflow cache 2023-01-10 14:30:52 -05:00
357d15b034 Merge branch 'main' of https://github.com/Infisical/infisical 2023-01-10 10:56:02 -08:00
a3db20cacf Fixed the bug with wrong project id in local storage 2023-01-10 10:55:52 -08:00
0ae73e873f Merge branch 'Infisical:main' into feat/translation-fr 2023-01-10 19:36:10 +01:00
b8edcab0d5 delete push-frontend-image-docker 2023-01-10 13:25:34 -05:00
be8a274e5a create separate frontend workflow 2023-01-10 13:20:37 -05:00
06f8826d67 Merge pull request from Infisical/docker-cache
Docker cache
2023-01-10 12:01:51 -05:00
97f77dcada set push to true for backend workflow 2023-01-10 11:59:17 -05:00
e4d302b7e1 Add cache to build step in backend 2023-01-10 11:56:42 -05:00
3eb2209eb8 add cache to build after test step 2023-01-10 11:35:58 -05:00
e7c75b544d Fixed the discrepancies between projectIds in url and local storage 2023-01-10 08:35:14 -08:00
07e6eb88ea use github cache 2023-01-10 11:21:14 -05:00
c81320c09d remove frontend testing 2023-01-10 11:14:40 -05:00
b10e28b9b5 set push to false to test cache 2023-01-10 10:44:51 -05:00
5409bdb0cb Add local cache 2023-01-10 10:37:53 -05:00
35c6e1d668 Merge pull request from Grraahaam/fix/typos
fix: readme and translation typos
2023-01-10 10:18:45 -05:00
d1467348d1 Update release_build.yml 2023-01-10 09:21:13 -05:00
b1ccb93d85 Update docker-image.yml 2023-01-10 09:19:55 -05:00
68c3b508e3 fix(doc): readme typos 2023-01-10 14:57:37 +01:00
1f68b8966d fix(front): translation typos 2023-01-10 14:57:22 +01:00
ef2da28cbe Merge branch 'Infisical:main' into feat/translation-fr 2023-01-10 14:53:01 +01:00
7fe706ad0d fix(lang): configured fr locale 2023-01-10 14:50:28 +01:00
a686462392 feat(lang): translated signup.json 2023-01-10 14:48:20 +01:00
878ca69f43 feat(lang): translated settings-project.json 2023-01-10 14:48:08 +01:00
ea9e185a65 feat(lang): translated settings-personal.json 2023-01-10 14:48:00 +01:00
1394368a43 feat(lang): translated settings-org.json 2023-01-10 14:47:50 +01:00
77b34467b9 feat(lang): translated settings-members.json 2023-01-10 14:47:19 +01:00
ee7cf7920d feat(lang): translated section-token.json 2023-01-10 14:46:55 +01:00
5bc8046f3f feat(lang): translated section-password.json 2023-01-10 14:46:46 +01:00
1423d05b52 feat(lang): translated section-members.json 2023-01-10 14:46:36 +01:00
1d0f51bb42 feat(lang): translated nav.json 2023-01-10 14:46:22 +01:00
aaa771a7b7 feat(lang): translated section-incident.json 2023-01-10 14:46:12 +01:00
2f67025376 feat(lang): translated login.json 2023-01-10 14:45:18 +01:00
f1c52fe332 feat(lang): translated integrations.json 2023-01-10 14:45:07 +01:00
1a90f27d6a feat(lang): translated dashboard.json 2023-01-10 14:44:56 +01:00
de1b75d99e feat(lang): translated common.json 2023-01-10 14:44:44 +01:00
295e93ac17 feat(lang): translated billing.json 2023-01-10 14:44:35 +01:00
0c59007fa8 feat(lang): translated activity.json 2023-01-10 14:44:23 +01:00
cbfd35e181 Merge pull request from Infisical/ph-telemetry
Draft: Telemetry for v2 routes
2023-01-10 15:27:41 +07:00
9b266309c2 Merge branch 'ph-telemetry' of https://github.com/Infisical/infisical into ph-telemetry 2023-01-10 15:22:12 +07:00
cc46b575b7 Delete array/brackets in /v2/secrets DELETE route 2023-01-10 15:21:50 +07:00
08ab27cad8 Merge branch 'ph-telemetry' of https://github.com/Infisical/infisical into ph-telemetry 2023-01-10 00:08:16 -08:00
387ef17038 Fix some dashabord bugs 2023-01-10 00:08:06 -08:00
b71ba35a22 Fix inviting existing user to org 2023-01-10 14:56:34 +07:00
c2a03e4e0c Comment out yaml fo rnow 2023-01-09 23:33:36 -08:00
266d8b7775 Fixed the bug with creating projects 2023-01-09 23:06:10 -08:00
52f234675a Merge branch 'main' of https://github.com/Infisical/infisical 2023-01-10 12:41:01 +07:00
0b2ac0470d Add activity logs docs 2023-01-10 12:40:52 +07:00
b1f62ffd35 Finish secret versioning docs 2023-01-10 12:04:53 +07:00
556a646dce Added sharing keys with a user while creating a new project 2023-01-09 19:01:22 -08:00
9762b580a5 fix typo in login message 2023-01-09 20:53:12 -05:00
9aa8bfa1a2 check if err is not nil first before checking error prefix 2023-01-09 19:43:01 -05:00
60a03cad98 Add error for when no login/no token 2023-01-09 19:28:22 -05:00
b702f29c46 Add warning log 2023-01-09 19:27:15 -05:00
12e104e12a Fix windows run bug by adding proper split on envior 2023-01-09 19:26:20 -05:00
b6ce660a3c add self recover when key not found in keychain 2023-01-09 19:22:03 -05:00
b03bd5fa08 set keyring to use defult keychain 2023-01-09 19:20:20 -05:00
6bd908f4cb allow viewing all secrets with service token 2023-01-09 19:18:43 -05:00
518606425a allow export to run with service token 2023-01-09 19:18:15 -05:00
ce7d411f29 Merge branch 'main' into ph-telemetry 2023-01-09 13:26:55 -08:00
933fed5da6 Got rid of i18n logs 2023-01-09 13:15:54 -08:00
486aa139c2 Changed frontend to use the new secrets routes 2023-01-09 13:14:07 -08:00
e3bf2791ee Continue PIT docs 2023-01-10 01:18:55 +07:00
f9e6ac2496 Add basic swagger autogen 2023-01-09 12:49:17 -05:00
a55b271525 Merge pull request from cerrussell/patch-1
Issue 159 - Docker image tags
2023-01-09 08:46:25 -05:00
b6189a90f4 Merge branch 'ph-telemetry' of https://github.com/Infisical/infisical into ph-telemetry 2023-01-09 17:11:51 +07:00
d2c77d9985 Patch integrations Secret querying by workspaceId 2023-01-09 17:11:33 +07:00
6ce12c71e1 Merge pull request from Infisical/activity-logs
Add endpoints for rollbacks and secret versions
2023-01-09 00:56:28 -08:00
8d53d2e4b1 Merge branch 'main' of https://github.com/Infisical/infisical 2023-01-09 10:38:51 +07:00
bd5dad71d4 Correct logging references 2023-01-09 10:27:36 +07:00
0bf8661350 fix: pattern folder based on i18next locales 2023-01-08 16:09:57 -03:00
35d23cf55c Finish preliminary /v2/secrets routes for batch/single CRUD secrets endpoints 2023-01-09 01:03:40 +07:00
69b819e7c4 refactor: adding translate pt-br in signup.json archive 2023-01-08 09:26:04 -03:00
d870ecc62a refactor: adding translate pt-br in setting-project.json archive 2023-01-08 09:25:52 -03:00
c0a0252cf5 refactor: adding translate pt-br in setting-personal.json archive 2023-01-08 09:25:38 -03:00
2f5186634c refactor: adding translate pt-br in setting-org.json archive 2023-01-08 09:25:27 -03:00
36525325fd refactor: adding translate pt-br in setting-members.json archive 2023-01-08 09:25:16 -03:00
a990a5ee7d refactor: adding translate pt-br in section-token.json archive 2023-01-08 09:24:55 -03:00
f2372bb265 refactor: adding translate pt-br in section-password.json archive 2023-01-08 09:24:44 -03:00
8c0046be87 refactor: adding translate pt-br in section-members.json archive 2023-01-08 09:24:29 -03:00
556858d1a8 refactor: adding translate pt-br in section-incident.json archive 2023-01-08 09:24:12 -03:00
2b147fce6e refactor: adding translate pt-br in nav.json archive 2023-01-08 09:23:31 -03:00
553be71ddf refactor: translate pt-br in login.json archive 2023-01-08 09:23:17 -03:00
9241020eb2 Added back latest tag 2023-01-08 03:12:30 -05:00
7e33f48a3b Added short commit to tag action 2023-01-08 03:06:40 -05:00
0312891f8b Merge pull request from Infisical/signup-flow
Refactor of the signup flow
2023-01-07 16:50:38 -08:00
6de4eca4fc Refactored signup and added team invitation step 2023-01-07 16:40:28 -08:00
b0fb86a6ac Added docs link to the dashboard 2023-01-07 11:46:17 -08:00
96b254d7c3 refactor: adding translate pt-br in login.json archive 2023-01-07 13:51:16 -03:00
3f1eaa8d42 refactor: adding translate pt-br in integrations.json archive 2023-01-07 13:51:04 -03:00
3e56fe95d2 refactor: adding translate pt-br in dashboard.json archive 2023-01-07 13:50:51 -03:00
15553e972a refactor: adding translate pt-br in common.json archive 2023-01-07 12:09:43 -03:00
47ab0b4a0f Add endpoints for rolling back a workspace to a secret snapshot and rolling back a secret to a version 2023-01-07 20:12:53 +07:00
f3f6871d81 UX fixes around the app 2023-01-06 22:39:54 -08:00
a438b8b91b Merge pull request from Infisical/snyk-upgrade-424d98e46758fcf23e4a0e06a413eb47
[Snyk] Upgrade @stripe/stripe-js from 1.36.0 to 1.46.0
2023-01-06 20:44:09 -08:00
498571b4fb Merge branch 'main' into snyk-upgrade-424d98e46758fcf23e4a0e06a413eb47 2023-01-06 20:41:44 -08:00
89136aab24 Merge pull request from JoaoVictor6/confirm-secret-key-delete
Add popup before secret delete
2023-01-06 20:40:40 -08:00
eed6c75836 Merge pull request from mocherfaoui/import-export-secrets
add ability to import/export secrets with comments
2023-01-06 20:26:36 -08:00
51368e6598 Trying to fix the telemetry issue in a pr check 2023-01-06 20:23:26 -08:00
7e534629ff Started adding telemetry to v2 routes 2023-01-06 17:32:19 -08:00
2c221dbb03 Fixed the missing field TS error 2023-01-06 17:08:27 -08:00
88ca056abb Fixing the typescript error 2023-01-06 17:04:46 -08:00
17133cd61b Fixing the yaml dependency version issue x2 2023-01-06 17:02:05 -08:00
2bbea36ce8 Fixing the yaml dependency version issue 2023-01-06 16:59:51 -08:00
5e03a54fa8 Merge branch 'main' into import-export-secrets 2023-01-06 16:07:11 -08:00
53273df51f Add single secrets v2 operations 2023-01-06 18:47:52 -05:00
a04fe00563 fix health check import 2023-01-06 15:55:30 -05:00
6afb276b35 Only show pass phrase env if env is not set 2023-01-06 15:13:55 -05:00
cb60151c0e Add status api 2023-01-06 10:51:15 -05:00
4c32f3dfd0 refactor: adding translate pt-br billing archive 2023-01-06 12:08:20 -03:00
c0d7b4ea88 feat: add "boilerplate" json 2023-01-06 11:41:32 -03:00
e6c631586a refactor: add "pt-BR" option 2023-01-06 11:40:51 -03:00
3e102fee3d chore: add language locale 2023-01-06 11:40:21 -03:00
9386efd7c4 Merge branch 'main' of https://github.com/Infisical/infisical 2023-01-05 23:15:26 -08:00
d90affbe87 UI bug fixes for the dashboard 2023-01-05 23:15:05 -08:00
08dc4532f4 Merge branch 'main' of https://github.com/Infisical/infisical 2023-01-06 13:09:32 +07:00
12a9b60cc5 refactor: use DeleteActionButton component and improve types 2023-01-05 22:35:03 -03:00
4c79aadc22 feat: create dialog and button for confirm delete 2023-01-05 22:34:18 -03:00
a87dc2fcb9 refactor: add new sentences
I would add it in the Korean folder, but I don't know it :(
2023-01-05 22:33:05 -03:00
098ae8533f Begin API key docs mint.json 2023-01-05 10:39:43 +07:00
9239b66b4b add new dependency: yaml 2022-12-30 01:06:00 +01:00
3715114232 add ability to export secrets with comments 2022-12-30 01:05:19 +01:00
5ef4e4cecb add ability to import secrets with comments 2022-12-30 01:03:07 +01:00
4f808a24bb fix: upgrade @stripe/stripe-js from 1.36.0 to 1.46.0
Snyk has created this PR to upgrade @stripe/stripe-js from 1.36.0 to 1.46.0.

See this package in npm:
https://www.npmjs.com/package/@stripe/stripe-js

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/53d4ecb6-6cc1-4918-aa73-bf9cae4ffd13?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-12-28 21:49:13 +00:00
204 changed files with 11571 additions and 4966 deletions
.github
MakefileREADME.md
backend
cli/packages
docker-compose.dev.yml
docs
frontend
components
const.js
ee
hooks
next-i18next.config.jspackage-lock.jsonpackage.json
pages
public
tsconfig.json
i18n
img

36
.github/values.yaml vendored Normal file

@ -0,0 +1,36 @@
frontend:
replicaCount: 1
image:
repository:
pullPolicy: Always
tag: "latest"
kubeSecretRef: managed-secret-frontend
backend:
replicaCount: 1
image:
repository:
pullPolicy: Always
tag: "latest"
kubeSecretRef: managed-backend-secret
ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
hostName: gamma.infisical.com
frontend:
path: /
pathType: Prefix
backend:
path: /api
pathType: Prefix
tls:
- secretName: echo-tls
hosts:
- gamma.infisical.com
backendEnvironmentVariables:
frontendEnvironmentVariables:

@ -1,5 +1,4 @@
name: Push to Docker Hub
name: Build, Publish and Deploy to Gamma
on: [workflow_dispatch]
jobs:
@ -10,8 +9,9 @@ jobs:
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 🔧 Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
@ -19,9 +19,13 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build backend and export to Docker
uses: docker/build-push-action@v3
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
load: true
context: backend
tags: infisical/backend:test
@ -35,11 +39,14 @@ jobs:
run: |
docker compose -f .github/resources/docker-compose.be-test.yml down
- name: 🏗️ Build backend and push
uses: docker/build-push-action@v3
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: backend
tags: infisical/backend:latest
tags: infisical/backend:${{ steps.commit.outputs.short }},
infisical/backend:latest
platforms: linux/amd64,linux/arm64
frontend-image:
@ -49,8 +56,9 @@ jobs:
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 🔧 Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
@ -58,10 +66,14 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build frontend and export to Docker
uses: docker/build-push-action@v3
uses: depot/build-push-action@v1
with:
load: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
project: 64mmf0n610
context: frontend
tags: infisical/frontend:test
build-args: |
@ -76,11 +88,45 @@ jobs:
run: |
docker stop infisical-frontend-test
- name: 🏗️ Build frontend and push
uses: docker/build-push-action@v3
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
push: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: frontend
tags: infisical/frontend:latest
tags: infisical/frontend:${{ steps.commit.outputs.short }},
infisical/frontend:latest
platforms: linux/amd64,linux/arm64
build-args: |
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

@ -1,4 +1,4 @@
name: Go releaser
name: Build and release CLI
on:
push:

@ -7,6 +7,9 @@ push:
up-dev:
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:
docker-compose -f docker-compose.yml up --build

@ -21,7 +21,7 @@
<a href="https://github.com/infisical/infisical/blob/main/CONTRIBUTING.md">
<img src="https://img.shields.io/badge/PRs-Welcome-brightgreen" alt="PRs welcome!" />
</a>
<a href="">
<a href="https://github.com/Infisical/infisical/issues">
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a>
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">
@ -40,13 +40,15 @@
- **[Language-Agnostic CLI](https://infisical.com/docs/cli/overview)** that pulls and injects environment variables into your local workflow
- **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure
- **Navigate Multiple Environments** per project (e.g. development, staging, production, etc.)
- **Personal/Shared** scoping for environment variables
- **Personal overrides** for environment variables
- **[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
- **[Activity Logs](https://infisical.com/docs/getting-started/dashboard/audit-logs)** - check what user in the project is performing what actions with secrets
- **[Point-in-time Secrets Recovery](https://infisical.com/docs/getting-started/dashboard/pit-recovery)** - roll back to any snapshot of you secrets
- 🔜 **1-Click Deploy** to Digital Ocean and Heroku
- 🔜 **Authentication/Authorization** for projects (read/write controls soon)
- 🔜 **Automatic Secret Rotation**
- 🔜 **2FA**
- 🔜 **Access Logs**
- 🔜 **Slack Integration & MS Teams** integrations
And more.
@ -65,7 +67,7 @@ To quickly get started, visit our [get started guide](https://infisical.com/docs
Infisical makes secret management simple and end-to-end encrypted by default. We're on a mission to make it more accessible to all developers, <i>not just security teams</i>.
According to a [report](https://www.ekransystem.com/en/blog/secrets-management) in 2019, only 10% of organizations use secret management solutions despite all using digital secrets to some extent.
According to a [report](https://www.ekransystem.com/en/blog/secrets-management), only 10% of organizations use secret management solutions despite all using digital secrets to some extent.
If you care about efficiency and security, then Infisical is right for you.
@ -319,7 +321,7 @@ Looking to report a security vulnerability? Please don't post about it in GitHub
## 🚨 Stay Up-to-Date
Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of new features are coming very quickly. Watch **releases** of this repository to be notified about future updates:
Infisical officially launched as v.1.0 on November 21st, 2022. There are a lot of new features coming very frequently. Watch **releases** of this repository to be notified about future updates:
![infisical-star-github](https://github.com/Infisical/infisical/blob/main/.github/images/star-infisical.gif?raw=true)
@ -331,10 +333,10 @@ Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jon4hz"><img src="https://avatars.githubusercontent.com/u/26183582?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/JoaoVictor6"><img src="https://avatars.githubusercontent.com/u/68869379?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mocherfaoui"><img src="https://avatars.githubusercontent.com/u/37941426?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jon4hz"><img src="https://avatars.githubusercontent.com/u/26183582?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
## 🌎 Translations
Infisical is currently aviable in English and Korean. Help us translate Infisical to your language!
Infisical is currently available in English and Korean. Help us translate Infisical to your language!
You can find all the info in [this issue](https://github.com/Infisical/infisical/issues/181).

File diff suppressed because it is too large Load Diff

@ -37,6 +37,8 @@
"query-string": "^7.1.3",
"rimraf": "^3.0.2",
"stripe": "^10.7.0",
"swagger-autogen": "^2.22.0",
"swagger-ui-express": "^4.6.0",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3",
@ -4549,7 +4551,6 @@
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -6690,7 +6691,6 @@
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"bin": {
"json5": "lib/cli.js"
},
@ -11174,6 +11174,47 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/swagger-autogen": {
"version": "2.22.0",
"resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.22.0.tgz",
"integrity": "sha512-MPdtwgx/RL3og0RjFVV9hPoQv3x+c3ZRhS0Vjp9k94DLV7iUgIuCg8H+uAT8oD5w48ATTRT1VjcOHlCGH62pdA==",
"dependencies": {
"acorn": "^7.4.1",
"deepmerge": "^4.2.2",
"glob": "^7.1.7",
"json5": "^2.2.1"
}
},
"node_modules/swagger-autogen/node_modules/acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/swagger-ui-dist": {
"version": "4.15.5",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz",
"integrity": "sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA=="
},
"node_modules/swagger-ui-express": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.0.tgz",
"integrity": "sha512-ZxpQFp1JR2RF8Ar++CyJzEDdvufa08ujNUJgMVTMWPi86CuQeVdBtvaeO/ysrz6dJAYXf9kbVNhWD7JWocwqsA==",
"dependencies": {
"swagger-ui-dist": ">=4.11.0"
},
"engines": {
"node": ">= v0.10.32"
},
"peerDependencies": {
"express": ">=4.0.0"
}
},
"node_modules/tar": {
"version": "6.1.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz",
@ -15582,8 +15623,7 @@
"deepmerge": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
"dev": true
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
},
"delayed-stream": {
"version": "1.0.0",
@ -17200,8 +17240,7 @@
"json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
},
"jsonwebtoken": {
"version": "9.0.0",
@ -20424,6 +20463,37 @@
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true
},
"swagger-autogen": {
"version": "2.22.0",
"resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.22.0.tgz",
"integrity": "sha512-MPdtwgx/RL3og0RjFVV9hPoQv3x+c3ZRhS0Vjp9k94DLV7iUgIuCg8H+uAT8oD5w48ATTRT1VjcOHlCGH62pdA==",
"requires": {
"acorn": "^7.4.1",
"deepmerge": "^4.2.2",
"glob": "^7.1.7",
"json5": "^2.2.1"
},
"dependencies": {
"acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="
}
}
},
"swagger-ui-dist": {
"version": "4.15.5",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz",
"integrity": "sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA=="
},
"swagger-ui-express": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.0.tgz",
"integrity": "sha512-ZxpQFp1JR2RF8Ar++CyJzEDdvufa08ujNUJgMVTMWPi86CuQeVdBtvaeO/ysrz6dJAYXf9kbVNhWD7JWocwqsA==",
"requires": {
"swagger-ui-dist": ">=4.11.0"
}
},
"tar": {
"version": "6.1.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz",

@ -1,46 +1,11 @@
{
"dependencies": {
"@godaddy/terminus": "^4.11.2",
"@octokit/rest": "^19.0.5",
"@sentry/node": "^7.14.0",
"@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0",
"axios": "^1.1.3",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"crypto-js": "^4.1.1",
"dotenv": "^16.0.1",
"express": "^4.18.1",
"express-rate-limit": "^6.7.0",
"express-validator": "^6.14.2",
"handlebars": "^4.7.7",
"helmet": "^5.1.1",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10",
"mongoose": "^6.7.2",
"nodemailer": "^6.8.0",
"posthog-node": "^2.2.2",
"query-string": "^7.1.3",
"rimraf": "^3.0.2",
"stripe": "^10.7.0",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3",
"utility-types": "^3.10.0",
"winston": "^3.8.2",
"winston-loki": "^6.0.6"
},
"name": "infisical-api",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"start": "npm run build && node build/index.js",
"dev": "nodemon",
"swagger-autogen": "node ./swagger.ts",
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build",
"lint": "eslint . --ext .ts",
"lint-and-fix": "eslint . --ext .ts --fix",
@ -108,5 +73,43 @@
"suiteNameTemplate": "{filepath}",
"classNameTemplate": "{classname}",
"titleTemplate": "{title}"
},
"dependencies": {
"@godaddy/terminus": "^4.11.2",
"@octokit/rest": "^19.0.5",
"@sentry/node": "^7.14.0",
"@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0",
"axios": "^1.1.3",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"crypto-js": "^4.1.1",
"dotenv": "^16.0.1",
"express": "^4.18.1",
"express-rate-limit": "^6.7.0",
"express-validator": "^6.14.2",
"handlebars": "^4.7.7",
"helmet": "^5.1.1",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10",
"mongoose": "^6.7.2",
"nodemailer": "^6.8.0",
"posthog-node": "^2.2.2",
"query-string": "^7.1.3",
"rimraf": "^3.0.2",
"stripe": "^10.7.0",
"swagger-autogen": "^2.22.0",
"swagger-ui-express": "^4.6.0",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3",
"utility-types": "^3.10.0",
"winston": "^3.8.2",
"winston-loki": "^6.0.6"
}
}

@ -1,11 +1,15 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { patchRouterParam } = require('./utils/patchAsyncRoutes');
import express from 'express';
import express, { Request, Response } from 'express';
import helmet from 'helmet';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import dotenv from 'dotenv';
import swaggerUi = require('swagger-ui-express');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const swaggerFile = require('../api-documentation.json')
dotenv.config();
import { PORT, NODE_ENV, SITE_URL } from './config';
@ -38,11 +42,15 @@ import {
} from './routes/v1';
import {
secret as v2SecretRouter,
secrets as v2SecretsRouter,
workspace as v2WorkspaceRouter,
serviceTokenData as v2ServiceTokenDataRouter,
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
} from './routes/v2';
import { healthCheck } from './routes/status';
import { getLogger } from './utils/logger';
import { RouteNotFoundError } from './utils/errors';
import { requestErrorHandler } from './middleware/requestErrorHandler';
@ -89,18 +97,26 @@ app.use('/api/v1/membership', v1MembershipRouter);
app.use('/api/v1/key', v1KeyRouter);
app.use('/api/v1/invite-org', v1InviteOrgRouter);
app.use('/api/v1/secret', v1SecretRouter);
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate
app.use('/api/v1/service-token', v1ServiceTokenRouter); // stop supporting
app.use('/api/v1/password', v1PasswordRouter);
app.use('/api/v1/stripe', v1StripeRouter);
app.use('/api/v1/integration', v1IntegrationRouter);
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
// v2 routes
app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter);
app.use('/api/v2/workspace', v2EnvironmentRouter);
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/secrets', v2SecretsRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
app.use('/api/v2/api-key-data', v2APIKeyDataRouter);
// api docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
// Server status
app.use('/api', healthCheck)
//* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next) => {
if (res.headersSent) return next();

@ -3,7 +3,7 @@ import * as Sentry from '@sentry/node';
import axios from 'axios';
import { readFileSync } from 'fs';
import { IntegrationAuth, Integration } from '../../models';
import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../../variables';
import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
import { IntegrationService } from '../../services';
import { getApps, revokeAccess } from '../../integrations';
@ -31,11 +31,17 @@ export const oAuthExchange = async (
if (!INTEGRATION_SET.has(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({
workspaceId,
integration,
code
code,
environment: environments[0].slug,
});
} catch (err) {
Sentry.setUser(null);

@ -115,13 +115,14 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
if (!membershipOrg) {
throw new Error('Failed to validate organization membership');
}
invitee = await User.findOne({
email: inviteeEmail
});
}).select('+publicKey');
if (invitee) {
// case: invitee is an existing user
inviteeMembershipOrg = await MembershipOrg.findOne({
user: invitee._id,
organization: organizationId

@ -9,7 +9,6 @@ import {
import { pushKeys } from '../../helpers/key';
import { eventPushSecrets } from '../../events';
import { EventService } from '../../services';
import { ENV_SET } from '../../variables';
import { postHogClient } from '../../services';
interface PushSecret {
@ -44,7 +43,8 @@ export const pushSecrets = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
// 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');
}
@ -116,7 +116,8 @@ export const pullSecrets = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
// 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');
}
@ -183,7 +184,8 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
// 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');
}

@ -1,7 +1,6 @@
import { Request, Response } from 'express';
import { ServiceToken } from '../../models';
import { createToken } from '../../helpers/auth';
import { ENV_SET } from '../../variables';
import { JWT_SERVICE_SECRET } from '../../config';
/**
@ -36,7 +35,8 @@ export const createServiceToken = async (req: Request, res: Response) => {
} = req.body;
// 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');
}

@ -65,7 +65,6 @@ export const createAPIKeyData = async (req: Request, res: Response) => {
apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
} catch (err) {
console.error(err);
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({

@ -0,0 +1,204 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import {
Secret,
ServiceToken,
Workspace,
Integration,
ServiceTokenData,
} from '../../models';
import { SecretVersion } from '../../ee/models';
/**
* Create new workspace environment named [environmentName] under workspace with id
* @param req
* @param res
* @returns
*/
export const createWorkspaceEnvironment = async (
req: Request,
res: Response
) => {
const { workspaceId } = req.params;
const { environmentName, environmentSlug } = req.body;
try {
const workspace = await Workspace.findById(workspaceId).exec();
if (
!workspace ||
workspace?.environments.find(
({ name, slug }) => slug === environmentSlug || environmentName === name
)
) {
throw new Error('Failed to create workspace environment');
}
workspace?.environments.push({
name: environmentName.toLowerCase(),
slug: environmentSlug.toLowerCase(),
});
await workspace.save();
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create new workspace environment',
});
}
return res.status(200).send({
message: 'Successfully created new environment',
workspace: workspaceId,
environment: {
name: environmentName,
slug: environmentSlug,
},
});
};
/**
* Rename workspace environment with new name and slug of a workspace with [workspaceId]
* Old slug [oldEnvironmentSlug] must be provided
* @param req
* @param res
* @returns
*/
export const renameWorkspaceEnvironment = async (
req: Request,
res: Response
) => {
const { workspaceId } = req.params;
const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body;
try {
// user should pass both new slug and env name
if (!environmentSlug || !environmentName) {
throw new Error('Invalid environment given.');
}
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw new Error('Failed to create workspace environment');
}
const isEnvExist = workspace.environments.some(
({ name, slug }) =>
slug !== oldEnvironmentSlug &&
(name === environmentName || slug === environmentSlug)
);
if (isEnvExist) {
throw new Error('Invalid environment given');
}
const envIndex = workspace?.environments.findIndex(
({ slug }) => slug === oldEnvironmentSlug
);
if (envIndex === -1) {
throw new Error('Invalid environment given');
}
workspace.environments[envIndex].name = environmentName.toLowerCase();
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
await workspace.save();
await Secret.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretVersion.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceToken.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceTokenData.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Integration.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update workspace environment',
});
}
return res.status(200).send({
message: 'Successfully update environment',
workspace: workspaceId,
environment: {
name: environmentName,
slug: environmentSlug,
},
});
};
/**
* Delete workspace environment by [environmentSlug] of workspace [workspaceId] and do the clean up
* @param req
* @param res
* @returns
*/
export const deleteWorkspaceEnvironment = async (
req: Request,
res: Response
) => {
const { workspaceId } = req.params;
const { environmentSlug } = req.body;
try {
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw new Error('Failed to create workspace environment');
}
const envIndex = workspace?.environments.findIndex(
({ slug }) => slug === environmentSlug
);
if (envIndex === -1) {
throw new Error('Invalid environment given');
}
workspace.environments.splice(envIndex, 1);
await workspace.save();
// clean up
await Secret.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await SecretVersion.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceToken.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceTokenData.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await Integration.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete workspace environment',
});
}
return res.status(200).send({
message: 'Successfully deleted environment',
workspace: workspaceId,
environment: environmentSlug,
});
};

@ -2,10 +2,14 @@ import * as workspaceController from './workspaceController';
import * as serviceTokenDataController from './serviceTokenDataController';
import * as apiKeyDataController from './apiKeyDataController';
import * as secretController from './secretController';
import * as secretsController from './secretsController';
import * as environmentController from './environmentController';
export {
workspaceController,
serviceTokenDataController,
apiKeyDataController,
secretController
secretController,
secretsController,
environmentController
}

@ -7,10 +7,68 @@ const { ValidationError } = mongoose.Error;
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
import { AnyBulkWriteOperation } from 'mongodb';
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
import { postHogClient } from '../../services';
export const batchCreateSecrets = async (req: Request, res: Response) => {
/**
* Create secret for workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
*/
export const createSecret = async (req: Request, res: Response) => {
const secretToCreate: CreateSecretRequestBody = req.body.secret;
const { workspaceId, environment } = req.params
const sanitizedSecret: SanitizedSecretForCreate = {
secretKeyCiphertext: secretToCreate.secretKeyCiphertext,
secretKeyIV: secretToCreate.secretKeyIV,
secretKeyTag: secretToCreate.secretKeyTag,
secretKeyHash: secretToCreate.secretKeyHash,
secretValueCiphertext: secretToCreate.secretValueCiphertext,
secretValueIV: secretToCreate.secretValueIV,
secretValueTag: secretToCreate.secretValueTag,
secretValueHash: secretToCreate.secretValueHash,
secretCommentCiphertext: secretToCreate.secretCommentCiphertext,
secretCommentIV: secretToCreate.secretCommentIV,
secretCommentTag: secretToCreate.secretCommentTag,
secretCommentHash: secretToCreate.secretCommentHash,
workspace: new Types.ObjectId(workspaceId),
environment,
type: secretToCreate.type,
user: new Types.ObjectId(req.user._id)
}
const [error, secret] = await to(Secret.create(sanitizedSecret).then())
if (error instanceof ValidationError) {
throw RouteValidationError({ message: error.message, stack: error.stack })
}
if (postHogClient) {
postHogClient.capture({
event: 'secrets added',
distinctId: req.user.email,
properties: {
numberOfSecrets: 1,
workspaceId,
environment,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.['user-agent']
}
});
}
res.status(200).send({
secret
})
}
/**
* Create many secrets for workspace wiht id [workspaceId] and environment [environment]
* @param req
* @param res
*/
export const createSecrets = async (req: Request, res: Response) => {
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
const { workspaceId, environmentName } = req.params
const { workspaceId, environment } = req.params
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
secretsToCreate.forEach(rawSecret => {
@ -28,7 +86,7 @@ export const batchCreateSecrets = async (req: Request, res: Response) => {
secretCommentTag: rawSecret.secretCommentTag,
secretCommentHash: rawSecret.secretCommentHash,
workspace: new Types.ObjectId(workspaceId),
environment: environmentName,
environment,
type: rawSecret.type,
user: new Types.ObjectId(req.user._id)
}
@ -36,7 +94,7 @@ export const batchCreateSecrets = async (req: Request, res: Response) => {
sanitizedSecretesToCreate.push(safeUpdateFields)
})
const [bulkCreateError, newlyCreatedSecrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then())
const [bulkCreateError, secrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then())
if (bulkCreateError) {
if (bulkCreateError instanceof ValidationError) {
throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack })
@ -45,20 +103,31 @@ export const batchCreateSecrets = async (req: Request, res: Response) => {
throw InternalServerError({ message: "Unable to process your batch create request. Please try again", stack: bulkCreateError.stack })
}
res.status(200).send()
}
export const createSingleSecret = async (req: Request, res: Response) => {
try {
const secretFromDB = await Secret.findById(req.params.secretId)
return res.status(200).send(secretFromDB);
} catch (e) {
throw BadRequestError({ message: "Unable to find the requested secret" })
if (postHogClient) {
postHogClient.capture({
event: 'secrets added',
distinctId: req.user.email,
properties: {
numberOfSecrets: (secretsToCreate ?? []).length,
workspaceId,
environment,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.['user-agent']
}
});
}
res.status(200).send({
secrets
})
}
export const batchDeleteSecrets = async (req: Request, res: Response) => {
/**
* Delete secrets in workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
*/
export const deleteSecrets = async (req: Request, res: Response) => {
const { workspaceId, environmentName } = req.params
const secretIdsToDelete: string[] = req.body.secretIds
@ -70,10 +139,12 @@ export const batchDeleteSecrets = async (req: Request, res: Response) => {
const secretsUserCanDeleteSet: Set<string> = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString()));
const deleteOperationsToPerform: AnyBulkWriteOperation<ISecret>[] = []
let numSecretsDeleted = 0;
secretIdsToDelete.forEach(secretIdToDelete => {
if (secretsUserCanDeleteSet.has(secretIdToDelete)) {
const deleteOperation = { deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } } }
deleteOperationsToPerform.push(deleteOperation)
numSecretsDeleted++;
} else {
throw RouteValidationError({ message: "You cannot delete secrets that you do not have access to" })
}
@ -87,10 +158,57 @@ export const batchDeleteSecrets = async (req: Request, res: Response) => {
throw InternalServerError()
}
if (postHogClient) {
postHogClient.capture({
event: 'secrets deleted',
distinctId: req.user.email,
properties: {
numberOfSecrets: numSecretsDeleted,
environment: environmentName,
workspaceId,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.['user-agent']
}
});
}
res.status(200).send()
}
export const batchModifySecrets = async (req: Request, res: Response) => {
/**
* Delete secret with id [secretId]
* @param req
* @param res
*/
export const deleteSecret = async (req: Request, res: Response) => {
await Secret.findByIdAndDelete(req._secret._id)
if (postHogClient) {
postHogClient.capture({
event: 'secrets deleted',
distinctId: req.user.email,
properties: {
numberOfSecrets: 1,
workspaceId: req._secret.workspace.toString(),
environment: req._secret.environment,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.['user-agent']
}
});
}
res.status(200).send({
secret: req._secret
})
}
/**
* Update secrets for workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
* @returns
*/
export const updateSecrets = async (req: Request, res: Response) => {
const { workspaceId, environmentName } = req.params
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
@ -101,7 +219,6 @@ export const batchModifySecrets = async (req: Request, res: Response) => {
const secretsUserCanModifySet: Set<string> = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString()));
const updateOperationsToPerform: any = []
secretsModificationsRequested.forEach(userModifiedSecret => {
if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) {
const sanitizedSecret: SanitizedSecretModify = {
@ -135,23 +252,99 @@ export const batchModifySecrets = async (req: Request, res: Response) => {
throw InternalServerError()
}
if (postHogClient) {
postHogClient.capture({
event: 'secrets modified',
distinctId: req.user.email,
properties: {
numberOfSecrets: (secretsModificationsRequested ?? []).length,
environment: environmentName,
workspaceId,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.['user-agent']
}
});
}
return res.status(200).send()
}
export const fetchAllSecrets = async (req: Request, res: Response) => {
/**
* Update a secret within workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
* @returns
*/
export const updateSecret = async (req: Request, res: Response) => {
const { workspaceId, environmentName } = req.params
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
const [secretIdUserCanModifyError, secretIdUserCanModify] = await to(Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
if (secretIdUserCanModifyError && !secretIdUserCanModify) {
throw BadRequestError()
}
const sanitizedSecret: SanitizedSecretModify = {
secretKeyCiphertext: secretModificationsRequested.secretKeyCiphertext,
secretKeyIV: secretModificationsRequested.secretKeyIV,
secretKeyTag: secretModificationsRequested.secretKeyTag,
secretKeyHash: secretModificationsRequested.secretKeyHash,
secretValueCiphertext: secretModificationsRequested.secretValueCiphertext,
secretValueIV: secretModificationsRequested.secretValueIV,
secretValueTag: secretModificationsRequested.secretValueTag,
secretValueHash: secretModificationsRequested.secretValueHash,
secretCommentCiphertext: secretModificationsRequested.secretCommentCiphertext,
secretCommentIV: secretModificationsRequested.secretCommentIV,
secretCommentTag: secretModificationsRequested.secretCommentTag,
secretCommentHash: secretModificationsRequested.secretCommentHash,
}
const [error, singleModificationUpdate] = await to(Secret.updateOne({ _id: secretModificationsRequested._id, workspace: workspaceId }, { $inc: { version: 1 }, $set: sanitizedSecret }).then())
if (error instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: error.stack })
}
if (postHogClient) {
postHogClient.capture({
event: 'secrets modified',
distinctId: req.user.email,
properties: {
numberOfSecrets: 1,
environment: environmentName,
workspaceId,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.['user-agent']
}
});
}
return res.status(200).send(singleModificationUpdate)
}
/**
* Return secrets for workspace with id [workspaceId], environment [environment] and user
* with id [req.user._id]
* @param req
* @param res
* @returns
*/
export const getSecrets = async (req: Request, res: Response) => {
const { environment } = req.query;
const { workspaceId } = req.params;
let userId: string | undefined = undefined // Used for choosing the personal secrets to fetch in
let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user
let userEmail: Types.ObjectId | undefined = undefined // used for posthog
if (req.user) {
userId = req.user._id.toString();
userId = req.user._id;
userEmail = req.user.email;
}
if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
userEmail = req.serviceTokenData.user.email;
}
const [retriveAllSecretsError, allSecrets] = await to(Secret.find(
const [err, secrets] = await to(Secret.find(
{
workspace: workspaceId,
environment,
@ -160,9 +353,49 @@ export const fetchAllSecrets = async (req: Request, res: Response) => {
}
).then())
if (retriveAllSecretsError instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to get secrets, please try again", stack: retriveAllSecretsError.stack })
if (err) {
throw RouteValidationError({ message: "Failed to get secrets, please try again", stack: err.stack })
}
return res.json(allSecrets)
if (postHogClient) {
postHogClient.capture({
event: 'secrets pulled',
distinctId: userEmail,
properties: {
numberOfSecrets: (secrets ?? []).length,
environment,
workspaceId,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.['user-agent']
}
});
}
return res.json(secrets)
}
/**
* Return secret with id [secretId]
* @param req
* @param res
* @returns
*/
export const getSecret = async (req: Request, res: Response) => {
// if (postHogClient) {
// postHogClient.capture({
// event: 'secrets pulled',
// distinctId: req.user.email,
// properties: {
// numberOfSecrets: 1,
// workspaceId: req._secret.workspace.toString(),
// environment: req._secret.environment,
// channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
// userAgent: req.headers?.['user-agent']
// }
// });
// }
return res.status(200).send({
secret: req._secret
});
}

@ -0,0 +1,476 @@
import to from 'await-to-js';
import { Types } from 'mongoose';
import { Request, Response } from 'express';
import { ISecret, Secret } from '../../models';
import {
SECRET_PERSONAL,
SECRET_SHARED,
ACTION_ADD_SECRETS,
ACTION_READ_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS
} from '../../variables';
import { ValidationError } from '../../utils/errors';
import { EventService } from '../../services';
import { eventPushSecrets } from '../../events';
import { EESecretService, EELogService } from '../../ee/services';
import { postHogClient } from '../../services';
import { BadRequestError } from '../../utils/errors';
/**
* Create secret(s) for workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
*/
export const createSecrets = async (req: Request, res: Response) => {
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
const { workspaceId, environment } = req.body;
let toAdd;
if (Array.isArray(req.body.secrets)) {
// case: create multiple secrets
toAdd = req.body.secrets;
} else if (typeof req.body.secrets === 'object') {
// case: create 1 secret
toAdd = [req.body.secrets];
}
const newSecrets = await Secret.insertMany(
toAdd.map(({
type,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
}: {
type: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
}) => ({
version: 1,
workspace: new Types.ObjectId(workspaceId),
type,
user: type === SECRET_PERSONAL ? req.user : undefined,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
}))
);
setTimeout(async () => {
// trigger event - push secrets
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId
})
});
}, 5000);
// (EE) add secret versions for new secrets
await EESecretService.addSecretVersions({
secretVersions: newSecrets.map(({
_id,
version,
workspace,
type,
user,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
}) => ({
_id: new Types.ObjectId(),
secret: _id,
version,
workspace,
type,
user,
environment,
isDeleted: false,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
}))
});
const addAction = await EELogService.createActionSecret({
name: ACTION_ADD_SECRETS,
userId: req.user._id.toString(),
workspaceId,
secretIds: newSecrets.map((n) => n._id)
});
// (EE) create (audit) log
addAction && await EELogService.createLog({
userId: req.user._id.toString(),
workspaceId,
actions: [addAction],
channel,
ipAddress: req.ip
});
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId
});
if (postHogClient) {
postHogClient.capture({
event: 'secrets added',
distinctId: req.user.email,
properties: {
numberOfSecrets: toAdd.length,
environment,
workspaceId,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.['user-agent']
}
});
}
return res.status(200).send({
secrets: newSecrets
});
}
/**
* Return secret(s) for workspace with id [workspaceId], environment [environment] and user
* with id [req.user._id]
* @param req
* @param res
* @returns
*/
export const getSecrets = async (req: Request, res: Response) => {
const { workspaceId, environment } = req.query;
let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user
let userEmail: Types.ObjectId | undefined = undefined // used for posthog
if (req.user) {
userId = req.user._id;
userEmail = req.user.email;
}
if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
userEmail = req.serviceTokenData.user.email;
}
const [err, secrets] = await to(Secret.find(
{
workspace: workspaceId,
environment,
$or: [
{ user: userId },
{ user: { $exists: false } }
],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
).then())
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
const readAction = await EELogService.createActionSecret({
name: ACTION_READ_SECRETS,
userId: req.user._id.toString(),
workspaceId: workspaceId as string,
secretIds: secrets.map((n: any) => n._id)
});
readAction && await EELogService.createLog({
userId: req.user._id.toString(),
workspaceId: workspaceId as string,
actions: [readAction],
channel,
ipAddress: req.ip
});
if (postHogClient) {
postHogClient.capture({
event: 'secrets pulled',
distinctId: userEmail,
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel,
userAgent: req.headers?.['user-agent']
}
});
}
return res.status(200).send({
secrets
});
}
/**
* Update secret(s)
* @param req
* @param res
*/
export const updateSecrets = async (req: Request, res: Response) => {
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
// TODO: move type
interface PatchSecret {
id: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
}
const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => {
const {
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
} = secret;
return ({
updateOne: {
filter: { _id: new Types.ObjectId(secret.id) },
update: {
$inc: {
version: 1
},
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
...((
secretCommentCiphertext &&
secretCommentIV &&
secretCommentTag
) ? {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
} : {}),
}
}
});
});
await Secret.bulkWrite(updateOperationsToPerform);
const secretModificationsBySecretId: { [key: string]: PatchSecret } = {};
req.body.secrets.forEach((secret: PatchSecret) => {
secretModificationsBySecretId[secret.id] = secret;
});
const ListOfSecretsBeforeModifications = req.secrets
const secretVersions = {
secretVersions: ListOfSecretsBeforeModifications.map((secret: ISecret) => {
const {
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
} = secretModificationsBySecretId[secret._id.toString()]
return ({
secret: secret._id,
version: secret.version + 1,
workspace: secret.workspace,
type: secret.type,
environment: secret.environment,
secretKeyCiphertext: secretKeyCiphertext ? secretKeyCiphertext : secret.secretKeyCiphertext,
secretKeyIV: secretKeyIV ? secretKeyIV : secret.secretKeyIV,
secretKeyTag: secretKeyTag ? secretKeyTag : secret.secretKeyTag,
secretValueCiphertext: secretValueCiphertext ? secretValueCiphertext : secret.secretValueCiphertext,
secretValueIV: secretValueIV ? secretValueIV : secret.secretValueIV,
secretValueTag: secretValueTag ? secretValueTag : secret.secretValueTag,
secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext,
secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
});
})
}
await EESecretService.addSecretVersions(secretVersions);
// group secrets into workspaces so updated secrets can
// be logged and snapshotted separately for each workspace
const workspaceSecretObj: any = {};
req.secrets.forEach((s: any) => {
if (s.workspace.toString() in workspaceSecretObj) {
workspaceSecretObj[s.workspace.toString()].push(s);
} else {
workspaceSecretObj[s.workspace.toString()] = [s]
}
});
Object.keys(workspaceSecretObj).forEach(async (key) => {
// trigger event - push secrets
setTimeout(async () => {
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: key
})
});
}, 10000);
const updateAction = await EELogService.createActionSecret({
name: ACTION_UPDATE_SECRETS,
userId: req.user._id.toString(),
workspaceId: key,
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
});
// (EE) create (audit) log
updateAction && await EELogService.createLog({
userId: req.user._id.toString(),
workspaceId: key,
actions: [updateAction],
channel,
ipAddress: req.ip
});
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: key
})
if (postHogClient) {
postHogClient.capture({
event: 'secrets modified',
distinctId: req.user.email,
properties: {
numberOfSecrets: workspaceSecretObj[key].length,
environment: workspaceSecretObj[key][0].environment,
workspaceId: key,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.['user-agent']
}
});
}
});
return res.status(200).send({
secrets: await Secret.find({
_id: {
$in: req.secrets.map((secret: ISecret) => secret._id)
}
})
});
}
/**
* Delete secret(s) with id [workspaceId] and environment [environment]
* @param req
* @param res
*/
export const deleteSecrets = async (req: Request, res: Response) => {
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
const toDelete = req.secrets.map((s: any) => s._id);
await Secret.deleteMany({
_id: {
$in: toDelete
}
});
await EESecretService.markDeletedSecretVersions({
secretIds: toDelete
});
// group secrets into workspaces so deleted secrets can
// be logged and snapshotted separately for each workspace
const workspaceSecretObj: any = {};
req.secrets.forEach((s: any) => {
if (s.workspace.toString() in workspaceSecretObj) {
workspaceSecretObj[s.workspace.toString()].push(s);
} else {
workspaceSecretObj[s.workspace.toString()] = [s]
}
});
Object.keys(workspaceSecretObj).forEach(async (key) => {
// trigger event - push secrets
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: key
})
});
const deleteAction = await EELogService.createActionSecret({
name: ACTION_DELETE_SECRETS,
userId: req.user._id.toString(),
workspaceId: key,
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
});
// (EE) create (audit) log
deleteAction && await EELogService.createLog({
userId: req.user._id.toString(),
workspaceId: key,
actions: [deleteAction],
channel,
ipAddress: req.ip
});
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: key
})
if (postHogClient) {
postHogClient.capture({
event: 'secrets deleted',
distinctId: req.user.email,
properties: {
numberOfSecrets: workspaceSecretObj[key].length,
environment: workspaceSecretObj[key][0].environment,
workspaceId: key,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.['user-agent']
}
});
}
});
return res.status(200).send({
secrets: req.secrets
});
}

@ -19,7 +19,6 @@ import {
import { pushKeys } from '../../helpers/key';
import { postHogClient, EventService } from '../../services';
import { eventPushSecrets } from '../../events';
import { ENV_SET } from '../../variables';
interface V2PushSecret {
type: string; // personal or shared
@ -52,7 +51,8 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
// 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');
}
@ -129,6 +129,11 @@ export const pullSecrets = async (req: Request, res: Response) => {
} else if (req.serviceTokenData) {
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({
userId,

@ -1,6 +1,8 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Secret } from '../../../models';
import { SecretVersion } from '../../models';
import { EESecretService } from '../../services';
/**
* Return secret versions for secret with id [secretId]
@ -33,4 +35,103 @@ import { SecretVersion } from '../../models';
return res.status(200).send({
secretVersions
});
}
/**
* Roll back secret with id [secretId] to version [version]
* @param req
* @param res
* @returns
*/
export const rollbackSecretVersion = async (req: Request, res: Response) => {
let secret;
try {
const { secretId } = req.params;
const { version } = req.body;
// validate secret version
const oldSecretVersion = await SecretVersion.findOne({
secret: secretId,
version
});
if (!oldSecretVersion) throw new Error('Failed to find secret version');
const {
workspace,
type,
user,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
} = oldSecretVersion;
// update secret
secret = await Secret.findByIdAndUpdate(
secretId,
{
$inc: {
version: 1
},
workspace,
type,
user,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
},
{
new: true
}
);
if (!secret) throw new Error('Failed to find and update secret');
// add new secret version
await new SecretVersion({
secret: secretId,
version: secret.version,
workspace,
type,
user,
environment,
isDeleted: false,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
}).save();
// take secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: secret.workspace.toString()
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to roll back secret version'
});
}
return res.status(200).send({
secret
});
}

@ -2,6 +2,12 @@ import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { SecretSnapshot } from '../../models';
/**
* Return secret snapshot with id [secretSnapshotId]
* @param req
* @param res
* @returns
*/
export const getSecretSnapshot = async (req: Request, res: Response) => {
let secretSnapshot;
try {

@ -1,9 +1,17 @@
import e, { Request, Response } from 'express';
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Secret
} from '../../../models';
import {
SecretSnapshot,
Log
Log,
SecretVersion,
ISecretVersion
} from '../../models';
import { EESecretService } from '../../services';
import { getLatestSecretVersionIds } from '../../helpers/secretVersion';
/**
* Return secret snapshots for workspace with id [workspaceId]
@ -63,6 +71,159 @@ export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Respon
});
}
/**
* Rollback secret snapshot with id [secretSnapshotId] to version [version]
* @param req
* @param res
* @returns
*/
export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Response) => {
let secrets;
try {
const { workspaceId } = req.params;
const { version } = req.body;
// validate secret snapshot
const secretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId,
version
}).populate<{ secretVersions: ISecretVersion[]}>('secretVersions');
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
// TODO: fix any
const oldSecretVersionsObj: any = secretSnapshot.secretVersions
.reduce((accumulator, s) => ({
...accumulator,
[`${s.secret.toString()}`]: s
}), {});
const latestSecretVersionIds = await getLatestSecretVersionIds({
secretIds: secretSnapshot.secretVersions.map((sv) => sv.secret)
});
// TODO: fix any
const latestSecretVersions: any = (await SecretVersion.find({
_id: {
$in: latestSecretVersionIds.map((s) => s.versionId)
}
}, 'secret version'))
.reduce((accumulator, s) => ({
...accumulator,
[`${s.secret.toString()}`]: s
}), {});
// delete existing secrets
await Secret.deleteMany({
workspace: workspaceId
});
// add secrets
secrets = await Secret.insertMany(
secretSnapshot.secretVersions.map((sv) => {
const secretId = sv.secret;
const {
workspace,
type,
user,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
createdAt
} = oldSecretVersionsObj[secretId.toString()];
return ({
_id: secretId,
version: latestSecretVersions[secretId.toString()].version + 1,
workspace,
type,
user,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext: '',
secretCommentIV: '',
secretCommentTag: '',
createdAt
});
})
);
// add secret versions
await SecretVersion.insertMany(
secrets.map(({
_id,
version,
workspace,
type,
user,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
}) => ({
_id: new Types.ObjectId(),
secret: _id,
version,
workspace,
type,
user,
environment,
isDeleted: false,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
}))
);
// update secret versions of restored secrets as not deleted
await SecretVersion.updateMany({
secret: {
$in: secretSnapshot.secretVersions.map((sv) => sv.secret)
}
}, {
isDeleted: false
});
// take secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to roll back secret snapshot'
});
}
return res.status(200).send({
secrets
});
}
/**
* Return (audit) logs for workspace with id [workspaceId]
* @param req

@ -1,7 +1,10 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { Secret } from '../../models';
import { SecretVersion, Action } from '../models';
import {
getLatestSecretVersionIds,
getLatestNSecretSecretVersionIds
} from '../helpers/secretVersion';
import { ACTION_UPDATE_SECRETS } from '../../variables';
/**
@ -30,65 +33,23 @@ const createActionSecretHelper = async ({
if (name === ACTION_UPDATE_SECRETS) {
// case: action is updating secrets
// -> add old and new secret versions
// TODO: make query more efficient
latestSecretVersions = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds,
},
},
},
{
$sort: { version: -1 },
},
{
$group: {
_id: "$secret",
versions: { $push: "$$ROOT" },
},
},
{
$project: {
_id: 0,
secret: "$_id",
versions: { $slice: ["$versions", 2] },
},
}
]))
.map((s) => ({
oldSecretVersion: s.versions[0]._id,
newSecretVersion: s.versions[1]._id
}));
latestSecretVersions = (await getLatestNSecretSecretVersionIds({
secretIds,
n: 2
}))
.map((s) => ({
oldSecretVersion: s.versions[0]._id,
newSecretVersion: s.versions[1]._id
}));
} else {
// case: action is adding, deleting, or reading secrets
// -> add new secret versions
latestSecretVersions = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds
}
}
},
{
$group: {
_id: '$secret',
version: { $max: '$version' },
versionId: { $max: '$_id' } // secret version id
}
},
{
$sort: { version: -1 }
}
])
.exec())
.map((s) => ({
newSecretVersion: s.versionId
}));
latestSecretVersions = (await getLatestSecretVersionIds({
secretIds
}))
.map((s) => ({
newSecretVersion: s.versionId
}));
}
action = await new Action({

@ -1,11 +1,11 @@
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import {
Secret,
Secret,
ISecret
} from '../../models';
import {
SecretSnapshot,
SecretSnapshot,
SecretVersion,
ISecretVersion
} from '../models';
@ -18,24 +18,24 @@ import {
* @param {String} obj.workspaceId
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot
*/
const takeSecretSnapshotHelper = async ({
const takeSecretSnapshotHelper = async ({
workspaceId
}: {
workspaceId: string;
}) => {
let secretSnapshot;
try {
const secretIds = (await Secret.find({
workspace: workspaceId
}, '_id')).map((s) => s._id);
const latestSecretVersions = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds
}
$match: {
secret: {
$in: secretIds
}
}
},
{
@ -48,14 +48,14 @@ import {
{
$sort: { version: -1 }
}
])
])
.exec())
.map((s) => s.versionId);
const latestSecretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId
}).sort({ version: -1 });
secretSnapshot = await new SecretSnapshot({
workspace: workspaceId,
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
@ -66,7 +66,7 @@ import {
Sentry.captureException(err);
throw new Error('Failed to take a secret snapshot');
}
return secretSnapshot;
}
@ -87,9 +87,9 @@ const addSecretVersionsHelper = async ({
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to add secret versions');
throw new Error(`Failed to add secret versions [err=${err}]`);
}
return newSecretVersions;
}
@ -120,39 +120,39 @@ const markDeletedSecretVersionsHelper = async ({
const initSecretVersioningHelper = async () => {
try {
await Secret.updateMany(
await Secret.updateMany(
{ version: { $exists: false } },
{ $set: { version: 1 } }
);
const unversionedSecrets: ISecret[] = await Secret.aggregate([
{
$lookup: {
from: 'secretversions',
localField: '_id',
foreignField: 'secret',
as: 'versions',
},
},
{
$match: {
versions: { $size: 0 },
},
},
]);
if (unversionedSecrets.length > 0) {
await addSecretVersionsHelper({
secretVersions: unversionedSecrets.map((s, idx) => ({
...s,
secret: s._id,
version: s.version ? s.version : 1,
isDeleted: false,
workspace: s.workspace,
environment: s.environment
}))
});
}
const unversionedSecrets: ISecret[] = await Secret.aggregate([
{
$lookup: {
from: 'secretversions',
localField: '_id',
foreignField: 'secret',
as: 'versions',
},
},
{
$match: {
versions: { $size: 0 },
},
},
]);
if (unversionedSecrets.length > 0) {
await addSecretVersionsHelper({
secretVersions: unversionedSecrets.map((s, idx) => ({
...s,
secret: s._id,
version: s.version ? s.version : 1,
isDeleted: false,
workspace: s.workspace,
environment: s.environment
}))
});
}
} catch (err) {
Sentry.setUser(null);
@ -162,7 +162,7 @@ const initSecretVersioningHelper = async () => {
}
export {
takeSecretSnapshotHelper,
takeSecretSnapshotHelper,
addSecretVersionsHelper,
markDeletedSecretVersionsHelper,
initSecretVersioningHelper

@ -0,0 +1,110 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { SecretVersion } from '../models';
/**
* Return latest secret versions for secrets with ids [secretIds]
* @param {Object} obj
* @param {Object} obj.secretIds = ids of secrets to get latest versions for
* @returns
*/
const getLatestSecretVersionIds = async ({
secretIds
}: {
secretIds: Types.ObjectId[];
}) => {
interface LatestSecretVersionId {
_id: Types.ObjectId;
version: number;
versionId: Types.ObjectId;
}
let latestSecretVersionIds: LatestSecretVersionId[];
try {
latestSecretVersionIds = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds
}
}
},
{
$group: {
_id: '$secret',
version: { $max: '$version' },
versionId: { $max: '$_id' } // id of latest secret version
}
},
{
$sort: { version: -1 }
}
])
.exec());
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get latest secret versions');
}
return latestSecretVersionIds;
}
/**
* Return latest [n] secret versions for secrets with ids [secretIds]
* @param {Object} obj
* @param {Object} obj.secretIds = ids of secrets to get latest versions for
* @param {Number} obj.n - number of latest secret versions to return for each secret
* @returns
*/
const getLatestNSecretSecretVersionIds = async ({
secretIds,
n
}: {
secretIds: Types.ObjectId[];
n: number;
}) => {
// TODO: optimize query
let latestNSecretVersions;
try {
latestNSecretVersions = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds,
},
},
},
{
$sort: { version: -1 },
},
{
$group: {
_id: "$secret",
versions: { $push: "$$ROOT" },
},
},
{
$project: {
_id: 0,
secret: "$_id",
versions: { $slice: ["$versions", n] },
},
}
]));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get latest n secret versions');
}
return latestNSecretVersions;
}
export {
getLatestSecretVersionIds,
getLatestNSecretSecretVersionIds
}

@ -2,31 +2,18 @@ import { Schema, model, Types } from 'mongoose';
import {
SECRET_SHARED,
SECRET_PERSONAL,
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD
} from '../../variables';
/**
* TODO:
* 1. Modify SecretVersion to also contain XX
* - type
* - user
* - environment
* 2. Modify SecretSnapshot to point to arrays of SecretVersion
*/
export interface ISecretVersion {
_id?: Types.ObjectId;
secret: Types.ObjectId;
version: number;
_id: Types.ObjectId;
secret: Types.ObjectId;
version: number;
workspace: Types.ObjectId; // new
type: string; // new
user: Types.ObjectId; // new
environment: string; // new
isDeleted: boolean;
secretKeyCiphertext: string;
isDeleted: boolean;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretKeyHash: string;
@ -37,17 +24,17 @@ export interface ISecretVersion {
}
const secretVersionSchema = new Schema<ISecretVersion>(
{
secret: { // could be deleted
type: Schema.Types.ObjectId,
ref: 'Secret',
required: true
},
version: {
type: Number,
default: 1,
required: true
},
{
secret: { // could be deleted
type: Schema.Types.ObjectId,
ref: 'Secret',
required: true
},
version: {
type: Number,
default: 1,
required: true
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
@ -65,15 +52,14 @@ const secretVersionSchema = new Schema<ISecretVersion>(
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
isDeleted: {
type: Boolean,
default: false,
required: true
},
secretKeyCiphertext: {
isDeleted: { // consider removing field
type: Boolean,
default: false,
required: true
},
secretKeyCiphertext: {
type: String,
required: true
},
@ -86,8 +72,7 @@ const secretVersionSchema = new Schema<ISecretVersion>(
required: true
},
secretKeyHash: {
type: String,
required: true
type: String
},
secretValueCiphertext: {
type: String,
@ -102,13 +87,12 @@ const secretVersionSchema = new Schema<ISecretVersion>(
required: true
},
secretValueHash: {
type: String,
required: true
type: String
}
},
{
timestamps: true
}
},
{
timestamps: true
}
);
const SecretVersion = model<ISecretVersion>('SecretVersion', secretVersionSchema);

@ -5,7 +5,7 @@ import {
requireSecretAuth,
validateRequest
} from '../../../middleware';
import { query, param } from 'express-validator';
import { query, param, body } from 'express-validator';
import { secretController } from '../../controllers/v1';
import { ADMIN, MEMBER } from '../../../variables';
@ -24,4 +24,17 @@ router.get(
secretController.getSecretVersions
);
router.post(
'/:secretId/secret-versions/rollback',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('secretId').exists().trim(),
body('version').exists().isInt(),
secretController.rollbackSecretVersion
);
export default router;

@ -7,7 +7,7 @@ import {
requireAuth,
validateRequest
} from '../../../middleware';
import { param } from 'express-validator';
import { param, body } from 'express-validator';
import { ADMIN, MEMBER } from '../../../variables';
import { secretSnapshotController } from '../../controllers/v1';

@ -5,7 +5,7 @@ import {
requireWorkspaceAuth,
validateRequest
} from '../../../middleware';
import { param, query } from 'express-validator';
import { param, query, body } from 'express-validator';
import { ADMIN, MEMBER } from '../../../variables';
import { workspaceController } from '../../controllers/v1';
@ -37,6 +37,20 @@ router.get(
workspaceController.getWorkspaceSecretSnapshotsCount
);
router.post(
'/:workspaceId/secret-snapshots/rollback',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('workspaceId').exists().trim(),
body('version').exists().isInt(),
validateRequest,
workspaceController.rollbackWorkspaceSecretSnapshot
);
router.get(
'/:workspaceId/logs',
requireAuth({

@ -72,7 +72,7 @@ const getSecretsHelper = async ({
try {
const key = await getKey({ workspaceId });
const secrets = await Secret.find({
workspaceId,
workspace: workspaceId,
environment,
type: SECRET_SHARED
});
@ -84,7 +84,7 @@ const getSecretsHelper = async ({
tag: secret.secretKeyTag,
key
});
const secretValue = decryptSymmetric({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,

@ -7,8 +7,6 @@ import {
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
import { BotService } from '../services';
import {
ENV_DEV,
EVENT_PUSH_SECRETS,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY
} from '../variables';
@ -36,11 +34,13 @@ interface Update {
const handleOAuthExchangeHelper = async ({
workspaceId,
integration,
code
code,
environment
}: {
workspaceId: string;
integration: string;
code: string;
environment: string;
}) => {
let action;
let integrationAuth;
@ -102,9 +102,9 @@ const handleOAuthExchangeHelper = async ({
// initialize new integration after exchange
await new Integration({
workspace: workspaceId,
environment: ENV_DEV,
isActive: false,
app: null,
environment,
integration,
integrationAuth: integrationAuth._id
}).save();

@ -6,7 +6,9 @@ const apiLimiter = rateLimit({
max: 450,
standardHeaders: true,
legacyHeaders: false,
skip: (request) => request.path === '/healthcheck'
skip: (request) => {
return request.path === '/healthcheck' || request.path === '/api/status'
}
});
// 5 requests per hour

@ -3,6 +3,7 @@ import { Types } from 'mongoose';
import {
Secret,
ISecret,
Membership
} from '../models';
import {
EESecretService,
@ -20,6 +21,46 @@ import {
ACTION_READ_SECRETS
} from '../variables';
/**
* Validate that user with id [userId] can modify secrets with ids [secretIds]
* @param {Object} obj
* @param {Object} obj.userId - id of user to validate
* @param {Object} obj.secretIds - secret ids
* @returns {Secret[]} secrets
*/
const validateSecrets = async ({
userId,
secretIds
}: {
userId: string;
secretIds: string[];
}) =>{
let secrets;
try {
secrets = await Secret.find({
_id: {
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
}
});
const workspaceIdsSet = new Set((await Membership.find({
user: userId
}, 'workspace'))
.map((m) => m.workspace.toString()));
secrets.forEach((secret: ISecret) => {
if (!workspaceIdsSet.has(secret.workspace.toString())) {
throw new Error('Failed to validate secret');
}
});
} catch (err) {
throw new Error('Failed to validate secrets');
}
return secrets;
}
interface V1PushSecret {
ciphertextKey: string;
ivKey: string;
@ -187,6 +228,7 @@ const v1PushSecrets = async ({
}) => {
const newSecret = newSecretsObj[`${type}-${secretKeyHash}`];
return ({
_id: new Types.ObjectId(),
secret: _id,
version: version ? version + 1 : 1,
workspace: new Types.ObjectId(workspaceId),
@ -258,6 +300,7 @@ const v1PushSecrets = async ({
secretValueTag,
secretValueHash
}) => ({
_id: new Types.ObjectId(),
secret: _id,
version,
workspace,
@ -280,7 +323,7 @@ const v1PushSecrets = async ({
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId
})
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
@ -527,6 +570,7 @@ const v1PushSecrets = async ({
environment: string;
}): Promise<ISecret[]> => {
let secrets: any; // TODO: FIX any
try {
// get shared workspace secrets
const sharedSecrets = await Secret.find({
@ -655,6 +699,7 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
};
export {
validateSecrets,
v1PushSecrets,
v2PushSecrets,
pullSecrets,

@ -9,14 +9,9 @@ import {
INTEGRATION_GITHUB,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_GITHUB_API_URL
INTEGRATION_NETLIFY_API_URL
} from '../variables';
interface GitHubApp {
name: string;
}
/**
* Return list of names of apps for integration named [integration]
* @param {Object} obj
@ -47,6 +42,7 @@ const getApps = async ({
break;
case INTEGRATION_VERCEL:
apps = await getAppsVercel({
integrationAuth,
accessToken
});
break;
@ -110,17 +106,28 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
* @returns {Object[]} apps - names of Vercel apps
* @returns {String} apps.name - name of Vercel app
*/
const getAppsVercel = async ({ accessToken }: { accessToken: string }) => {
const getAppsVercel = async ({
integrationAuth,
accessToken
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
let apps;
try {
const res = (
await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
headers: {
Authorization: `Bearer ${accessToken}`
},
...( integrationAuth?.teamId ? {
params: {
teamId: integrationAuth.teamId
}
} : {})
})
).data;
apps = res.projects.map((a: any) => ({
name: a.name
}));

@ -8,8 +8,7 @@ import {
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITHUB_API_URL
INTEGRATION_GITHUB_TOKEN_URL
} from '../variables';
import {
SITE_URL,

@ -1,4 +1,4 @@
import axios from 'axios';
import axios, { AxiosError } from 'axios';
import * as Sentry from '@sentry/node';
import { Octokit } from '@octokit/rest';
// import * as sodium from 'libsodium-wrappers';
@ -12,14 +12,10 @@ import {
INTEGRATION_GITHUB,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_GITHUB_API_URL
INTEGRATION_NETLIFY_API_URL
} from '../variables';
import { access, appendFile } from 'fs';
// TODO: need a helper function in the future to handle integration
// envar priorities (i.e. prioritize secrets within integration or those on Infisical)
/**
* Sync/push [secrets] to [app] in integration named [integration]
* @param {Object} obj
@ -53,6 +49,7 @@ const syncSecrets = async ({
case INTEGRATION_VERCEL:
await syncSecretsVercel({
integration,
integrationAuth,
secrets,
accessToken
});
@ -139,14 +136,15 @@ const syncSecretsHeroku = async ({
*/
const syncSecretsVercel = async ({
integration,
integrationAuth,
secrets,
accessToken
}: {
integration: IIntegration,
integrationAuth: IIntegrationAuth,
secrets: any;
accessToken: string;
}) => {
interface VercelSecret {
id?: string;
type: string;
@ -156,129 +154,135 @@ const syncSecretsVercel = async ({
}
try {
// Get all (decrypted) secrets back from Vercel in
// decrypted format
const params = new URLSearchParams({
decrypt: "true"
});
const res = (await Promise.all((await axios.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
{
// Get all (decrypted) secrets back from Vercel in
// decrypted format
const params: { [key: string]: string } = {
decrypt: 'true',
...( integrationAuth?.teamId ? {
teamId: integrationAuth.teamId
} : {})
}
const res = (await Promise.all((await axios.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
))
.data
.envs
.filter((secret: VercelSecret) => secret.target.includes(integration.target))
.map(async (secret: VercelSecret) => (await axios.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
{
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}`,
{
}
)).data)
)).reduce((obj: any, secret: any) => ({
...obj,
[secret.key]: secret
}), {});
const updateSecrets: VercelSecret[] = [];
const deleteSecrets: VercelSecret[] = [];
const newSecrets: VercelSecret[] = [];
// Identify secrets to create
Object.keys(secrets).map((key) => {
if (!(key in res)) {
// case: secret has been created
newSecrets.push({
key: key,
value: secrets[key],
type: 'encrypted',
target: [integration.target]
});
}
});
// Identify secrets to update and delete
Object.keys(res).map((key) => {
if (key in secrets) {
if (res[key].value !== secrets[key]) {
// case: secret value has changed
updateSecrets.push({
id: res[key].id,
key: key,
value: secrets[key],
type: 'encrypted',
target: [integration.target]
});
}
} else {
// case: secret has been deleted
deleteSecrets.push({
id: res[key].id,
key: key,
value: res[key].value,
type: 'encrypted',
target: [integration.target],
});
}
});
// Sync/push new secrets
if (newSecrets.length > 0) {
await axios.post(
`${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`,
newSecrets,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
}
// Sync/push updated secrets
if (updateSecrets.length > 0) {
updateSecrets.forEach(async (secret: VercelSecret) => {
const {
id,
...updatedSecret
} = secret;
await axios.patch(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
updatedSecret,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
});
}
// Delete secrets
if (deleteSecrets.length > 0) {
deleteSecrets.forEach(async (secret: VercelSecret) => {
await axios.delete(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
)).data)
)).reduce((obj: any, secret: any) => ({
...obj,
[secret.key]: secret
}), {});
const updateSecrets: VercelSecret[] = [];
const deleteSecrets: VercelSecret[] = [];
const newSecrets: VercelSecret[] = [];
// Identify secrets to create
Object.keys(secrets).map((key) => {
if (!(key in res)) {
// case: secret has been created
newSecrets.push({
key: key,
value: secrets[key],
type: 'encrypted',
target: [integration.target]
});
}
});
// Identify secrets to update and delete
Object.keys(res).map((key) => {
if (key in secrets) {
if (res[key].value !== secrets[key]) {
// case: secret value has changed
updateSecrets.push({
id: res[key].id,
key: key,
value: secrets[key],
type: 'encrypted',
target: [integration.target]
});
}
} else {
// case: secret has been deleted
deleteSecrets.push({
id: res[key].id,
key: key,
value: res[key].value,
type: 'encrypted',
target: [integration.target],
});
}
});
// Sync/push new secrets
if (newSecrets.length > 0) {
await axios.post(
`${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`,
newSecrets,
{
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
}
// Sync/push updated secrets
if (updateSecrets.length > 0) {
updateSecrets.forEach(async (secret: VercelSecret) => {
const {
id,
...updatedSecret
} = secret;
await axios.patch(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
updatedSecret,
{
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
});
}
// Delete secrets
if (deleteSecrets.length > 0) {
deleteSecrets.forEach(async (secret: VercelSecret) => {
await axios.delete(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
{
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
});
}
}
);
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Vercel');
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Vercel');
}
}
@ -302,188 +306,188 @@ const syncSecretsNetlify = async ({
}) => {
try {
interface NetlifyValue {
id?: string;
context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production',
value: string;
}
interface NetlifySecret {
key: string;
values: NetlifyValue[];
}
interface NetlifySecretsRes {
[index: string]: NetlifySecret;
}
const getParams = new URLSearchParams({
context_name: 'all', // integration.context or all
site_id: integration.siteId
});
const res = (await axios.get(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
{
params: getParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
))
.data
.reduce((obj: any, secret: any) => ({
...obj,
[secret.key]: secret
}), {});
const newSecrets: NetlifySecret[] = []; // createEnvVars
const deleteSecrets: string[] = []; // deleteEnvVar
const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue
const updateSecrets: NetlifySecret[] = []; // setEnvVarValue
interface NetlifyValue {
id?: string;
context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production',
value: string;
}
interface NetlifySecret {
key: string;
values: NetlifyValue[];
}
interface NetlifySecretsRes {
[index: string]: NetlifySecret;
}
const getParams = new URLSearchParams({
context_name: 'all', // integration.context or all
site_id: integration.siteId
});
const res = (await axios.get(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
{
params: getParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
))
.data
.reduce((obj: any, secret: any) => ({
...obj,
[secret.key]: secret
}), {});
const newSecrets: NetlifySecret[] = []; // createEnvVars
const deleteSecrets: string[] = []; // deleteEnvVar
const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue
const updateSecrets: NetlifySecret[] = []; // setEnvVarValue
// identify secrets to create and update
Object.keys(secrets).map((key) => {
if (!(key in res)) {
// case: Infisical secret does not exist in Netlify -> create secret
newSecrets.push({
key,
values: [{
value: secrets[key],
context: integration.context
}]
});
} else {
// case: Infisical secret exists in Netlify
const contexts = res[key].values
.reduce((obj: any, value: NetlifyValue) => ({
...obj,
[value.context]: value
}), {});
if (integration.context in contexts) {
// case: Netlify secret value exists in integration context
if (secrets[key] !== contexts[integration.context].value) {
// case: Infisical and Netlify secret values are different
// -> update Netlify secret context and value
updateSecrets.push({
key,
values: [{
context: integration.context,
value: secrets[key]
}]
});
}
} else {
// case: Netlify secret value does not exist in integration context
// -> add the new Netlify secret context and value
updateSecrets.push({
key,
values: [{
context: integration.context,
value: secrets[key]
}]
});
}
}
})
// identify secrets to delete
// TODO: revise (patch case where 1 context was deleted but others still there
Object.keys(res).map((key) => {
// loop through each key's context
if (!(key in secrets)) {
// case: Netlify secret does not exist in Infisical
const numberOfValues = res[key].values.length;
res[key].values.forEach((value: NetlifyValue) => {
if (value.context === integration.context) {
if (numberOfValues <= 1) {
// case: Netlify secret value has less than 1 context -> delete secret
deleteSecrets.push(key);
} else {
// case: Netlify secret value has more than 1 context -> delete secret value context
deleteSecretValues.push({
key,
values: [{
id: value.id,
context: integration.context,
value: value.value
}]
});
}
}
});
}
});
// identify secrets to create and update
Object.keys(secrets).map((key) => {
if (!(key in res)) {
// case: Infisical secret does not exist in Netlify -> create secret
newSecrets.push({
key,
values: [{
value: secrets[key],
context: integration.context
}]
});
} else {
// case: Infisical secret exists in Netlify
const contexts = res[key].values
.reduce((obj: any, value: NetlifyValue) => ({
...obj,
[value.context]: value
}), {});
if (integration.context in contexts) {
// case: Netlify secret value exists in integration context
if (secrets[key] !== contexts[integration.context].value) {
// case: Infisical and Netlify secret values are different
// -> update Netlify secret context and value
updateSecrets.push({
key,
values: [{
context: integration.context,
value: secrets[key]
}]
});
}
} else {
// case: Netlify secret value does not exist in integration context
// -> add the new Netlify secret context and value
updateSecrets.push({
key,
values: [{
context: integration.context,
value: secrets[key]
}]
});
}
}
})
// identify secrets to delete
// TODO: revise (patch case where 1 context was deleted but others still there
Object.keys(res).map((key) => {
// loop through each key's context
if (!(key in secrets)) {
// case: Netlify secret does not exist in Infisical
const numberOfValues = res[key].values.length;
res[key].values.forEach((value: NetlifyValue) => {
if (value.context === integration.context) {
if (numberOfValues <= 1) {
// case: Netlify secret value has less than 1 context -> delete secret
deleteSecrets.push(key);
} else {
// case: Netlify secret value has more than 1 context -> delete secret value context
deleteSecretValues.push({
key,
values: [{
id: value.id,
context: integration.context,
value: value.value
}]
});
}
}
});
}
});
const syncParams = new URLSearchParams({
site_id: integration.siteId
});
const syncParams = new URLSearchParams({
site_id: integration.siteId
});
if (newSecrets.length > 0) {
await axios.post(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
newSecrets,
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
}
if (newSecrets.length > 0) {
await axios.post(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
newSecrets,
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
}
if (updateSecrets.length > 0) {
updateSecrets.forEach(async (secret: NetlifySecret) => {
await axios.patch(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
{
context: secret.values[0].context,
value: secret.values[0].value
},
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
});
}
if (updateSecrets.length > 0) {
updateSecrets.forEach(async (secret: NetlifySecret) => {
await axios.patch(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
{
context: secret.values[0].context,
value: secret.values[0].value
},
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
});
}
if (deleteSecrets.length > 0) {
deleteSecrets.forEach(async (key: string) => {
await axios.delete(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`,
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
});
}
if (deleteSecrets.length > 0) {
deleteSecrets.forEach(async (key: string) => {
await axios.delete(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`,
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
});
}
if (deleteSecretValues.length > 0) {
deleteSecretValues.forEach(async (secret: NetlifySecret) => {
await axios.delete(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`,
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
});
}
if (deleteSecretValues.length > 0) {
deleteSecretValues.forEach(async (secret: NetlifySecret) => {
await axios.delete(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`,
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Heroku');
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Heroku');
}
}

@ -8,6 +8,7 @@ import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizati
import requireServiceTokenAuth from './requireServiceTokenAuth';
import requireServiceTokenDataAuth from './requireServiceTokenDataAuth';
import requireSecretAuth from './requireSecretAuth';
import requireSecretsAuth from './requireSecretsAuth';
import validateRequest from './validateRequest';
export {
@ -21,5 +22,6 @@ export {
requireServiceTokenAuth,
requireServiceTokenDataAuth,
requireSecretAuth,
requireSecretsAuth,
validateRequest
};

@ -5,6 +5,9 @@ import {
validateMembership
} from '../helpers/membership';
// note: used for old /v1/secret and /v2/secret routes.
// newer /v2/secrets routes use [requireSecretsAuth] middleware
/**
* Validate if user on request has proper membership to modify secret.
* @param {Object} obj
@ -34,7 +37,7 @@ const requireSecretAuth = ({
acceptedRoles
});
req.secret = secret as any;
req._secret = secret;
next();
} catch (err) {

@ -0,0 +1,49 @@
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors';
import { Secret, Membership } from '../models';
import { validateSecrets } from '../helpers/secret';
// TODO: make this work for delete route
const requireSecretsAuth = ({
acceptedRoles
}: {
acceptedRoles: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
let secrets;
try {
if (Array.isArray(req.body.secrets)) {
// case: validate multiple secrets
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: req.body.secrets.map((s: any) => s.id)
});
} else if (typeof req.body.secrets === 'object') { // change this to check for object
// case: validate 1 secret
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: req.body.secrets.id
});
} else if (Array.isArray(req.body.secretIds)) {
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: req.body.secretIds
});
} else if (typeof req.body.secretIds === 'string') {
// case: validate secretIds
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: [req.body.secretIds]
});
}
req.secrets = secrets;
return next();
} catch (err) {
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret(s)' }));
}
}
}
export default requireSecretsAuth;

@ -1,9 +1,5 @@
import { Schema, model, Types } from 'mongoose';
import {
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@ -13,7 +9,7 @@ import {
export interface IIntegration {
_id: Types.ObjectId;
workspace: Types.ObjectId;
environment: 'dev' | 'test' | 'staging' | 'prod';
environment: string;
isActive: boolean;
app: string;
target: string;
@ -32,7 +28,6 @@ const integrationSchema = new Schema<IIntegration>(
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
isActive: {

@ -2,10 +2,6 @@ import { Schema, model, Types } from 'mongoose';
import {
SECRET_SHARED,
SECRET_PERSONAL,
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD
} from '../variables';
export interface ISecret {
@ -53,7 +49,6 @@ const secretSchema = new Schema<ISecret>(
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
secretKeyCiphertext: {
@ -69,8 +64,7 @@ const secretSchema = new Schema<ISecret>(
required: true
},
secretKeyHash: {
type: String,
required: true
type: String
},
secretValueCiphertext: {
type: String,
@ -85,8 +79,7 @@ const secretSchema = new Schema<ISecret>(
required: true
},
secretValueHash: {
type: String,
required: true
type: String
},
secretCommentCiphertext: {
type: String,

@ -1,7 +1,4 @@
import { Schema, model, Types } from 'mongoose';
import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables';
// TODO: deprecate
export interface IServiceToken {
_id: Types.ObjectId;
name: string;
@ -33,7 +30,6 @@ const serviceTokenSchema = new Schema<IServiceToken>(
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
expiresAt: {

@ -4,6 +4,10 @@ export interface IWorkspace {
_id: Types.ObjectId;
name: string;
organization: Types.ObjectId;
environments: Array<{
name: string;
slug: string;
}>;
}
const workspaceSchema = new Schema<IWorkspace>({
@ -15,7 +19,33 @@ const workspaceSchema = new Schema<IWorkspace>({
type: Schema.Types.ObjectId,
ref: 'Organization',
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);

@ -0,0 +1,5 @@
import healthCheck from './status';
export {
healthCheck
}

@ -0,0 +1,15 @@
import express, { Request, Response } from 'express';
const router = express.Router();
router.get(
'/status',
(req: Request, res: Response) => {
res.status(200).json({
date: new Date(),
message: 'Ok',
})
}
);
export default router

@ -0,0 +1,57 @@
import express, { Response, Request } from 'express';
const router = express.Router();
import { body, param } from 'express-validator';
import { environmentController } from '../../controllers/v2';
import {
requireAuth,
requireWorkspaceAuth,
validateRequest,
} from '../../middleware';
import { ADMIN, MEMBER } from '../../variables';
router.post(
'/:workspaceId/environments',
requireAuth({
acceptedAuthModes: ['jwt'],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
}),
param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(),
body('environmentName').exists().trim(),
validateRequest,
environmentController.createWorkspaceEnvironment
);
router.put(
'/:workspaceId/environments',
requireAuth({
acceptedAuthModes: ['jwt'],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
}),
param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(),
body('environmentName').exists().trim(),
body('oldEnvironmentSlug').exists().trim(),
validateRequest,
environmentController.renameWorkspaceEnvironment
);
router.delete(
'/:workspaceId/environments',
requireAuth({
acceptedAuthModes: ['jwt'],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
}),
param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(),
validateRequest,
environmentController.deleteWorkspaceEnvironment
);
export default router;

@ -1,11 +1,15 @@
import secret from './secret';
import secret from './secret'; // stop-supporting
import secrets from './secrets';
import workspace from './workspace';
import serviceTokenData from './serviceTokenData';
import apiKeyData from './apiKeyData';
import environment from "./environment"
export {
secret,
secrets,
workspace,
serviceTokenData,
apiKeyData
}
apiKeyData,
environment
}

@ -1,18 +1,21 @@
import express, { Request, Response } from 'express';
import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware';
import express from 'express';
import {
requireAuth,
requireWorkspaceAuth,
requireSecretAuth,
validateRequest
} from '../../middleware';
import { body, param, query } from 'express-validator';
import { ADMIN, MEMBER } from '../../variables';
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
import { secretController } from '../../controllers/v2';
import { fetchAllSecrets } from '../../controllers/v2/secretController';
// note to devs: stop supporting
const router = express.Router();
/**
* Create many secrets for a given workspace and environmentName
*/
router.post(
'/batch-create/workspace/:workspaceId/environment/:environmentName',
'/batch-create/workspace/:workspaceId/environment/:environment',
requireAuth({
acceptedAuthModes: ['jwt']
}),
@ -20,15 +23,29 @@ router.post(
acceptedRoles: [ADMIN, MEMBER]
}),
param('workspaceId').exists().isMongoId().trim(),
param('environmentName').exists().trim(),
param('environment').exists().trim(),
body('secrets').exists().isArray().custom((value) => value.every((item: CreateSecretRequestBody) => typeof item === 'object')),
body('channel'),
validateRequest,
secretController.batchCreateSecrets
secretController.createSecrets
);
router.post(
'/workspace/:workspaceId/environment/:environment',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('workspaceId').exists().isMongoId().trim(),
param('environment').exists().trim(),
body('secret').exists().isObject(),
body('channel'),
validateRequest,
secretController.createSecret
);
/**
* Get all secrets for a given environment and workspace id
*/
router.get(
'/workspace/:workspaceId',
param('workspaceId').exists().trim(),
@ -39,13 +56,23 @@ router.get(
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
query('channel'),
validateRequest,
fetchAllSecrets
secretController.getSecrets
);
router.get(
'/:secretId',
requireAuth({
acceptedAuthModes: ['jwt', 'serviceToken']
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
validateRequest,
secretController.getSecret
);
/**
* Batch delete secrets in a given workspace and environment name
*/
router.delete(
'/batch/workspace/:workspaceId/environment/:environmentName',
requireAuth({
@ -58,13 +85,22 @@ router.delete(
acceptedRoles: [ADMIN, MEMBER]
}),
validateRequest,
secretController.batchDeleteSecrets
secretController.deleteSecrets
);
router.delete(
'/:secretId',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('secretId').isMongoId(),
validateRequest,
secretController.deleteSecret
);
/**
* Apply modifications to many existing secrets in a given workspace and environment
*/
router.patch(
'/batch-modify/workspace/:workspaceId/environment/:environmentName',
requireAuth({
@ -77,7 +113,23 @@ router.patch(
acceptedRoles: [ADMIN, MEMBER]
}),
validateRequest,
secretController.batchModifySecrets
secretController.updateSecrets
);
router.patch(
'/workspace/:workspaceId/environment/:environmentName',
requireAuth({
acceptedAuthModes: ['jwt']
}),
body('secret').isObject(),
param('workspaceId').exists().isMongoId().trim(),
param('environmentName').exists().trim(),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
validateRequest,
secretController.updateSecret
);
export default router;

@ -0,0 +1,157 @@
import express from 'express';
const router = express.Router();
import {
requireAuth,
requireWorkspaceAuth,
requireSecretsAuth,
validateRequest
} from '../../middleware';
import { query, check, body } from 'express-validator';
import { secretsController } from '../../controllers/v2';
import {
ADMIN,
MEMBER,
SECRET_PERSONAL,
SECRET_SHARED
} from '../../variables';
router.post(
'/',
body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim(),
body('secrets')
.exists()
.custom((value) => {
if (Array.isArray(value)) {
// case: create multiple secrets
if (value.length === 0) throw new Error('secrets cannot be an empty array')
for (const secret of value) {
if (
!secret.type ||
!(secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED) ||
!secret.secretKeyCiphertext ||
!secret.secretKeyIV ||
!secret.secretKeyTag ||
!secret.secretValueCiphertext ||
!secret.secretValueIV ||
!secret.secretValueTag
) {
throw new Error('secrets array must contain objects that have required secret properties');
}
}
} else if (typeof value === 'object') {
// case: update 1 secret
if (
!value.type ||
!(value.type === SECRET_PERSONAL || value.type === SECRET_SHARED) ||
!value.secretKeyCiphertext ||
!value.secretKeyIV ||
!value.secretKeyTag ||
!value.secretValueCiphertext ||
!value.secretValueIV ||
!value.secretValueTag
) {
throw new Error('secrets object is missing required secret properties');
}
} else {
throw new Error('secrets must be an object or an array of objects')
}
return true;
}),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
}),
secretsController.createSecrets
);
router.get(
'/',
query('workspaceId').exists().trim(),
query('environment').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'serviceToken']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
location: 'query'
}),
secretsController.getSecrets
);
router.patch(
'/',
body('secrets')
.exists()
.custom((value) => {
if (Array.isArray(value)) {
// case: update multiple secrets
if (value.length === 0) throw new Error('secrets cannot be an empty array')
for (const secret of value) {
if (
!secret.id
) {
throw new Error('Each secret must contain a ID property');
}
}
} else if (typeof value === 'object') {
// case: update 1 secret
if (
!value.id
) {
throw new Error('secret must contain a ID property');
}
} else {
throw new Error('secrets must be an object or an array of objects')
}
return true;
}),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
secretsController.updateSecrets
);
router.delete(
'/',
body('secretIds')
.exists()
.custom((value) => {
// case: delete 1 secret
if (typeof value === 'string') return true;
if (Array.isArray(value)) {
// case: delete multiple secrets
if (value.length === 0) throw new Error('secrets cannot be an empty array');
return value.every((id: string) => typeof id === 'string')
}
throw new Error('secretIds must be a string or an array of strings');
})
.not()
.isEmpty(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
secretsController.deleteSecrets
);
export default router;

@ -11,10 +11,6 @@ import {
setIntegrationAuthAccessHelper,
} from '../helpers/integration';
import { exchangeCode } from '../integrations';
import {
ENV_DEV,
EVENT_PUSH_SECRETS
} from '../variables';
// should sync stuff be here too? Probably.
// TODO: move bot functions to IntegrationService.
@ -32,22 +28,26 @@ class IntegrationService {
* - Create bot sequence for integration
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.environment - workspace environment
* @param {String} obj.integration - name of integration
* @param {String} obj.code - code
*/
static async handleOAuthExchange({
workspaceId,
integration,
code
code,
environment
}: {
workspaceId: string;
integration: string;
code: string;
environment: string;
}) {
await handleOAuthExchangeHelper({
workspaceId,
integration,
code
code,
environment
});
}

@ -7,12 +7,12 @@ import {
} from '../config';
import { getLogger } from '../utils/logger';
if(TELEMETRY_ENABLED){
if(!TELEMETRY_ENABLED){
getLogger("backend-main").info([
"",
"Infisical collects telemetry data about general usage.",
"The data helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth for investors as we support Infisical as open-source software.",
"To opt out of telemetry, you can set `TELEMETRY_ENABLED=false` within the environment variables",
"To improve, Infisical collects telemetry data about general usage.",
"This helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth as we support Infisical as open-source software.",
"To opt into telemetry, you can set `TELEMETRY_ENABLED=true` within the environment variables.",
].join('\n'))
}

@ -28,7 +28,13 @@ if (SMTP_SECURE) {
}
break;
default:
mailOpts.secure = true;
if (SMTP_HOST.includes('amazonaws.com')) {
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
} else {
mailOpts.secure = true;
}
break;
}
}

@ -1,4 +1,5 @@
import * as express from 'express';
import { ISecret } from '../../models';
// TODO: fix (any) types
declare global {
@ -12,7 +13,8 @@ declare global {
integration: any;
integrationAuth: any;
bot: any;
secret: any;
_secret: any;
secrets: any;
secretSnapshot: any;
serviceToken: any;
accessToken: any;

@ -19,7 +19,6 @@ import {
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_GITHUB_API_URL,
INTEGRATION_OPTIONS
} from './integration';
import {
@ -66,7 +65,6 @@ export {
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_GITHUB_API_URL,
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS,
ACTION_ADD_SECRETS,

@ -11,10 +11,10 @@ const INTEGRATION_VERCEL = 'vercel';
const INTEGRATION_NETLIFY = 'netlify';
const INTEGRATION_GITHUB = 'github';
const INTEGRATION_SET = new Set([
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
]);
// integration types
@ -23,22 +23,21 @@ const INTEGRATION_OAUTH2 = 'oauth2';
// integration oauth endpoints
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
const INTEGRATION_VERCEL_TOKEN_URL =
'https://api.vercel.com/v2/oauth/access_token';
'https://api.vercel.com/v2/oauth/access_token';
const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token';
const INTEGRATION_GITHUB_TOKEN_URL =
'https://github.com/login/oauth/access_token';
'https://github.com/login/oauth/access_token';
// integration apps endpoints
const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com';
const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com';
const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com';
const INTEGRATION_GITHUB_API_URL = 'https://api.github.com';
const INTEGRATION_OPTIONS = [
{
name: 'Heroku',
slug: 'heroku',
image: 'Heroku',
image: 'Heroku',
isAvailable: true,
type: 'oauth2',
clientId: CLIENT_ID_HEROKU,
@ -47,8 +46,8 @@ const INTEGRATION_OPTIONS = [
{
name: 'Vercel',
slug: 'vercel',
image: 'Vercel',
isAvailable: false,
image: 'Vercel',
isAvailable: true,
type: 'vercel',
clientId: '',
clientSlug: CLIENT_SLUG_VERCEL,
@ -57,7 +56,7 @@ const INTEGRATION_OPTIONS = [
{
name: 'Netlify',
slug: 'netlify',
image: 'Netlify',
image: 'Netlify',
isAvailable: false,
type: 'oauth2',
clientId: CLIENT_ID_NETLIFY,
@ -66,17 +65,17 @@ const INTEGRATION_OPTIONS = [
{
name: 'GitHub',
slug: 'github',
image: 'GitHub',
image: 'GitHub',
isAvailable: false,
type: 'oauth2',
clientId: CLIENT_ID_GITHUB,
docsLink: ''
},
{
name: 'Google Cloud Platform',
slug: 'gcp',
image: 'Google Cloud Platform',
image: 'Google Cloud Platform',
isAvailable: false,
type: '',
clientId: '',
@ -85,7 +84,7 @@ const INTEGRATION_OPTIONS = [
{
name: 'Amazon Web Services',
slug: 'aws',
image: 'Amazon Web Services',
image: 'Amazon Web Services',
isAvailable: false,
type: '',
clientId: '',
@ -94,7 +93,7 @@ const INTEGRATION_OPTIONS = [
{
name: 'Microsoft Azure',
slug: 'azure',
image: 'Microsoft Azure',
image: 'Microsoft Azure',
isAvailable: false,
type: '',
clientId: '',
@ -103,7 +102,7 @@ const INTEGRATION_OPTIONS = [
{
name: 'Travis CI',
slug: 'travisci',
image: 'Travis CI',
image: 'Travis CI',
isAvailable: false,
type: '',
clientId: '',
@ -112,7 +111,7 @@ const INTEGRATION_OPTIONS = [
{
name: 'Circle CI',
slug: 'circleci',
image: 'Circle CI',
image: 'Circle CI',
isAvailable: false,
type: '',
clientId: '',
@ -121,19 +120,18 @@ const INTEGRATION_OPTIONS = [
]
export {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_GITHUB_API_URL,
INTEGRATION_OPTIONS
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_OPTIONS
};

22
backend/swagger.ts Normal file

@ -0,0 +1,22 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' });
const doc = {
info: {
title: 'Infisical API',
description: 'List of all available APIs that can be consumed',
},
host: ['https://infisical.com'],
securityDefinitions: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
}
};
const outputFile = './api-documentation.json';
const endpointsFiles = ['./src/app.ts'];
swaggerAutogen(outputFile, endpointsFiles, doc);

@ -16,10 +16,11 @@ import (
)
const (
FormatDotenv string = "dotenv"
FormatJson string = "json"
FormatCSV string = "csv"
FormatYaml string = "yaml"
FormatDotenv string = "dotenv"
FormatJson string = "json"
FormatCSV string = "csv"
FormatYaml string = "yaml"
FormatDotEnvExport string = "dotenv-export"
)
// exportCmd represents the export command
@ -31,8 +32,8 @@ var exportCmd = &cobra.Command{
Args: cobra.NoArgs,
PreRun: func(cmd *cobra.Command, args []string) {
toggleDebug(cmd, args)
util.RequireLogin()
util.RequireLocalWorkspaceFile()
// util.RequireLogin()
// util.RequireLocalWorkspaceFile()
},
Run: func(cmd *cobra.Command, args []string) {
envName, err := cmd.Flags().GetString("env")
@ -85,6 +86,8 @@ func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string,
switch strings.ToLower(format) {
case FormatDotenv:
return formatAsDotEnv(envs), nil
case FormatDotEnvExport:
return formatAsDotEnvExport(envs), nil
case FormatJson:
return formatAsJson(envs), nil
case FormatCSV:
@ -92,7 +95,7 @@ func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string,
case FormatYaml:
return formatAsYaml(envs), nil
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
}
// 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 {
var dotenv string
for _, env := range envs {

@ -6,6 +6,7 @@ package cmd
import (
"encoding/base64"
"encoding/hex"
"strings"
"errors"
"fmt"
@ -17,6 +18,7 @@ import (
"github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/srp"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/fatih/color"
"github.com/go-resty/resty/v2"
"github.com/manifoldco/promptui"
log "github.com/sirupsen/logrus"
@ -31,7 +33,9 @@ var loginCmd = &cobra.Command{
PreRun: toggleDebug,
Run: func(cmd *cobra.Command, args []string) {
currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
if err != nil {
if err != nil && strings.Contains(err.Error(), "The specified item could not be found in the keyring") { // if the key can't be found allow them to override
log.Debug(err)
} else if err != nil {
util.HandleError(err)
}
@ -97,7 +101,7 @@ var loginCmd = &cobra.Command{
util.HandleError(err, "Unable to write write to Infisical Config file. Please try again")
}
log.Infoln("Nice! You are loggin as:", email)
color.Green("Nice! You are logged in as: %v", email)
},
}

@ -15,7 +15,7 @@ var rootCmd = &cobra.Command{
Short: "Infisical CLI is used to inject environment variables into any process",
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},
Version: "0.2.0",
Version: "0.2.2",
}
// Execute adds all child commands to the root command and sets flags appropriately.

@ -12,8 +12,8 @@ import (
"strings"
"syscall"
"github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/fatih/color"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@ -85,16 +85,50 @@ var runCmd = &cobra.Command{
secrets = util.OverrideWithPersonalSecrets(secrets)
}
secretsByKey := getSecretsByKeys(secrets)
environmentVariables := make(map[string]string)
// add all existing environment vars
for _, s := range os.Environ() {
kv := strings.SplitN(s, "=", 2)
key := kv[0]
value := kv[1]
environmentVariables[key] = value
}
// check to see if there are any reserved key words in secrets to inject
reservedEnvironmentVariables := []string{"HOME", "PATH", "PS1", "PS2"}
for _, reservedEnvName := range reservedEnvironmentVariables {
if _, ok := secretsByKey[reservedEnvName]; ok {
delete(secretsByKey, reservedEnvName)
util.PrintWarning(fmt.Sprintf("Infisical secret named [%v] has been removed because it is a reserved secret name", reservedEnvName))
}
}
// now add infisical secrets
for k, v := range secretsByKey {
environmentVariables[k] = v.Value
}
// turn it back into a list of envs
var env []string
for key, value := range environmentVariables {
s := key + "=" + value
env = append(env, s)
}
log.Debugf("injecting the following environment variables into shell: %v", env)
if cmd.Flags().Changed("command") {
command := cmd.Flag("command").Value.String()
err = executeMultipleCommandWithEnvs(command, secrets)
err = executeMultipleCommandWithEnvs(command, len(secretsByKey), env)
if err != nil {
util.HandleError(err, "Unable to execute your chained command")
}
} else {
err = executeSingleCommandWithEnvs(args, secrets)
err = executeSingleCommandWithEnvs(args, len(secretsByKey), env)
if err != nil {
util.HandleError(err, "Unable to execute your single command")
}
@ -111,24 +145,21 @@ func init() {
}
// Will execute a single command and pass in the given secrets into the process
func executeSingleCommandWithEnvs(args []string, secrets []models.SingleEnvironmentVariable) error {
func executeSingleCommandWithEnvs(args []string, secretsCount int, env []string) error {
command := args[0]
argsForCommand := args[1:]
numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(secrets))
log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected)
log.Debugf("executing command: %s %s \n", command, strings.Join(argsForCommand, " "))
log.Debugf("Secrets injected: %v", secrets)
color.Green("Injecting %v Infisical secrets into your application process", secretsCount)
cmd := exec.Command(command, argsForCommand...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = getAllEnvs(secrets)
cmd.Env = env
return execCmd(cmd)
}
func executeMultipleCommandWithEnvs(fullCommand string, secrets []models.SingleEnvironmentVariable) error {
func executeMultipleCommandWithEnvs(fullCommand string, secretsCount int, env []string) error {
shell := [2]string{"sh", "-c"}
if runtime.GOOS == "windows" {
shell = [2]string{"cmd", "/C"}
@ -140,12 +171,10 @@ func executeMultipleCommandWithEnvs(fullCommand string, secrets []models.SingleE
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = getAllEnvs(secrets)
cmd.Env = env
numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(secrets))
log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected)
color.Green("Injecting %v Infisical secrets into your application process", secretsCount)
log.Debugf("executing command: %s %s %s \n", shell[0], shell[1], fullCommand)
log.Debugf("Secrets injected: %v", secrets)
return execCmd(cmd)
}
@ -175,23 +204,3 @@ func execCmd(cmd *exec.Cmd) error {
os.Exit(waitStatus.ExitStatus())
return nil
}
func getAllEnvs(envsToInject []models.SingleEnvironmentVariable) []string {
env_map := make(map[string]string)
for _, env := range os.Environ() {
splitEnv := strings.Split(env, "=")
env_map[splitEnv[0]] = splitEnv[1]
}
for _, env := range envsToInject {
env_map[env.Key] = env.Value // overrite any envs with ones to inject if they clash
}
var allEnvs []string
for key, value := range env_map {
allEnvs = append(allEnvs, fmt.Sprintf("%s=%s", key, value))
}
return allEnvs
}

@ -311,14 +311,25 @@ var secretsDeleteCmd = &cobra.Command{
func init() {
secretsCmd.AddCommand(secretsGetCmd)
secretsCmd.AddCommand(secretsSetCmd)
secretsCmd.AddCommand(secretsDeleteCmd)
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
secretsCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
secretsGetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
}
secretsCmd.AddCommand(secretsSetCmd)
secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
}
secretsCmd.AddCommand(secretsDeleteCmd)
secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
}
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
rootCmd.AddCommand(secretsCmd)
}

@ -23,6 +23,10 @@ func PrintErrorAndExit(exitCode int, err error, messages ...string) {
os.Exit(exitCode)
}
func PrintWarning(message string) {
color.Yellow("Warning: %v", message)
}
func PrintMessageAndExit(messages ...string) {
if len(messages) > 0 {
for _, message := range messages {

@ -33,6 +33,7 @@ func GetKeyRing() (keyring.Keyring, error) {
LibSecretCollectionName: KEYRING_SERVICE_NAME,
KWalletAppID: KEYRING_SERVICE_NAME,
KWalletFolder: KEYRING_SERVICE_NAME,
KeychainName: "login", // default so user will not be prompted
KeychainTrustApplication: true,
WinCredPrefix: KEYRING_SERVICE_NAME,
FileDir: fmt.Sprintf("~/%s-file-vault", KEYRING_SERVICE_NAME),
@ -53,9 +54,10 @@ func GetKeyRing() (keyring.Keyring, error) {
}
func fileKeyringPassphrasePrompt(prompt string) (string, error) {
fmt.Println("You may set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable to avoid typing password")
if password, ok := os.LookupEnv("INFISICAL_VAULT_FILE_PASSPHRASE"); ok {
return password, nil
} else {
fmt.Println("You may set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable to avoid typing password")
}
fmt.Fprintf(os.Stderr, "%s:", prompt)

@ -28,6 +28,8 @@ services:
- ./backend/src:/app/src
- ./backend/nodemon.json:/app/nodemon.json
- /app/node_modules
- ./backend/api-documentation.json:/app/api-documentation.json
- ./backend/swagger.ts:/app/swagger.ts
command: npm run dev
env_file: .env
environment:
@ -48,6 +50,7 @@ services:
- ./frontend/public:/app/public
- ./frontend/styles:/app/styles
- ./frontend/components:/app/components
- ./frontend/ee:/app/ee
- ./frontend/locales:/app/locales
- ./frontend/next-i18next.config.js:/app/next-i18next.config.js
env_file: .env

@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v2/secrets/"
---

@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v2/secrets/"
---

@ -0,0 +1,4 @@
---
title: "Read"
openapi: "GET /api/v2/secrets/"
---

@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v2/secrets/"
---

@ -0,0 +1,3 @@
---
title: "Authentication"
---

@ -0,0 +1,3 @@
---
title: "Introduction"
---

@ -12,12 +12,12 @@ Export environment variables from the platform into a file format.
## Options
| Option | Description | Default value |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` |
| `--projectId` | Only required if injecting via the [service token method](../token). If you are not using service token, the project id will be automatically retrieved from the `.infisical.json` located at the root of your local project. | `None` |
| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` |
| `--format` | Format of the output file. Accepted values: `dotenv`, `csv` and `json` | `dotenv` |
| Option | Description | Default value |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` |
| `--projectId` | Only required if injecting via the [service token method](../token). If you are not using service token, the project id will be automatically retrieved from the `.infisical.json` located at the root of your local project. | `None` |
| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` |
| `--format` | Format of the output file. Accepted values: `dotenv`, `dotenv-export`, `csv` and `json` | `dotenv` |
## Examples
@ -25,6 +25,9 @@ Export environment variables from the platform into a file format.
# Export variables to a .env file
infisical export > .env
# Export variables to a .env file (with export keyword)
infisical export --format=dotenv-export > .env
# Export variables to a CSV file
infisical export --format=csv > secrets.csv
@ -33,4 +36,5 @@ infisical export --format=json > secrets.json
# Export variables to a YAML file
infisical export --format=yaml > secrets.yaml
```

@ -2,8 +2,10 @@
title: "Activity Logs"
---
Activity logs record all actions going through Infisical including CRUD operations applied to environment variables. They help answer questions like:
Activity logs record all actions going through Infisical including who performed which CRUD operations on environment variables and from what IP address. They help answer questions like:
- Who added or updated environment variables recently?
- Did Bob read environment variables last week (if at all)?
- What IP address was used for that action?
![Activity logs](../../images/activity-logs.png)

@ -2,4 +2,22 @@
title: "Point-in-Time Recovery"
---
Point-in-time (PIT) recovery allows environment variables to be rolled back to any point in time. It's powered by snapshots that get captured after mutations to environment variables.
Point-in-time recovery allows environment variables to be rolled back to any point in time. It's powered by snapshots that get captured after mutations to environment variables.
## Commits
Similar to Git, a commit in Infisical is a snapshot of your project's secrets at a specific point in time. You can browse and view your project's snapshots via the "Point-in-Time Recovery" sidebar.
![PIT commits](../../images/pit-commits.png)
![PIT snapshots](../../images/pit-snapshots.png)
## Rolling back
Environment variables can be rolled back to any point in time via the "Rollback to this snapshot" button.
![PIT snapshot](../../images/pit-snapshot.png)
<Note>
Rolling back environment variables to a past snapshot creates a new commit and
snapshot at the top of the stack and updates secret versions.
</Note>

@ -0,0 +1,14 @@
---
title: "Secret Versioning"
---
Secret versioning records changes made to every secret.
![secret versioning](../../images/secret-versioning.png)
<Note>
You can copy and paste a secret version value to the "Value" input field "roll
back" to that secret version. This creates a new secret version at the top of
the stack. We're releasing the ability to press and automatically roll back to
a secret version soon.
</Note>

@ -1,5 +0,0 @@
---
title: "Secret Versioning"
---
Secret versioning allows an individual environment variable to be rolled back without touching other project environment variables.

Binary file not shown.

After

(image error) Size: 394 KiB

Binary file not shown.

After

(image error) Size: 370 KiB

Binary file not shown.

After

(image error) Size: 172 KiB

BIN
docs/images/pit-commits.png Normal file

Binary file not shown.

After

(image error) Size: 271 KiB

Binary file not shown.

After

(image error) Size: 269 KiB

Binary file not shown.

After

(image error) Size: 364 KiB

Binary file not shown.

After

(image error) Size: 337 KiB

@ -81,7 +81,7 @@
"getting-started/dashboard/project",
"getting-started/dashboard/integrations",
"getting-started/dashboard/pit-recovery",
"getting-started/dashboard/versioning",
"getting-started/dashboard/secret-versioning",
"getting-started/dashboard/audit-logs",
"getting-started/dashboard/token"
]

@ -48,7 +48,7 @@ SMTP_FROM_NAME=Infisical
```
<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>
## Mailgun
@ -70,6 +70,28 @@ SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out em
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
![opening AWS SES console](../../images/email-aws-ses-console.png)
![creating AWS IAM SES user](../../images/email-aws-ses-user.png)
3. With your AWS SES SMTP credentials, you can now set up your SMTP environment variables:
```
SMTP_HOST=smtp.mailgun.org # obtained from credentials page
SMTP_HOST=email-smtp.ap-northeast-1.amazonaws.com # SMTP endpoint obtained from SMTP settings
SMTP_USERNAME=xxx # your SMTP username
SMTP_PASSWORD=xxx # your SMTP password
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
SMTP_FROM_NAME=Infisical
```
<Info>
Remember that you will need to restart Infisical for this to work properly.
</Info>
Remember that you will need to restart Infisical for this to work properly.
</Info>

@ -9,7 +9,6 @@ export const initPostHog = () => {
if (typeof window !== 'undefined') {
// @ts-ignore
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) {
console.log("Outside of posthog", "POSTHOG_API_KEY", POSTHOG_API_KEY, "POSTHOG_HOST", POSTHOG_HOST)
posthog.init(POSTHOG_API_KEY, {
api_host: POSTHOG_HOST
});

@ -4,13 +4,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export default function Error({ text }: { text: string }): JSX.Element {
return (
<div className="relative bg-red-500 opacity-100 border flex flex-row justify-center m-auto items-center w-fit rounded-full mb-4">
<div className="relative flex flex-row justify-center m-auto items-center w-fit rounded-full">
<FontAwesomeIcon
icon={faExclamationTriangle}
className="text-white mt-1.5 mb-2 mx-2"
className="text-red mt-1.5 mb-2 mx-2"
/>
{text && (
<p className="relative top-0 text-white mr-2 text-sm py-1">{text}</p>
<p className="relative top-0 text-red mr-2 text-sm py-1">{text}</p>
)}
</div>
);

@ -55,7 +55,7 @@ export default function EventFilter({
{selected != '' ? (
<p className="select-none text-bunker-100">{t("activity:event." + selected)}</p>
) : (
<p className="select-none">Select an event</p>
<p className="select-none">{String(t("common:select-event"))}</p>
)}
{selected != '' ? (
<FontAwesomeIcon

@ -11,7 +11,6 @@ import {
faKey,
faMobile,
faPlug,
faTimeline,
faUser,
} from "@fortawesome/free-solid-svg-icons";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
@ -20,7 +19,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import getOrganizations from "~/pages/api/organization/getOrgs";
import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProjects";
import getOrganizationUsers from "~/pages/api/organization/GetOrgUsers";
import checkUserAction from "~/pages/api/userActions/checkUserAction";
import getUser from "~/pages/api/user/getUser";
import addUserToWorkspace from "~/pages/api/workspace/addUserToWorkspace";
import createWorkspace from "~/pages/api/workspace/createWorkspace";
import getWorkspaces from "~/pages/api/workspace/getWorkspaces";
@ -40,6 +39,7 @@ import Listbox from "./Listbox";
interface LayoutProps {
children: React.ReactNode;
}
const crypto = require("crypto");
export default function Layout({ children }: LayoutProps) {
const router = useRouter();
@ -84,12 +84,31 @@ export default function Layout({ children }: LayoutProps) {
});
const newWorkspaceId = newWorkspace._id;
const randomBytes = crypto.randomBytes(16).toString("hex");
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
const myUser = await getUser();
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey: myUser.publicKey,
privateKey: PRIVATE_KEY,
}) as { ciphertext: string; nonce: string };
await uploadKeys(
newWorkspaceId,
myUser._id,
ciphertext,
nonce
);
if (addAllUsers) {
console.log('adding other users')
const orgUsers = await getOrganizationUsers({
orgId: tempLocalStorage("orgData.id"),
});
orgUsers.map(async (user: any) => {
if (user.status == "accepted") {
if (user.status == "accepted" && user.email != myUser.email) {
const result = await addUserToWorkspace(
user.user.email,
newWorkspaceId
@ -184,6 +203,7 @@ export default function Layout({ children }: LayoutProps) {
if (
userWorkspaces.length == 0 &&
router.asPath != "/noprojects" &&
!router.asPath.includes("home")&&
!router.asPath.includes("settings")
) {
router.push("/noprojects");
@ -191,9 +211,16 @@ export default function Layout({ children }: LayoutProps) {
const intendedWorkspaceId = router.asPath
.split("/")
[router.asPath.split("/").length - 1].split("?")[0];
if (
!["heroku", "vercel", "github", "netlify"].includes(intendedWorkspaceId)
) {
localStorage.setItem("projectData.id", intendedWorkspaceId);
}
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
if (
intendedWorkspaceId != "heroku" &&
!["heroku", "vercel", "github", "netlify"].includes(intendedWorkspaceId) &&
!userWorkspaces
.map((workspace: { _id: string }) => workspace._id)
.includes(intendedWorkspaceId)
@ -239,14 +266,14 @@ export default function Layout({ children }: LayoutProps) {
.split("/")
[router.asPath.split("/").length - 1].split("?")[0]
) {
router.push(
"/dashboard/" +
workspaceMapping[workspaceSelected as any]
);
localStorage.setItem(
"projectData.id",
`${workspaceMapping[workspaceSelected as any]}`
);
router.push(
"/dashboard/" +
workspaceMapping[workspaceSelected as any]
);
}
} catch (error) {
console.log(error);

@ -46,7 +46,7 @@ export default function ListBox({
>
<div className="flex flex-row">
{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}
</span>
@ -69,7 +69,7 @@ export default function ListBox({
<Listbox.Option
key={personIdx}
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' : ''
} ${
active && !selected

@ -1,27 +1,11 @@
import React from "react";
import { Switch } from "@headlessui/react";
interface OverrideProps {
id: string;
keyName: string;
value: string;
pos: number;
comment: string;
}
interface ToggleProps {
enabled: boolean;
setEnabled: (value: boolean) => void;
addOverride: (value: OverrideProps) => void;
keyName: string;
value: string;
addOverride: (value: string | undefined, pos: number) => void;
pos: number;
id: string;
comment: string;
deleteOverride: (id: string) => void;
sharedToHide: string[];
setSharedToHide: (values: string[]) => void;
}
/**
@ -30,41 +14,23 @@ interface ToggleProps {
* @param {boolean} obj.enabled - whether the toggle is turned on or off
* @param {function} obj.setEnabled - change the state of the toggle
* @param {function} obj.addOverride - a function that adds an override to a certain secret
* @param {string} obj.keyName - key of a certain secret
* @param {string} obj.value - value of a certain secret
* @param {number} obj.pos - position of a certain secret
#TODO: make the secret id persistent?
* @param {string} obj.id - id of a certain secret
* @param {function} obj.deleteOverride - a function that deleted an override for a certain secret
* @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden.
* @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually
* @returns
*/
export default function Toggle ({
enabled,
setEnabled,
addOverride,
keyName,
value,
pos,
id,
comment,
deleteOverride,
sharedToHide,
setSharedToHide
pos
}: ToggleProps): JSX.Element {
return (
<Switch
checked={enabled}
onChange={() => {
if (enabled == false) {
addOverride({ id, keyName, value, pos, comment });
setSharedToHide([
...sharedToHide!,
id
])
addOverride('', pos);
} else {
deleteOverride(id);
addOverride(undefined, pos);
}
setEnabled(!enabled);
}}

@ -3,7 +3,6 @@ import Image from "next/image";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import {
FontAwesomeIcon,
FontAwesomeIconProps,
} from "@fortawesome/react-fontawesome";
const classNames = require("classnames");
@ -101,7 +100,7 @@ export default function Button(props: ButtonProps): JSX.Element {
<div
className={`${
props.loading == true ? "opacity-100" : "opacity-0"
} absolute flex items-center px-2 duration-200`}
} absolute flex items-center px-3 bg-primary duration-200 w-full`}
>
<Image
src="/images/loading/loadingblack.gif"

@ -109,7 +109,7 @@ const AddProjectMemberDialog = ({
selected={email ? email : data[0]}
onChange={setEmail}
data={data}
width="full"
isFull={true}
/>
)}
</div>

@ -8,7 +8,6 @@ import nacl from "tweetnacl";
import addServiceToken from "~/pages/api/serviceToken/addServiceToken";
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
import { envMapping } from "../../../public/data/frequentConstants";
import {
decryptAssymmetric,
encryptAssymmetric,
@ -34,11 +33,12 @@ const AddServiceTokenDialog = ({
workspaceId,
workspaceName,
serviceTokens,
environments,
setServiceTokens
}) => {
const [serviceToken, setServiceToken] = useState("");
const [serviceTokenName, setServiceTokenName] = useState("");
const [serviceTokenEnv, setServiceTokenEnv] = useState("Development");
const [selectedServiceTokenEnv, setSelectedServiceTokenEnv] = useState(environments?.[0]);
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day");
const [serviceTokenCopied, setServiceTokenCopied] = useState(false);
const { t } = useTranslation();
@ -66,15 +66,13 @@ const AddServiceTokenDialog = ({
let newServiceToken = await addServiceToken({
name: serviceTokenName,
workspaceId,
environment: envMapping[serviceTokenEnv],
environment: selectedServiceTokenEnv?.slug ? selectedServiceTokenEnv.slug : environments[0]?.name,
expiresIn: expiryMapping[serviceTokenExpiresIn],
encryptedKey: ciphertext,
iv,
tag
});
console.log('newServiceToken', newServiceToken);
setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData]));
setServiceToken(newServiceToken.serviceToken + "." + randomBytes);
};
@ -103,155 +101,159 @@ const AddServiceTokenDialog = ({
};
return (
<div className="z-50">
<div className='z-50'>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative" onClose={closeModal}>
<Dialog as='div' className='relative' onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
enter='ease-out duration-300'
enterFrom='opacity-0'
enterTo='opacity-100'
leave='ease-in duration-200'
leaveFrom='opacity-100'
leaveTo='opacity-0'
>
<div className="fixed inset-0 bg-bunker-700 bg-opacity-80" />
<div className='fixed inset-0 bg-bunker-700 bg-opacity-80' />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<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"
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'
>
{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">
{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.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50"
as='h3'
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,
})}
</Dialog.Title>
<div className="mt-2 mb-4">
<div className="flex flex-col">
<p className="text-sm text-gray-500">
{t("section-token:add-dialog.description")}
<div className='mt-2 mb-4'>
<div className='flex flex-col'>
<p className='text-sm text-gray-500'>
{t('section-token:add-dialog.description')}
</p>
</div>
</div>
<div className="max-h-28 mb-2">
<div className='max-h-28 mb-2'>
<InputField
label={t("section-token:add-dialog.name")}
label={t('section-token:add-dialog.name')}
onChangeHandler={setServiceTokenName}
type="varName"
type='varName'
value={serviceTokenName}
placeholder=""
placeholder=''
isRequired
/>
</div>
<div className="max-h-28 mb-2">
<div className='max-h-28 mb-2'>
<ListBox
selected={serviceTokenEnv}
onChange={setServiceTokenEnv}
data={[
"Development",
"Staging",
"Production",
"Testing",
]}
selected={selectedServiceTokenEnv?.name ? selectedServiceTokenEnv?.name : environments[0]?.name}
data={environments.map(({ name }) => name)}
onChange={(envName) =>
setSelectedServiceTokenEnv(
environments.find(
({ name }) => envName === name
) || {
name: 'unknown',
slug: 'unknown',
}
)
}
isFull={true}
text={`${t("common:environment")}: `}
text={`${t('common:environment')}: `}
/>
</div>
<div className="max-h-28">
<div className='max-h-28'>
<ListBox
selected={serviceTokenExpiresIn}
onChange={setServiceTokenExpiresIn}
data={[
"1 day",
"7 days",
"1 month",
"6 months",
"12 months",
'1 day',
'7 days',
'1 month',
'6 months',
'12 months',
]}
isFull={true}
text={`${t("common:expired-in")}: `}
text={`${t('common:expired-in')}: `}
/>
</div>
<div className="max-w-max">
<div className="mt-6 flex flex-col justify-start w-max">
<div className='max-w-max'>
<div className='mt-6 flex flex-col justify-start w-max'>
<Button
onButtonPressed={() => generateServiceToken()}
color="mineshaft"
text={t("section-token:add-dialog.add")}
textDisabled={t("section-token:add-dialog.add")}
size="md"
active={serviceTokenName == "" ? false : true}
color='mineshaft'
text={t('section-token:add-dialog.add')}
textDisabled={t('section-token:add-dialog.add')}
size='md'
active={serviceTokenName == '' ? false : true}
/>
</div>
</div>
</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
as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50"
as='h3'
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>
<div className="mt-2 mb-4">
<div className="flex flex-col">
<p className="text-sm text-gray-500">
<div className='mt-2 mb-4'>
<div className='flex flex-col'>
<p className='text-sm text-gray-500'>
{t(
"section-token:add-dialog.copy-service-token-description"
'section-token:add-dialog.copy-service-token-description'
)}
</p>
</div>
</div>
<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='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'>
<input
type="text"
type='text'
value={serviceToken}
id="serviceToken"
className="invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none"
id='serviceToken'
className='invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none'
></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}
</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
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 ? (
<FontAwesomeIcon
icon={faCheck}
className="pr-0.5"
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 px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm">
{t("common:click-to-copy")}
<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')}
</span>
</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
onButtonPressed={() => closeAddServiceTokenModal()}
color="mineshaft"
text="Close"
size="md"
color='mineshaft'
text='Close'
size='md'
/>
</div>
</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,77 @@
import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import { Dialog, Transition } from "@headlessui/react";
// #TODO: USE THIS. Currently it's not. Kinda complicated to set up because of state.
type Props = {
isOpen: boolean
onClose: () => void
onSubmit: () => void
}
export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
const { t } = useTranslation()
return (
<div>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={() => {}}>
<div className="fixed inset-0 overflow-y-auto">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-70" onClick={onClose} />
</Transition.Child>
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-400"
>
{t('dashboard:sidebar.delete-key-dialog.title')}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
{t('dashboard:sidebar.delete-key-dialog.confirm-delete-message')}
</p>
</div>
<div className="mt-6 flex justify-start">
<button
type="button"
className="inline-flex justify-center rounded-md border border-transparent bg-red-700 hover:bg-red-600 px-4 py-2 text-sm font-medium text-bunker-200 hover:text-white text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={onSubmit}
>
Delete
</button>
<button
type="button"
className="ml-2 inline-flex justify-center rounded-md border border-transparent bg-bunker-500 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-mineshaft-500 hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={onClose}
>
Cancel
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</div>
);
};

@ -36,7 +36,7 @@ export default function BottonRightPopup({
}: PopupProps): JSX.Element {
return (
<div
className="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-xl absolute bottom-0 right-0 mr-6 mb-6"
className="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-md absolute bottom-0 right-0 mr-6 mb-6"
role="alert"
>
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">

@ -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 deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken";
import { reverseEnvMapping } from '../../../public/data/frequentConstants';
import guidGenerator from '../../utilities/randomId';
import Button from '../buttons/Button';
@ -60,7 +59,7 @@ const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTok
{workspaceName}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{reverseEnvMapping[row.environment]}
{row.environment}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{new Date(row.expiresAt).toUTCString()}

@ -117,13 +117,13 @@ const UserTable = ({
return (
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
<div className="absolute rounded-t-md w-full h-14 bg-white/5"></div>
<table className="w-full my-1">
<thead className="text-gray-400">
<div className="absolute rounded-t-md w-full h-[3.25rem] bg-white/5"></div>
<table className="w-full my-0.5">
<thead className="text-gray-400 text-sm font-light">
<tr>
<th className="text-left pl-6 py-3.5">First Name</th>
<th className="text-left pl-6 py-3.5">Last Name</th>
<th className="text-left pl-6 py-3.5">Email</th>
<th className="text-left pl-6 py-3.5">FIRST NAME</th>
<th className="text-left pl-6 py-3.5">LAST NAME</th>
<th className="text-left pl-6 py-3.5">EMAIL</th>
<th></th>
</tr>
</thead>

Some files were not shown because too many files have changed in this diff Show More