mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-25 14:05:03 +00:00
Compare commits
76 Commits
v0.2.15
...
gen-exampl
Author | SHA1 | Date | |
---|---|---|---|
a685ac3e73 | |||
9a22975732 | |||
b0c541f8dc | |||
c84add0a2a | |||
ace0e9c56f | |||
498705f330 | |||
7892624709 | |||
d8889beaf7 | |||
6e67304e92 | |||
8b23e89a64 | |||
7611b999fe | |||
aba8feb985 | |||
747cc1134c | |||
db05412865 | |||
679b1d9c23 | |||
5ea5887146 | |||
13838861fb | |||
09c60322db | |||
68bf0b9efe | |||
56da34d343 | |||
086dd621b5 | |||
56a14925da | |||
c13cb23942 | |||
31df4a26fa | |||
9f9273bb02 | |||
8ae43cdcf6 | |||
1d72d310e5 | |||
e72e6cf2b7 | |||
0ac40acc40 | |||
56710657bd | |||
92f4979715 | |||
1e9118df33 | |||
e16c0e53ff | |||
0d57a26925 | |||
1bd180596e | |||
fca003dfd7 | |||
f1ef23874c | |||
16883cf168 | |||
1781b71399 | |||
fb62fa4d32 | |||
ed148a542d | |||
a4f7843727 | |||
48cd84ce77 | |||
3859a7e09b | |||
76d0127029 | |||
a94cd8c85c | |||
ee555f3f15 | |||
bd230a8b7d | |||
a4926d8833 | |||
7560d2f673 | |||
44b2bc1795 | |||
3ccc6e5d5c | |||
ccb579ecfd | |||
29f5e8aa78 | |||
d64357af61 | |||
37c91ae652 | |||
3a4cfa0834 | |||
cef45c2155 | |||
5143fc6eee | |||
186382619c | |||
91e70c5476 | |||
216ace9f61 | |||
6b99582a66 | |||
ea0fe1b92e | |||
72810acf2e | |||
a013768313 | |||
a660261678 | |||
7d181f334c | |||
46ab27af1a | |||
25bb966a32 | |||
52697dea97 | |||
ca688764a3 | |||
59ab4bf7f9 | |||
d4bc92bd5b | |||
7efdbeb787 | |||
43daff29dc |
README.md
backend
package-lock.jsonpackage.json
src
app.ts
config
controllers
v1
authController.tsintegrationAuthController.tsintegrationController.tsmembershipOrgController.tspasswordController.ts
v2
ee
controllers/v1
helpers
models
services
helpers
integrations
models
routes
services
variables
cli
main.go
packages
docs
frontend
package-lock.jsonpackage.jsonconst.ts
public
data
locales
src
components
basic
dashboard
AddTagsMenu.tsxCompareSecretsModal.tsxDashboardInputField.tsxDeleteActionButton.tsxDownloadSecretsMenu.tsxDropZone.tsxKeyPair.tsxSideBar.tsx
integrations
navigation
utilities
v2
Button
Card
DeleteActionModal
FormControl
HoverCard
IconButton
Input
Modal
Select
Table
UpgradePlanModal
index.tsxcontext
ee/components
hooks
pages
_app.tsx
activity
api
integrations
organization
workspace
dashboard
github.tsxheroku.tsxintegrations
login.tsxnetlify.tsxsettings/project
users
vercel.tsxviews/Settings/ProjectSettingsPage
ProjectSettingsPage.tsxindex.tsx
components
CopyProjectIDSection
EnvironmentSection
ProjectNameChangeSection
SecretTagsSection
ServiceTokenSection
index.tsxi18n
37
README.md
37
README.md
File diff suppressed because one or more lines are too long
229
backend/package-lock.json
generated
229
backend/package-lock.json
generated
@ -2892,14 +2892,27 @@
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
|
||||
},
|
||||
"node_modules/@sentry/node": {
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.19.0.tgz",
|
||||
"integrity": "sha512-yG7Tx32WqOkEHVotFLrumCcT9qlaSDTkFNZ+yLSvZXx74ifsE781DzBA9W7K7bBdYO3op+p2YdsOKzf3nPpAyQ==",
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "7.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.21.1.tgz",
|
||||
"integrity": "sha512-Og5wEEsy24fNvT/T7IKjcV4EvVK5ryY2kxbJzKY6GU2eX+i+aBl+n/vp7U0Es351C/AlTkS+0NOUsp2TQQFxZA==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.19.0",
|
||||
"@sentry/types": "7.19.0",
|
||||
"@sentry/utils": "7.19.0",
|
||||
"@sentry/types": "7.21.1",
|
||||
"@sentry/utils": "7.21.1",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/node": {
|
||||
"version": "7.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.21.1.tgz",
|
||||
"integrity": "sha512-B+p1nQHaFWdCCRVmvqlr/+vdQCI3mGLObucNfK2YC22IQZg7+3u6tEbxJ7umITIjeSSKgf7ZoZwCxL9VfkrNXg==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.21.1",
|
||||
"@sentry/types": "7.21.1",
|
||||
"@sentry/utils": "7.21.1",
|
||||
"cookie": "^0.4.1",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"lru_map": "^0.3.3",
|
||||
@ -2909,80 +2922,34 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/node/node_modules/@sentry/core": {
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.19.0.tgz",
|
||||
"integrity": "sha512-YF9cTBcAnO4R44092BJi5Wa2/EO02xn2ziCtmNgAVTN2LD31a/YVGxGBt/FDr4Y6yeuVehaqijVVvtpSmXrGJw==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.19.0",
|
||||
"@sentry/utils": "7.19.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/node/node_modules/@sentry/types": {
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.19.0.tgz",
|
||||
"integrity": "sha512-oGRAT6lfzoKrxO1mvxiSj0XHxWPd6Gd1wpPGuu6iJo03xgWDS+MIlD1h2unqL4N5fAzLjzmbC2D2lUw50Kn2pA==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/node/node_modules/@sentry/utils": {
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.19.0.tgz",
|
||||
"integrity": "sha512-2L6lq+c9Ol2uiRxQDdcgoapmHJp24MhMN0gIkn2alSfMJ+ls6bGXzQHx6JAIdoOiwFQXRZHKL9ecfAc8O+vItA==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.19.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/tracing": {
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.19.0.tgz",
|
||||
"integrity": "sha512-SWY17M3TsgBePaGowUcSqBwaT0TJQzuNexVnLojuU0k6F57L9hubvP9zaoosoCfARXQ/3NypAFWnlJyf570rFQ==",
|
||||
"version": "7.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.21.1.tgz",
|
||||
"integrity": "sha512-b1BTPsRaNQpohzegoz59KGuBl+To651vEq0vMS4tCzSyIdxkYso3JCrjDdEqW/2MliQYANNVrUai2bmwmU9h1g==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.19.0",
|
||||
"@sentry/types": "7.19.0",
|
||||
"@sentry/utils": "7.19.0",
|
||||
"@sentry/core": "7.21.1",
|
||||
"@sentry/types": "7.21.1",
|
||||
"@sentry/utils": "7.21.1",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/tracing/node_modules/@sentry/core": {
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.19.0.tgz",
|
||||
"integrity": "sha512-YF9cTBcAnO4R44092BJi5Wa2/EO02xn2ziCtmNgAVTN2LD31a/YVGxGBt/FDr4Y6yeuVehaqijVVvtpSmXrGJw==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.19.0",
|
||||
"@sentry/utils": "7.19.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"node_modules/@sentry/types": {
|
||||
"version": "7.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.21.1.tgz",
|
||||
"integrity": "sha512-3/IKnd52Ol21amQvI+kz+WB76s8/LR5YvFJzMgIoI2S8d82smIr253zGijRXxHPEif8kMLX4Yt+36VzrLxg6+A==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/tracing/node_modules/@sentry/types": {
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.19.0.tgz",
|
||||
"integrity": "sha512-oGRAT6lfzoKrxO1mvxiSj0XHxWPd6Gd1wpPGuu6iJo03xgWDS+MIlD1h2unqL4N5fAzLjzmbC2D2lUw50Kn2pA==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/tracing/node_modules/@sentry/utils": {
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.19.0.tgz",
|
||||
"integrity": "sha512-2L6lq+c9Ol2uiRxQDdcgoapmHJp24MhMN0gIkn2alSfMJ+ls6bGXzQHx6JAIdoOiwFQXRZHKL9ecfAc8O+vItA==",
|
||||
"node_modules/@sentry/utils": {
|
||||
"version": "7.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.21.1.tgz",
|
||||
"integrity": "sha512-F0W0AAi8tgtTx6ApZRI2S9HbXEA9ENX1phTZgdNNWcMFm1BNbc21XEwLqwXBNjub5nlA6CE8xnjXRgdZKx4kzQ==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.19.0",
|
||||
"@sentry/types": "7.21.1",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -3767,9 +3734,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz",
|
||||
"integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.0.tgz",
|
||||
"integrity": "sha512-zT7wZyNYu3N5Bu0wuZ6QccIf93Qk1eV8LOewxgjOZFd2DenOs98cJ7+Y6703d0wkaXGY6/nZd4EweJaHz9uzQw==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
@ -7165,9 +7132,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.7.2.tgz",
|
||||
"integrity": "sha512-lrP2V5U1qhaf+z33fiIn7aYAZZ1fVDly+TkFRjTujNBF/FIHESATj2RbgAOSlWqv32fsZXkXejXzeVfjbv35Ow==",
|
||||
"version": "6.7.3",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.7.3.tgz",
|
||||
"integrity": "sha512-bLC2Pt6Vpoov+1kBYvQgJXG/2DWXbfIvfK4Gh68kCdYGh6CVO31YxYuIGz70hyGwX2g4DmSzbs5IA8Px2neMCQ==",
|
||||
"dependencies": {
|
||||
"bson": "^4.7.0",
|
||||
"kareem": "2.4.1",
|
||||
@ -14343,81 +14310,53 @@
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
|
||||
},
|
||||
"@sentry/node": {
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.19.0.tgz",
|
||||
"integrity": "sha512-yG7Tx32WqOkEHVotFLrumCcT9qlaSDTkFNZ+yLSvZXx74ifsE781DzBA9W7K7bBdYO3op+p2YdsOKzf3nPpAyQ==",
|
||||
"@sentry/core": {
|
||||
"version": "7.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.21.1.tgz",
|
||||
"integrity": "sha512-Og5wEEsy24fNvT/T7IKjcV4EvVK5ryY2kxbJzKY6GU2eX+i+aBl+n/vp7U0Es351C/AlTkS+0NOUsp2TQQFxZA==",
|
||||
"requires": {
|
||||
"@sentry/core": "7.19.0",
|
||||
"@sentry/types": "7.19.0",
|
||||
"@sentry/utils": "7.19.0",
|
||||
"@sentry/types": "7.21.1",
|
||||
"@sentry/utils": "7.21.1",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/node": {
|
||||
"version": "7.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.21.1.tgz",
|
||||
"integrity": "sha512-B+p1nQHaFWdCCRVmvqlr/+vdQCI3mGLObucNfK2YC22IQZg7+3u6tEbxJ7umITIjeSSKgf7ZoZwCxL9VfkrNXg==",
|
||||
"requires": {
|
||||
"@sentry/core": "7.21.1",
|
||||
"@sentry/types": "7.21.1",
|
||||
"@sentry/utils": "7.21.1",
|
||||
"cookie": "^0.4.1",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"lru_map": "^0.3.3",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/core": {
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.19.0.tgz",
|
||||
"integrity": "sha512-YF9cTBcAnO4R44092BJi5Wa2/EO02xn2ziCtmNgAVTN2LD31a/YVGxGBt/FDr4Y6yeuVehaqijVVvtpSmXrGJw==",
|
||||
"requires": {
|
||||
"@sentry/types": "7.19.0",
|
||||
"@sentry/utils": "7.19.0",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/types": {
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.19.0.tgz",
|
||||
"integrity": "sha512-oGRAT6lfzoKrxO1mvxiSj0XHxWPd6Gd1wpPGuu6iJo03xgWDS+MIlD1h2unqL4N5fAzLjzmbC2D2lUw50Kn2pA=="
|
||||
},
|
||||
"@sentry/utils": {
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.19.0.tgz",
|
||||
"integrity": "sha512-2L6lq+c9Ol2uiRxQDdcgoapmHJp24MhMN0gIkn2alSfMJ+ls6bGXzQHx6JAIdoOiwFQXRZHKL9ecfAc8O+vItA==",
|
||||
"requires": {
|
||||
"@sentry/types": "7.19.0",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/tracing": {
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.19.0.tgz",
|
||||
"integrity": "sha512-SWY17M3TsgBePaGowUcSqBwaT0TJQzuNexVnLojuU0k6F57L9hubvP9zaoosoCfARXQ/3NypAFWnlJyf570rFQ==",
|
||||
"version": "7.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.21.1.tgz",
|
||||
"integrity": "sha512-b1BTPsRaNQpohzegoz59KGuBl+To651vEq0vMS4tCzSyIdxkYso3JCrjDdEqW/2MliQYANNVrUai2bmwmU9h1g==",
|
||||
"requires": {
|
||||
"@sentry/core": "7.19.0",
|
||||
"@sentry/types": "7.19.0",
|
||||
"@sentry/utils": "7.19.0",
|
||||
"@sentry/core": "7.21.1",
|
||||
"@sentry/types": "7.21.1",
|
||||
"@sentry/utils": "7.21.1",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/types": {
|
||||
"version": "7.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.21.1.tgz",
|
||||
"integrity": "sha512-3/IKnd52Ol21amQvI+kz+WB76s8/LR5YvFJzMgIoI2S8d82smIr253zGijRXxHPEif8kMLX4Yt+36VzrLxg6+A=="
|
||||
},
|
||||
"@sentry/utils": {
|
||||
"version": "7.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.21.1.tgz",
|
||||
"integrity": "sha512-F0W0AAi8tgtTx6ApZRI2S9HbXEA9ENX1phTZgdNNWcMFm1BNbc21XEwLqwXBNjub5nlA6CE8xnjXRgdZKx4kzQ==",
|
||||
"requires": {
|
||||
"@sentry/types": "7.21.1",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/core": {
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.19.0.tgz",
|
||||
"integrity": "sha512-YF9cTBcAnO4R44092BJi5Wa2/EO02xn2ziCtmNgAVTN2LD31a/YVGxGBt/FDr4Y6yeuVehaqijVVvtpSmXrGJw==",
|
||||
"requires": {
|
||||
"@sentry/types": "7.19.0",
|
||||
"@sentry/utils": "7.19.0",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/types": {
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.19.0.tgz",
|
||||
"integrity": "sha512-oGRAT6lfzoKrxO1mvxiSj0XHxWPd6Gd1wpPGuu6iJo03xgWDS+MIlD1h2unqL4N5fAzLjzmbC2D2lUw50Kn2pA=="
|
||||
},
|
||||
"@sentry/utils": {
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.19.0.tgz",
|
||||
"integrity": "sha512-2L6lq+c9Ol2uiRxQDdcgoapmHJp24MhMN0gIkn2alSfMJ+ls6bGXzQHx6JAIdoOiwFQXRZHKL9ecfAc8O+vItA==",
|
||||
"requires": {
|
||||
"@sentry/types": "7.19.0",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sinclair/typebox": {
|
||||
@ -15051,9 +14990,9 @@
|
||||
"integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz",
|
||||
"integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.0.tgz",
|
||||
"integrity": "sha512-zT7wZyNYu3N5Bu0wuZ6QccIf93Qk1eV8LOewxgjOZFd2DenOs98cJ7+Y6703d0wkaXGY6/nZd4EweJaHz9uzQw==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
@ -17626,9 +17565,9 @@
|
||||
}
|
||||
},
|
||||
"mongoose": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.7.2.tgz",
|
||||
"integrity": "sha512-lrP2V5U1qhaf+z33fiIn7aYAZZ1fVDly+TkFRjTujNBF/FIHESATj2RbgAOSlWqv32fsZXkXejXzeVfjbv35Ow==",
|
||||
"version": "6.7.3",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.7.3.tgz",
|
||||
"integrity": "sha512-bLC2Pt6Vpoov+1kBYvQgJXG/2DWXbfIvfK4Gh68kCdYGh6CVO31YxYuIGz70hyGwX2g4DmSzbs5IA8Px2neMCQ==",
|
||||
"requires": {
|
||||
"bson": "^4.7.0",
|
||||
"kareem": "2.4.1",
|
||||
|
@ -1,4 +1,32 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@godaddy/terminus": "^4.11.2",
|
||||
"@sentry/node": "^7.21.1",
|
||||
"@sentry/tracing": "^7.21.1",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"axios": "^1.2.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": "^8.5.1",
|
||||
"jsrp": "^0.2.4",
|
||||
"mongoose": "^6.7.3",
|
||||
"nodemailer": "^6.8.0",
|
||||
"posthog-node": "^2.1.0",
|
||||
"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"
|
||||
},
|
||||
"name": "infisical-api",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.js",
|
||||
|
@ -50,6 +50,7 @@ import {
|
||||
serviceTokenData as v2ServiceTokenDataRouter,
|
||||
apiKeyData as v2APIKeyDataRouter,
|
||||
environment as v2EnvironmentRouter,
|
||||
tags as v2TagsRouter,
|
||||
} from './routes/v2';
|
||||
|
||||
import { healthCheck } from './routes/status';
|
||||
@ -112,6 +113,7 @@ app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
||||
app.use('/api/v2/users', v2UsersRouter);
|
||||
app.use('/api/v2/organizations', v2OrganizationsRouter);
|
||||
app.use('/api/v2/workspace', v2EnvironmentRouter);
|
||||
app.use('/api/v2/workspace', v2TagsRouter);
|
||||
app.use('/api/v2/workspace', v2WorkspaceRouter);
|
||||
app.use('/api/v2/secret', v2SecretRouter); // deprecated
|
||||
app.use('/api/v2/secrets', v2SecretsRouter);
|
||||
|
@ -13,10 +13,13 @@ const MONGO_URL = process.env.MONGO_URL!;
|
||||
const NODE_ENV = process.env.NODE_ENV! || 'production';
|
||||
const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true;
|
||||
const LOKI_HOST = process.env.LOKI_HOST || undefined;
|
||||
const CLIENT_ID_AZURE = process.env.CLIENT_ID_AZURE!;
|
||||
const TENANT_ID_AZURE = process.env.TENANT_ID_AZURE!;
|
||||
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
|
||||
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
|
||||
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!;
|
||||
const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!;
|
||||
const CLIENT_SECRET_AZURE = process.env.CLIENT_SECRET_AZURE!;
|
||||
const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
|
||||
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
|
||||
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
|
||||
@ -60,10 +63,13 @@ export {
|
||||
NODE_ENV,
|
||||
VERBOSE_ERROR_OUTPUT,
|
||||
LOKI_HOST,
|
||||
CLIENT_ID_AZURE,
|
||||
TENANT_ID_AZURE,
|
||||
CLIENT_ID_HEROKU,
|
||||
CLIENT_ID_VERCEL,
|
||||
CLIENT_ID_NETLIFY,
|
||||
CLIENT_ID_GITHUB,
|
||||
CLIENT_SECRET_AZURE,
|
||||
CLIENT_SECRET_HEROKU,
|
||||
CLIENT_SECRET_VERCEL,
|
||||
CLIENT_SECRET_NETLIFY,
|
||||
|
@ -4,16 +4,21 @@ import jwt from 'jsonwebtoken';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import * as bigintConversion from 'bigint-conversion';
|
||||
const jsrp = require('jsrp');
|
||||
import { User } from '../../models';
|
||||
import { User, LoginSRPDetail } from '../../models';
|
||||
import { createToken, issueTokens, clearTokens } from '../../helpers/auth';
|
||||
import {
|
||||
ACTION_LOGIN,
|
||||
ACTION_LOGOUT
|
||||
} from '../../variables';
|
||||
import {
|
||||
NODE_ENV,
|
||||
JWT_AUTH_LIFETIME,
|
||||
JWT_AUTH_SECRET,
|
||||
JWT_REFRESH_SECRET
|
||||
} from '../../config';
|
||||
import LoginSRPDetail from '../../models/LoginSRPDetail';
|
||||
import { BadRequestError } from '../../utils/errors';
|
||||
import { EELogService } from '../../ee/services';
|
||||
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
|
||||
|
||||
declare module 'jsonwebtoken' {
|
||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||
@ -116,6 +121,18 @@ export const login2 = async (req: Request, res: Response) => {
|
||||
secure: NODE_ENV === 'production' ? true : false
|
||||
});
|
||||
|
||||
const loginAction = await EELogService.createAction({
|
||||
name: ACTION_LOGIN,
|
||||
userId: user._id
|
||||
});
|
||||
|
||||
loginAction && await EELogService.createLog({
|
||||
userId: user._id,
|
||||
actions: [loginAction],
|
||||
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
// return (access) token in response
|
||||
return res.status(200).send({
|
||||
token: tokens.token,
|
||||
@ -159,6 +176,19 @@ export const logout = async (req: Request, res: Response) => {
|
||||
sameSite: 'strict',
|
||||
secure: NODE_ENV === 'production' ? true : false
|
||||
});
|
||||
|
||||
const logoutAction = await EELogService.createAction({
|
||||
name: ACTION_LOGOUT,
|
||||
userId: req.user._id
|
||||
});
|
||||
|
||||
logoutAction && await EELogService.createLog({
|
||||
userId: req.user._id,
|
||||
actions: [logoutAction],
|
||||
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
|
@ -10,6 +10,31 @@ import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
|
||||
import { IntegrationService } from '../../services';
|
||||
import { getApps, revokeAccess } from '../../integrations';
|
||||
|
||||
/***
|
||||
* Return integration authorization with id [integrationAuthId]
|
||||
*/
|
||||
export const getIntegrationAuth = async (req: Request, res: Response) => {
|
||||
let integrationAuth;
|
||||
try {
|
||||
const { integrationAuthId } = req.params;
|
||||
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
||||
|
||||
if (!integrationAuth) return res.status(400).send({
|
||||
message: 'Failed to find integration authorization'
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get integration authorization'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
integrationAuth
|
||||
});
|
||||
}
|
||||
|
||||
export const getIntegrationOptions = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
@ -31,7 +56,6 @@ export const oAuthExchange = async (
|
||||
) => {
|
||||
try {
|
||||
const { workspaceId, code, integration } = req.body;
|
||||
|
||||
if (!INTEGRATION_SET.has(integration))
|
||||
throw new Error('Failed to validate integration');
|
||||
|
||||
@ -40,12 +64,16 @@ export const oAuthExchange = async (
|
||||
throw new Error("Failed to get environments")
|
||||
}
|
||||
|
||||
await IntegrationService.handleOAuthExchange({
|
||||
const integrationAuth = await IntegrationService.handleOAuthExchange({
|
||||
workspaceId,
|
||||
integration,
|
||||
code,
|
||||
environment: environments[0].slug,
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
integrationAuth
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
@ -53,10 +81,6 @@ export const oAuthExchange = async (
|
||||
message: 'Failed to get OAuth2 code-token exchange'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully enabled integration authorization'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@ -81,6 +105,13 @@ export const saveIntegrationAccessToken = async (
|
||||
integration: string;
|
||||
} = req.body;
|
||||
|
||||
const bot = await Bot.findOne({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!bot) throw new Error('Bot must be enabled to save integration access token');
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
integration
|
||||
@ -91,13 +122,6 @@ export const saveIntegrationAccessToken = async (
|
||||
new: true,
|
||||
upsert: true
|
||||
});
|
||||
|
||||
const bot = await Bot.findOne({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!bot) throw new Error('Bot must be enabled to save integration access token');
|
||||
|
||||
// encrypt and save integration access token
|
||||
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { Types } from 'mongoose';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Integration,
|
||||
@ -18,15 +19,40 @@ import { eventPushSecrets } from '../../events';
|
||||
export const createIntegration = async (req: Request, res: Response) => {
|
||||
let integration;
|
||||
try {
|
||||
const {
|
||||
integrationAuthId,
|
||||
app,
|
||||
appId,
|
||||
isActive,
|
||||
sourceEnvironment,
|
||||
targetEnvironment,
|
||||
owner
|
||||
} = req.body;
|
||||
|
||||
// TODO: validate [sourceEnvironment] and [targetEnvironment]
|
||||
|
||||
// initialize new integration after saving integration access token
|
||||
integration = await new Integration({
|
||||
workspace: req.integrationAuth.workspace._id,
|
||||
isActive: false,
|
||||
app: null,
|
||||
environment: req.integrationAuth.workspace?.environments[0].slug,
|
||||
environment: sourceEnvironment,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner,
|
||||
integration: req.integrationAuth.integration,
|
||||
integrationAuth: req.integrationAuth._id
|
||||
integrationAuth: new Types.ObjectId(integrationAuthId)
|
||||
}).save();
|
||||
|
||||
if (integration) {
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: integration.workspace.toString()
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
|
@ -77,8 +77,6 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
|
||||
// change role for (target) organization membership with id
|
||||
// [membershipOrgId]
|
||||
|
||||
// TODO
|
||||
|
||||
let membershipToChangeRole;
|
||||
// try {
|
||||
// } catch (err) {
|
||||
|
@ -4,12 +4,11 @@ import crypto from 'crypto';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const jsrp = require('jsrp');
|
||||
import * as bigintConversion from 'bigint-conversion';
|
||||
import { User, Token, BackupPrivateKey } from '../../models';
|
||||
import { User, Token, BackupPrivateKey, LoginSRPDetail } from '../../models';
|
||||
import { checkEmailVerification } from '../../helpers/signup';
|
||||
import { createToken } from '../../helpers/auth';
|
||||
import { sendMail } from '../../helpers/nodemailer';
|
||||
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
|
||||
import LoginSRPDetail from '../../models/LoginSRPDetail';
|
||||
import { BadRequestError } from '../../utils/errors';
|
||||
|
||||
/**
|
||||
|
@ -6,6 +6,7 @@ import * as apiKeyDataController from './apiKeyDataController';
|
||||
import * as secretController from './secretController';
|
||||
import * as secretsController from './secretsController';
|
||||
import * as environmentController from './environmentController';
|
||||
import * as tagController from './tagController';
|
||||
|
||||
export {
|
||||
usersController,
|
||||
@ -15,5 +16,6 @@ export {
|
||||
apiKeyDataController,
|
||||
secretController,
|
||||
secretsController,
|
||||
environmentController
|
||||
environmentController,
|
||||
tagController
|
||||
}
|
||||
|
@ -79,24 +79,38 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
*/
|
||||
|
||||
const channel = getChannelFromUserAgent(req.headers['user-agent'])
|
||||
const { workspaceId, environment } = req.body;
|
||||
const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body;
|
||||
|
||||
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_WRITE)
|
||||
if (!hasAccess) {
|
||||
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
|
||||
}
|
||||
|
||||
let toAdd;
|
||||
let listOfSecretsToCreate;
|
||||
if (Array.isArray(req.body.secrets)) {
|
||||
// case: create multiple secrets
|
||||
toAdd = req.body.secrets;
|
||||
listOfSecretsToCreate = req.body.secrets;
|
||||
} else if (typeof req.body.secrets === 'object') {
|
||||
// case: create 1 secret
|
||||
toAdd = [req.body.secrets];
|
||||
listOfSecretsToCreate = [req.body.secrets];
|
||||
}
|
||||
|
||||
const newSecrets = await Secret.insertMany(
|
||||
toAdd.map(({
|
||||
type secretsToCreateType = {
|
||||
type: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretCommentCiphertext: string;
|
||||
secretCommentIV: string;
|
||||
secretCommentTag: string;
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
const newlyCreatedSecrets = await Secret.insertMany(
|
||||
listOfSecretsToCreate.map(({
|
||||
type,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
@ -104,15 +118,11 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
}: {
|
||||
type: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
}) => {
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
tags
|
||||
}: secretsToCreateType) => {
|
||||
return ({
|
||||
version: 1,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
@ -124,7 +134,11 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
tags
|
||||
});
|
||||
})
|
||||
);
|
||||
@ -140,7 +154,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
|
||||
// (EE) add secret versions for new secrets
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: newSecrets.map(({
|
||||
secretVersions: newlyCreatedSecrets.map(({
|
||||
_id,
|
||||
version,
|
||||
workspace,
|
||||
@ -154,7 +168,11 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
secretValueHash,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
tags
|
||||
}) => ({
|
||||
_id: new Types.ObjectId(),
|
||||
secret: _id,
|
||||
@ -171,21 +189,25 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
secretValueHash,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
tags
|
||||
}))
|
||||
});
|
||||
|
||||
const addAction = await EELogService.createActionSecret({
|
||||
const addAction = await EELogService.createAction({
|
||||
name: ACTION_ADD_SECRETS,
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId,
|
||||
secretIds: newSecrets.map((n) => n._id)
|
||||
userId: req.user._id,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
secretIds: newlyCreatedSecrets.map((n) => n._id)
|
||||
});
|
||||
|
||||
// (EE) create (audit) log
|
||||
addAction && await EELogService.createLog({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
actions: [addAction],
|
||||
channel,
|
||||
ipAddress: req.ip
|
||||
@ -201,7 +223,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
event: 'secrets added',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: toAdd.length,
|
||||
numberOfSecrets: listOfSecretsToCreate.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel,
|
||||
@ -211,7 +233,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secrets: newSecrets
|
||||
secrets: newlyCreatedSecrets
|
||||
});
|
||||
}
|
||||
|
||||
@ -294,22 +316,22 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
],
|
||||
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
||||
}
|
||||
).then())
|
||||
).populate("tags").then())
|
||||
|
||||
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
|
||||
|
||||
const channel = getChannelFromUserAgent(req.headers['user-agent'])
|
||||
|
||||
const readAction = await EELogService.createActionSecret({
|
||||
const readAction = await EELogService.createAction({
|
||||
name: ACTION_READ_SECRETS,
|
||||
userId: userId,
|
||||
workspaceId: workspaceId as string,
|
||||
userId: new Types.ObjectId(userId),
|
||||
workspaceId: new Types.ObjectId(workspaceId as string),
|
||||
secretIds: secrets.map((n: any) => n._id)
|
||||
});
|
||||
|
||||
readAction && await EELogService.createLog({
|
||||
userId: userId,
|
||||
workspaceId: workspaceId as string,
|
||||
userId: new Types.ObjectId(userId),
|
||||
workspaceId: new Types.ObjectId(workspaceId as string),
|
||||
actions: [readAction],
|
||||
channel,
|
||||
ipAddress: req.ip
|
||||
@ -398,6 +420,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
secretCommentCiphertext: string;
|
||||
secretCommentIV: string;
|
||||
secretCommentTag: string;
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => {
|
||||
@ -410,7 +433,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag
|
||||
secretCommentTag,
|
||||
tags
|
||||
} = secret;
|
||||
|
||||
return ({
|
||||
@ -426,6 +450,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
tags,
|
||||
...((
|
||||
secretCommentCiphertext &&
|
||||
secretCommentIV &&
|
||||
@ -460,6 +485,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
tags
|
||||
} = secretModificationsBySecretId[secret._id.toString()]
|
||||
|
||||
return ({
|
||||
@ -477,6 +503,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext,
|
||||
secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
|
||||
secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
|
||||
tags: tags ? tags : secret.tags
|
||||
});
|
||||
})
|
||||
}
|
||||
@ -505,17 +532,17 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
const updateAction = await EELogService.createActionSecret({
|
||||
const updateAction = await EELogService.createAction({
|
||||
name: ACTION_UPDATE_SECRETS,
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: key,
|
||||
userId: req.user._id,
|
||||
workspaceId: new Types.ObjectId(key),
|
||||
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
|
||||
});
|
||||
|
||||
// (EE) create (audit) log
|
||||
updateAction && await EELogService.createLog({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: key,
|
||||
workspaceId: new Types.ObjectId(key),
|
||||
actions: [updateAction],
|
||||
channel,
|
||||
ipAddress: req.ip
|
||||
@ -631,17 +658,17 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
||||
workspaceId: key
|
||||
})
|
||||
});
|
||||
const deleteAction = await EELogService.createActionSecret({
|
||||
const deleteAction = await EELogService.createAction({
|
||||
name: ACTION_DELETE_SECRETS,
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: key,
|
||||
userId: req.user._id,
|
||||
workspaceId: new Types.ObjectId(key),
|
||||
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
|
||||
});
|
||||
|
||||
// (EE) create (audit) log
|
||||
deleteAction && await EELogService.createLog({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: key,
|
||||
workspaceId: new Types.ObjectId(key),
|
||||
actions: [deleteAction],
|
||||
channel,
|
||||
ipAddress: req.ip
|
||||
|
66
backend/src/controllers/v2/tagController.ts
Normal file
66
backend/src/controllers/v2/tagController.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
Membership,
|
||||
} from '../../models';
|
||||
import Tag, { ITag } from '../../models/tag';
|
||||
import { Builder } from "builder-pattern"
|
||||
import to from 'await-to-js';
|
||||
import { BadRequestError, UnauthorizedRequestError } from '../../utils/errors';
|
||||
import { MongoError } from 'mongodb';
|
||||
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
|
||||
|
||||
export const createWorkspaceTag = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params
|
||||
const { name, slug } = req.body
|
||||
const sanitizedTagToCreate = Builder<ITag>()
|
||||
.name(name)
|
||||
.workspace(new Types.ObjectId(workspaceId))
|
||||
.slug(slug)
|
||||
.user(new Types.ObjectId(req.user._id))
|
||||
.build();
|
||||
|
||||
const [err, createdTag] = await to(Tag.create(sanitizedTagToCreate))
|
||||
|
||||
if (err) {
|
||||
if ((err as MongoError).code === 11000) {
|
||||
throw BadRequestError({ message: "Tags must be unique in a workspace" })
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
res.json(createdTag)
|
||||
}
|
||||
|
||||
export const deleteWorkspaceTag = async (req: Request, res: Response) => {
|
||||
const { tagId } = req.params
|
||||
|
||||
const tagFromDB = await Tag.findById(tagId)
|
||||
if (!tagFromDB) {
|
||||
throw BadRequestError()
|
||||
}
|
||||
|
||||
// can only delete if the request user is one that belongs to the same workspace as the tag
|
||||
const membership = await Membership.findOne({
|
||||
user: req.user,
|
||||
workspace: tagFromDB.workspace
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
UnauthorizedRequestError({ message: 'Failed to validate membership' });
|
||||
}
|
||||
|
||||
const result = await Tag.findByIdAndDelete(tagId);
|
||||
|
||||
res.json(result);
|
||||
}
|
||||
|
||||
export const getWorkspaceTags = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params
|
||||
const workspaceTags = await Tag.find({ workspace: workspaceId })
|
||||
return res.json({
|
||||
workspaceTags
|
||||
})
|
||||
}
|
@ -15,7 +15,13 @@ export const getSecretSnapshot = async (req: Request, res: Response) => {
|
||||
|
||||
secretSnapshot = await SecretSnapshot
|
||||
.findById(secretSnapshotId)
|
||||
.populate('secretVersions');
|
||||
.populate({
|
||||
path: 'secretVersions',
|
||||
populate: {
|
||||
path: 'tags',
|
||||
model: 'Tag',
|
||||
}
|
||||
});
|
||||
|
||||
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
|
||||
|
||||
|
@ -1,39 +1,40 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import { SecretVersion, Action } from '../models';
|
||||
import { Action } from '../models';
|
||||
import {
|
||||
getLatestSecretVersionIds,
|
||||
getLatestNSecretSecretVersionIds
|
||||
} from '../helpers/secretVersion';
|
||||
import { ACTION_UPDATE_SECRETS } from '../../variables';
|
||||
import {
|
||||
ACTION_LOGIN,
|
||||
ACTION_LOGOUT,
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_READ_SECRETS,
|
||||
ACTION_DELETE_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
} from '../../variables';
|
||||
|
||||
/**
|
||||
* Create an (audit) action for secrets including
|
||||
* add, delete, update, and read actions.
|
||||
* Create an (audit) action for updating secrets
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.name - name of action
|
||||
* @param {ObjectId[]} obj.secretIds - ids of relevant secrets
|
||||
* @param {Types.ObjectId} obj.secretIds - ids of relevant secrets
|
||||
* @returns {Action} action - new action
|
||||
*/
|
||||
const createActionSecretHelper = async ({
|
||||
const createActionUpdateSecret = async ({
|
||||
name,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
}: {
|
||||
name: string;
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
userId: Types.ObjectId;
|
||||
workspaceId: Types.ObjectId;
|
||||
secretIds: Types.ObjectId[];
|
||||
}) => {
|
||||
|
||||
let action;
|
||||
let latestSecretVersions;
|
||||
try {
|
||||
if (name === ACTION_UPDATE_SECRETS) {
|
||||
// case: action is updating secrets
|
||||
// -> add old and new secret versions
|
||||
latestSecretVersions = (await getLatestNSecretSecretVersionIds({
|
||||
const latestSecretVersions = (await getLatestNSecretSecretVersionIds({
|
||||
secretIds,
|
||||
n: 2
|
||||
}))
|
||||
@ -41,17 +42,7 @@ const createActionSecretHelper = async ({
|
||||
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 getLatestSecretVersionIds({
|
||||
secretIds
|
||||
}))
|
||||
.map((s) => ({
|
||||
newSecretVersion: s.versionId
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
action = await new Action({
|
||||
name,
|
||||
user: userId,
|
||||
@ -64,10 +55,148 @@ const createActionSecretHelper = async ({
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create update secret action');
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an (audit) action for creating, reading, and deleting
|
||||
* secrets
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.name - name of action
|
||||
* @param {Types.ObjectId} obj.secretIds - ids of relevant secrets
|
||||
* @returns {Action} action - new action
|
||||
*/
|
||||
const createActionSecret = async ({
|
||||
name,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
}: {
|
||||
name: string;
|
||||
userId: Types.ObjectId;
|
||||
workspaceId: Types.ObjectId;
|
||||
secretIds: Types.ObjectId[];
|
||||
}) => {
|
||||
let action;
|
||||
try {
|
||||
// case: action is adding, deleting, or reading secrets
|
||||
// -> add new secret versions
|
||||
const latestSecretVersions = (await getLatestSecretVersionIds({
|
||||
secretIds
|
||||
}))
|
||||
.map((s) => ({
|
||||
newSecretVersion: s.versionId
|
||||
}));
|
||||
|
||||
action = await new Action({
|
||||
name,
|
||||
user: userId,
|
||||
workspace: workspaceId,
|
||||
payload: {
|
||||
secretVersions: latestSecretVersions
|
||||
}
|
||||
}).save();
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create action create/read/delete secret action');
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an (audit) action for user with id [userId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.name - name of action
|
||||
* @param {String} obj.userId - id of user associated with action
|
||||
* @returns
|
||||
*/
|
||||
const createActionUser = ({
|
||||
name,
|
||||
userId
|
||||
}: {
|
||||
name: string;
|
||||
userId: Types.ObjectId;
|
||||
}) => {
|
||||
let action;
|
||||
try {
|
||||
action = new Action({
|
||||
name,
|
||||
user: userId
|
||||
}).save();
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create user action');
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an (audit) action.
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.name - name of action
|
||||
* @param {Types.ObjectId} obj.userId - id of user associated with action
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace associated with action
|
||||
* @param {Types.ObjectId[]} obj.secretIds - ids of secrets associated with action
|
||||
*/
|
||||
const createActionHelper = async ({
|
||||
name,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds,
|
||||
}: {
|
||||
name: string;
|
||||
userId: Types.ObjectId;
|
||||
workspaceId?: Types.ObjectId;
|
||||
secretIds?: Types.ObjectId[];
|
||||
}) => {
|
||||
let action;
|
||||
try {
|
||||
switch (name) {
|
||||
case ACTION_LOGIN:
|
||||
case ACTION_LOGOUT:
|
||||
action = await createActionUser({
|
||||
name,
|
||||
userId
|
||||
});
|
||||
break;
|
||||
case ACTION_ADD_SECRETS:
|
||||
case ACTION_READ_SECRETS:
|
||||
case ACTION_DELETE_SECRETS:
|
||||
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
|
||||
action = await createActionSecret({
|
||||
name,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
});
|
||||
break;
|
||||
case ACTION_UPDATE_SECRETS:
|
||||
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
|
||||
action = await createActionUpdateSecret({
|
||||
name,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create action');
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
export { createActionSecretHelper };
|
||||
export {
|
||||
createActionHelper
|
||||
};
|
@ -1,9 +1,19 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
Log,
|
||||
IAction
|
||||
} from '../models';
|
||||
|
||||
/**
|
||||
* Create an (audit) log
|
||||
* @param {Object} obj
|
||||
* @param {Types.ObjectId} obj.userId - id of user associated with the log
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace associated with the log
|
||||
* @param {IAction[]} obj.actions - actions to include in log
|
||||
* @param {String} obj.channel - channel (web/cli/auto) associated with the log
|
||||
* @param {String} obj.ipAddress - ip address associated with the log
|
||||
* @returns {Log} log - new audit log
|
||||
*/
|
||||
const createLogHelper = async ({
|
||||
userId,
|
||||
workspaceId,
|
||||
@ -11,8 +21,8 @@ const createLogHelper = async ({
|
||||
channel,
|
||||
ipAddress
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
userId: Types.ObjectId;
|
||||
workspaceId?: Types.ObjectId;
|
||||
actions: IAction[];
|
||||
channel: string;
|
||||
ipAddress: string;
|
||||
@ -21,7 +31,7 @@ const createLogHelper = async ({
|
||||
try {
|
||||
log = await new Log({
|
||||
user: userId,
|
||||
workspace: workspaceId,
|
||||
workspace: workspaceId ?? undefined,
|
||||
actionNames: actions.map((a) => a.name),
|
||||
actions,
|
||||
channel,
|
||||
|
@ -1,10 +1,18 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
import {
|
||||
ACTION_LOGIN,
|
||||
ACTION_LOGOUT,
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_READ_SECRETS,
|
||||
ACTION_DELETE_SECRETS
|
||||
} from '../../variables';
|
||||
|
||||
export interface IAction {
|
||||
name: string;
|
||||
user?: Types.ObjectId,
|
||||
workspace?: Types.ObjectId,
|
||||
payload: {
|
||||
payload?: {
|
||||
secretVersions?: Types.ObjectId[]
|
||||
}
|
||||
}
|
||||
@ -13,7 +21,15 @@ const actionSchema = new Schema<IAction>(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
required: true,
|
||||
enum: [
|
||||
ACTION_LOGIN,
|
||||
ACTION_LOGOUT,
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_READ_SECRETS,
|
||||
ACTION_DELETE_SECRETS
|
||||
]
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
import {
|
||||
ACTION_LOGIN,
|
||||
ACTION_LOGOUT,
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_READ_SECRETS,
|
||||
@ -29,6 +31,8 @@ const logSchema = new Schema<ILog>(
|
||||
actionNames: {
|
||||
type: [String],
|
||||
enum: [
|
||||
ACTION_LOGIN,
|
||||
ACTION_LOGOUT,
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_READ_SECRETS,
|
||||
|
@ -21,6 +21,7 @@ export interface ISecretVersion {
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHash: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
const secretVersionSchema = new Schema<ISecretVersion>(
|
||||
@ -88,7 +89,12 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
||||
},
|
||||
secretValueHash: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
tags: {
|
||||
ref: 'Tag',
|
||||
type: [Schema.Types.ObjectId],
|
||||
default: []
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
|
@ -1,14 +1,12 @@
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
Log,
|
||||
Action,
|
||||
IAction
|
||||
} from '../models';
|
||||
import {
|
||||
createLogHelper
|
||||
} from '../helpers/log';
|
||||
import {
|
||||
createActionSecretHelper
|
||||
createActionHelper
|
||||
} from '../helpers/action';
|
||||
import EELicenseService from './EELicenseService';
|
||||
|
||||
@ -33,8 +31,8 @@ class EELogService {
|
||||
channel,
|
||||
ipAddress
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
userId: Types.ObjectId;
|
||||
workspaceId?: Types.ObjectId;
|
||||
actions: IAction[];
|
||||
channel: string;
|
||||
ipAddress: string;
|
||||
@ -50,26 +48,26 @@ class EELogService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an (audit) action for secrets including
|
||||
* add, delete, update, and read actions.
|
||||
* Create an (audit) action
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.name - name of action
|
||||
* @param {ObjectId[]} obj.secretIds - secret ids
|
||||
* @param {Types.ObjectId} obj.userId - id of user associated with the action
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace associated with the action
|
||||
* @param {ObjectId[]} obj.secretIds - ids of secrets associated with the action
|
||||
* @returns {Action} action - new action
|
||||
*/
|
||||
static async createActionSecret({
|
||||
static async createAction({
|
||||
name,
|
||||
userId,
|
||||
workspaceId,
|
||||
secretIds
|
||||
}: {
|
||||
name: string;
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
secretIds: Types.ObjectId[];
|
||||
userId: Types.ObjectId;
|
||||
workspaceId?: Types.ObjectId;
|
||||
secretIds?: Types.ObjectId[];
|
||||
}) {
|
||||
if (!EELicenseService.isLicenseValid) return null;
|
||||
return await createActionSecretHelper({
|
||||
return await createActionHelper({
|
||||
name,
|
||||
userId,
|
||||
workspaceId,
|
||||
|
@ -30,6 +30,7 @@ interface Update {
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.integration - name of integration
|
||||
* @param {String} obj.code - code
|
||||
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
|
||||
*/
|
||||
const handleOAuthExchangeHelper = async ({
|
||||
workspaceId,
|
||||
@ -42,7 +43,6 @@ const handleOAuthExchangeHelper = async ({
|
||||
code: string;
|
||||
environment: string;
|
||||
}) => {
|
||||
let action;
|
||||
let integrationAuth;
|
||||
try {
|
||||
const bot = await Bot.findOne({
|
||||
@ -98,21 +98,13 @@ const handleOAuthExchangeHelper = async ({
|
||||
accessExpiresAt: res.accessExpiresAt
|
||||
});
|
||||
}
|
||||
|
||||
// initialize new integration after exchange
|
||||
await new Integration({
|
||||
workspace: workspaceId,
|
||||
isActive: false,
|
||||
app: null,
|
||||
environment,
|
||||
integration,
|
||||
integrationAuth: integrationAuth._id
|
||||
}).save();
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to handle OAuth2 code-token exchange')
|
||||
}
|
||||
|
||||
return integrationAuth;
|
||||
}
|
||||
/**
|
||||
* Sync/push environment variables in workspace with id [workspaceId] to
|
||||
|
@ -406,10 +406,10 @@ const v2PushSecrets = async ({
|
||||
secretIds: toDelete
|
||||
});
|
||||
|
||||
const deleteAction = await EELogService.createActionSecret({
|
||||
const deleteAction = await EELogService.createAction({
|
||||
name: ACTION_DELETE_SECRETS,
|
||||
userId,
|
||||
workspaceId,
|
||||
userId: new Types.ObjectId(userId),
|
||||
workspaceId: new Types.ObjectId(userId),
|
||||
secretIds: toDelete
|
||||
});
|
||||
|
||||
@ -499,10 +499,10 @@ const v2PushSecrets = async ({
|
||||
})
|
||||
});
|
||||
|
||||
const updateAction = await EELogService.createActionSecret({
|
||||
const updateAction = await EELogService.createAction({
|
||||
name: ACTION_UPDATE_SECRETS,
|
||||
userId,
|
||||
workspaceId,
|
||||
userId: new Types.ObjectId(userId),
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
secretIds: toUpdate.map((u) => u._id)
|
||||
});
|
||||
|
||||
@ -536,10 +536,10 @@ const v2PushSecrets = async ({
|
||||
})
|
||||
});
|
||||
|
||||
const addAction = await EELogService.createActionSecret({
|
||||
const addAction = await EELogService.createAction({
|
||||
name: ACTION_ADD_SECRETS,
|
||||
userId,
|
||||
workspaceId,
|
||||
userId: new Types.ObjectId(userId),
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
secretIds: newSecrets.map((n) => n._id)
|
||||
});
|
||||
addAction && actions.push(addAction);
|
||||
@ -553,8 +553,8 @@ const v2PushSecrets = async ({
|
||||
// (EE) create (audit) log
|
||||
if (actions.length > 0) {
|
||||
await EELogService.createLog({
|
||||
userId,
|
||||
workspaceId,
|
||||
userId: new Types.ObjectId(userId),
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
actions,
|
||||
channel,
|
||||
ipAddress
|
||||
@ -645,16 +645,16 @@ const pullSecrets = async ({
|
||||
environment
|
||||
})
|
||||
|
||||
const readAction = await EELogService.createActionSecret({
|
||||
const readAction = await EELogService.createAction({
|
||||
name: ACTION_READ_SECRETS,
|
||||
userId,
|
||||
workspaceId,
|
||||
userId: new Types.ObjectId(userId),
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
secretIds: secrets.map((n: any) => n._id)
|
||||
});
|
||||
|
||||
readAction && await EELogService.createLog({
|
||||
userId,
|
||||
workspaceId,
|
||||
userId: new Types.ObjectId(userId),
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
actions: [readAction],
|
||||
channel,
|
||||
ipAddress
|
||||
|
@ -3,6 +3,7 @@ import * as Sentry from '@sentry/node';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { IIntegrationAuth } from '../models';
|
||||
import {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
@ -40,6 +41,11 @@ const getApps = async ({
|
||||
let apps: App[];
|
||||
try {
|
||||
switch (integrationAuth.integration) {
|
||||
case INTEGRATION_AZURE_KEY_VAULT:
|
||||
apps = await getAppsAzureKeyVault({
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_HEROKU:
|
||||
apps = await getAppsHeroku({
|
||||
accessToken
|
||||
@ -81,6 +87,15 @@ const getApps = async ({
|
||||
return apps;
|
||||
};
|
||||
|
||||
const getAppsAzureKeyVault = async ({
|
||||
accessToken
|
||||
}: {
|
||||
accessToken: string;
|
||||
}) => {
|
||||
// TODO
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of apps for Heroku integration
|
||||
* @param {Object} obj
|
||||
@ -247,7 +262,9 @@ const getAppsRender = async ({
|
||||
const res = (
|
||||
await axios.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'Accept-Encoding': 'application/json'
|
||||
}
|
||||
})
|
||||
).data;
|
||||
@ -257,6 +274,7 @@ const getAppsRender = async ({
|
||||
name: a.service.name,
|
||||
appId: a.service.id
|
||||
}));
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
@ -296,7 +314,9 @@ const getAppsFlyio = async ({
|
||||
url: INTEGRATION_FLYIO_API_URL,
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + accessToken
|
||||
'Authorization': 'Bearer ' + accessToken,
|
||||
'Accept': 'application/json',
|
||||
'Accept-Encoding': 'application/json'
|
||||
},
|
||||
data: {
|
||||
query,
|
||||
|
@ -1,10 +1,12 @@
|
||||
import axios from 'axios';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_AZURE_TOKEN_URL,
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
@ -12,15 +14,27 @@ import {
|
||||
} from '../variables';
|
||||
import {
|
||||
SITE_URL,
|
||||
CLIENT_ID_AZURE,
|
||||
CLIENT_ID_VERCEL,
|
||||
CLIENT_ID_NETLIFY,
|
||||
CLIENT_ID_GITHUB,
|
||||
CLIENT_SECRET_AZURE,
|
||||
CLIENT_SECRET_HEROKU,
|
||||
CLIENT_SECRET_VERCEL,
|
||||
CLIENT_SECRET_NETLIFY,
|
||||
CLIENT_SECRET_GITHUB
|
||||
} from '../config';
|
||||
|
||||
interface ExchangeCodeAzureResponse {
|
||||
token_type: string;
|
||||
scope: string;
|
||||
expires_in: number;
|
||||
ext_expires_in: number;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
id_token: string;
|
||||
}
|
||||
|
||||
interface ExchangeCodeHerokuResponse {
|
||||
token_type: string;
|
||||
access_token: string;
|
||||
@ -75,6 +89,11 @@ const exchangeCode = async ({
|
||||
|
||||
try {
|
||||
switch (integration) {
|
||||
case INTEGRATION_AZURE_KEY_VAULT:
|
||||
obj = await exchangeCodeAzure({
|
||||
code
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_HEROKU:
|
||||
obj = await exchangeCodeHeroku({
|
||||
code
|
||||
@ -105,6 +124,46 @@ const exchangeCode = async ({
|
||||
return obj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return [accessToken] for Azure OAuth2 code-token exchange
|
||||
* @param param0
|
||||
*/
|
||||
const exchangeCodeAzure = async ({
|
||||
code
|
||||
}: {
|
||||
code: string;
|
||||
}) => {
|
||||
const accessExpiresAt = new Date();
|
||||
let res: ExchangeCodeAzureResponse;
|
||||
try {
|
||||
res = (await axios.post(
|
||||
INTEGRATION_AZURE_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
scope: 'https://vault.azure.net/.default openid offline_access', // TODO: do we need all these permissions?
|
||||
client_id: CLIENT_ID_AZURE,
|
||||
client_secret: CLIENT_SECRET_AZURE,
|
||||
redirect_uri: `${SITE_URL}/integrations/azure-key-vault/oauth2/callback`
|
||||
} as any)
|
||||
)).data;
|
||||
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + res.expires_in
|
||||
);
|
||||
} catch (err: any) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange with Azure');
|
||||
}
|
||||
|
||||
return ({
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
accessExpiresAt
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
|
||||
* OAuth2 code-token exchange
|
||||
@ -168,7 +227,7 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
|
||||
code: code,
|
||||
client_id: CLIENT_ID_VERCEL,
|
||||
client_secret: CLIENT_SECRET_VERCEL,
|
||||
redirect_uri: `${SITE_URL}/vercel`
|
||||
redirect_uri: `${SITE_URL}/integrations/vercel/oauth2/callback`
|
||||
} as any)
|
||||
)
|
||||
).data;
|
||||
@ -208,7 +267,7 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => {
|
||||
code: code,
|
||||
client_id: CLIENT_ID_NETLIFY,
|
||||
client_secret: CLIENT_SECRET_NETLIFY,
|
||||
redirect_uri: `${SITE_URL}/netlify`
|
||||
redirect_uri: `${SITE_URL}/integrations/netlify/oauth2/callback`
|
||||
} as any)
|
||||
)
|
||||
).data;
|
||||
@ -260,10 +319,11 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
|
||||
client_id: CLIENT_ID_GITHUB,
|
||||
client_secret: CLIENT_SECRET_GITHUB,
|
||||
code: code,
|
||||
redirect_uri: `${SITE_URL}/github`
|
||||
redirect_uri: `${SITE_URL}/integrations/github/oauth2/callback`
|
||||
},
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Accept-Encoding': 'application/json'
|
||||
}
|
||||
})
|
||||
).data;
|
||||
|
@ -1,13 +1,26 @@
|
||||
import axios from 'axios';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { INTEGRATION_HEROKU } from '../variables';
|
||||
import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU } from '../variables';
|
||||
import {
|
||||
CLIENT_SECRET_HEROKU
|
||||
SITE_URL,
|
||||
CLIENT_ID_AZURE,
|
||||
CLIENT_SECRET_AZURE,
|
||||
CLIENT_SECRET_HEROKU
|
||||
} from '../config';
|
||||
import {
|
||||
INTEGRATION_HEROKU_TOKEN_URL
|
||||
INTEGRATION_AZURE_TOKEN_URL,
|
||||
INTEGRATION_HEROKU_TOKEN_URL
|
||||
} from '../variables';
|
||||
|
||||
interface RefreshTokenAzureResponse {
|
||||
token_type: string;
|
||||
scope: string;
|
||||
expires_in: number;
|
||||
ext_expires_in: 4871;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return new access token by exchanging refresh token [refreshToken] for integration
|
||||
* named [integration]
|
||||
@ -25,6 +38,11 @@ const exchangeRefresh = async ({
|
||||
let accessToken;
|
||||
try {
|
||||
switch (integration) {
|
||||
case INTEGRATION_AZURE_KEY_VAULT:
|
||||
accessToken = await exchangeRefreshAzure({
|
||||
refreshToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_HEROKU:
|
||||
accessToken = await exchangeRefreshHeroku({
|
||||
refreshToken
|
||||
@ -40,6 +58,38 @@ const exchangeRefresh = async ({
|
||||
return accessToken;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||
* Azure integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.refreshToken - refresh token to use to get new access token for Azure
|
||||
* @returns
|
||||
*/
|
||||
const exchangeRefreshAzure = async ({
|
||||
refreshToken
|
||||
}: {
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
try {
|
||||
const res: RefreshTokenAzureResponse = (await axios.post(
|
||||
INTEGRATION_AZURE_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
client_id: CLIENT_ID_AZURE,
|
||||
scope: 'openid offline_access',
|
||||
refresh_token: refreshToken,
|
||||
grant_type: 'refresh_token',
|
||||
client_secret: CLIENT_SECRET_AZURE
|
||||
} as any)
|
||||
)).data;
|
||||
|
||||
return res.access_token;
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get refresh OAuth2 access token for Azure');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||
* Heroku integration
|
||||
@ -52,23 +102,23 @@ const exchangeRefreshHeroku = async ({
|
||||
}: {
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
let accessToken;
|
||||
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors
|
||||
try {
|
||||
const res = await axios.post(
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
client_secret: CLIENT_SECRET_HEROKU
|
||||
} as any)
|
||||
);
|
||||
|
||||
let accessToken;
|
||||
try {
|
||||
const res = await axios.post(
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
client_secret: CLIENT_SECRET_HEROKU
|
||||
} as any)
|
||||
);
|
||||
|
||||
accessToken = res.data.access_token;
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get new OAuth2 access token for Heroku');
|
||||
throw new Error('Failed to refresh OAuth2 access token for Heroku');
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
|
@ -1,11 +1,10 @@
|
||||
import axios from 'axios';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
// import * as sodium from 'libsodium-wrappers';
|
||||
import sodium from 'libsodium-wrappers';
|
||||
// const sodium = require('libsodium-wrappers');
|
||||
import { IIntegration, IIntegrationAuth } from '../models';
|
||||
import {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
@ -18,7 +17,6 @@ import {
|
||||
INTEGRATION_RENDER_API_URL,
|
||||
INTEGRATION_FLYIO_API_URL
|
||||
} from '../variables';
|
||||
import { access, appendFile } from 'fs';
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to [app] in integration named [integration]
|
||||
@ -41,6 +39,13 @@ const syncSecrets = async ({
|
||||
}) => {
|
||||
try {
|
||||
switch (integration.integration) {
|
||||
case INTEGRATION_AZURE_KEY_VAULT:
|
||||
await syncSecretsAzureKeyVault({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_HEROKU:
|
||||
await syncSecretsHeroku({
|
||||
integration,
|
||||
@ -93,6 +98,151 @@ const syncSecrets = async ({
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Azure Key Vault with vault URI [integration.app]
|
||||
* @param {Object} obj
|
||||
* @param {IIntegration} obj.integration - integration details
|
||||
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||
* @param {String} obj.accessToken - access token for Azure Key Vault integration
|
||||
*/
|
||||
const syncSecretsAzureKeyVault = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
}: {
|
||||
integration: IIntegration;
|
||||
secrets: any;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
try {
|
||||
|
||||
interface GetAzureKeyVaultSecret {
|
||||
id: string; // secret URI
|
||||
attributes: {
|
||||
enabled: true,
|
||||
created: number;
|
||||
updated: number;
|
||||
recoveryLevel: string;
|
||||
recoverableDays: number;
|
||||
}
|
||||
}
|
||||
|
||||
interface AzureKeyVaultSecret extends GetAzureKeyVaultSecret {
|
||||
key: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all secrets from Azure Key Vault by paginating through URL [url]
|
||||
* @param {String} url - pagination URL to get next set of secrets from Azure Key Vault
|
||||
* @returns
|
||||
*/
|
||||
const paginateAzureKeyVaultSecrets = async (url: string) => {
|
||||
let result: GetAzureKeyVaultSecret[] = [];
|
||||
|
||||
while (url) {
|
||||
const res = await axios.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
result = result.concat(res.data.value);
|
||||
url = res.data.nextLink;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(`${integration.app}/secrets?api-version=7.3`);
|
||||
|
||||
let lastSlashIndex: number;
|
||||
const res = (await Promise.all(getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => {
|
||||
if (!lastSlashIndex) {
|
||||
lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf('/');
|
||||
}
|
||||
|
||||
const azureKeyVaultSecret = await axios.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
return ({
|
||||
...azureKeyVaultSecret.data,
|
||||
key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1),
|
||||
});
|
||||
})))
|
||||
.reduce((obj: any, secret: any) => ({
|
||||
...obj,
|
||||
[secret.key]: secret
|
||||
}), {});
|
||||
|
||||
const setSecrets: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[] = [];
|
||||
|
||||
Object.keys(secrets).forEach((key) => {
|
||||
const hyphenatedKey = key.replace(/_/g, '-');
|
||||
if (!(hyphenatedKey in res)) {
|
||||
// case: secret has been created
|
||||
setSecrets.push({
|
||||
key: hyphenatedKey,
|
||||
value: secrets[key]
|
||||
});
|
||||
} else {
|
||||
if (secrets[key] !== res[hyphenatedKey].value) {
|
||||
// case: secret has been updated
|
||||
setSecrets.push({
|
||||
key: hyphenatedKey,
|
||||
value: secrets[key]
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const deleteSecrets: AzureKeyVaultSecret[] = [];
|
||||
|
||||
Object.keys(res).forEach((key) => {
|
||||
const underscoredKey = key.replace(/-/g, '_');
|
||||
if (!(underscoredKey in secrets)) {
|
||||
deleteSecrets.push(res[key]);
|
||||
}
|
||||
});
|
||||
|
||||
// Sync/push set secrets
|
||||
if (setSecrets.length > 0) {
|
||||
setSecrets.forEach(async ({ key, value }) => {
|
||||
await axios.put(
|
||||
`${integration.app}/secrets/${key}?api-version=7.3`,
|
||||
{
|
||||
value
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (deleteSecrets.length > 0) {
|
||||
deleteSecrets.forEach(async (secret) => {
|
||||
await axios.delete(`${integration.app}/secrets/${secret.key}?api-version=7.3`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to Azure Key Vault');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Heroku app named [integration.app]
|
||||
* @param {Object} obj
|
||||
|
@ -1,23 +0,0 @@
|
||||
import mongoose, { Schema, model } from 'mongoose';
|
||||
|
||||
const LoginSRPDetailSchema = new Schema(
|
||||
{
|
||||
clientPublicKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
serverBInt: { type: mongoose.Schema.Types.Buffer },
|
||||
expireAt: { type: Date }
|
||||
}
|
||||
);
|
||||
|
||||
const LoginSRPDetail = model('LoginSRPDetail', LoginSRPDetailSchema);
|
||||
|
||||
// LoginSRPDetailSchema.index({ "expireAt": 1 }, { expireAfterSeconds: 0 });
|
||||
|
||||
export default LoginSRPDetail;
|
@ -16,6 +16,7 @@ import UserAction, { IUserAction } from './userAction';
|
||||
import Workspace, { IWorkspace } from './workspace';
|
||||
import ServiceTokenData, { IServiceTokenData } from './serviceTokenData';
|
||||
import APIKeyData, { IAPIKeyData } from './apiKeyData';
|
||||
import LoginSRPDetail, { ILoginSRPDetail } from './loginSRPDetail';
|
||||
|
||||
export {
|
||||
BackupPrivateKey,
|
||||
@ -53,5 +54,7 @@ export {
|
||||
ServiceTokenData,
|
||||
IServiceTokenData,
|
||||
APIKeyData,
|
||||
IAPIKeyData
|
||||
IAPIKeyData,
|
||||
LoginSRPDetail,
|
||||
ILoginSRPDetail
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
import {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
@ -17,7 +18,7 @@ export interface IIntegration {
|
||||
owner: string;
|
||||
targetEnvironment: string;
|
||||
appId: string;
|
||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio';
|
||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault';
|
||||
integrationAuth: Types.ObjectId;
|
||||
}
|
||||
|
||||
@ -59,6 +60,7 @@ const integrationSchema = new Schema<IIntegration>(
|
||||
integration: {
|
||||
type: String,
|
||||
enum: [
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
import {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
@ -9,7 +10,7 @@ import {
|
||||
export interface IIntegrationAuth {
|
||||
_id: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio';
|
||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault';
|
||||
teamId: string;
|
||||
accountId: string;
|
||||
refreshCiphertext?: string;
|
||||
@ -31,6 +32,7 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
||||
integration: {
|
||||
type: String,
|
||||
enum: [
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
|
29
backend/src/models/loginSRPDetail.ts
Normal file
29
backend/src/models/loginSRPDetail.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import mongoose, { Schema, model, Types } from 'mongoose';
|
||||
|
||||
export interface ILoginSRPDetail {
|
||||
_id: Types.ObjectId;
|
||||
clientPublicKey: string;
|
||||
email: string;
|
||||
serverBInt: mongoose.Schema.Types.Buffer;
|
||||
expireAt: Date;
|
||||
}
|
||||
|
||||
const loginSRPDetailSchema = new Schema<ILoginSRPDetail>(
|
||||
{
|
||||
clientPublicKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
serverBInt: { type: mongoose.Schema.Types.Buffer },
|
||||
expireAt: { type: Date }
|
||||
}
|
||||
);
|
||||
|
||||
const LoginSRPDetail = model('LoginSRPDetail', loginSRPDetailSchema);
|
||||
|
||||
export default LoginSRPDetail;
|
@ -23,6 +23,7 @@ export interface ISecret {
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
secretCommentHash?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
const secretSchema = new Schema<ISecret>(
|
||||
@ -47,6 +48,11 @@ const secretSchema = new Schema<ISecret>(
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
tags: {
|
||||
ref: 'Tag',
|
||||
type: [Schema.Types.ObjectId],
|
||||
default: []
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
required: true
|
||||
|
49
backend/src/models/tag.ts
Normal file
49
backend/src/models/tag.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
|
||||
export interface ITag {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
slug: string;
|
||||
user: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
}
|
||||
|
||||
const tagSchema = new Schema<ITag>(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
lowercase: true,
|
||||
validate: [
|
||||
function (value: any) {
|
||||
return value.indexOf(' ') === -1;
|
||||
},
|
||||
'slug cannot contain spaces'
|
||||
]
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace'
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
tagSchema.index({ slug: 1, workspace: 1 }, { unique: true })
|
||||
tagSchema.index({ workspace: 1 })
|
||||
|
||||
const Tag = model<ITag>('Tag', tagSchema);
|
||||
|
||||
export default Tag;
|
@ -12,6 +12,7 @@ export interface IUser {
|
||||
salt?: string;
|
||||
verifier?: string;
|
||||
refreshVersion?: number;
|
||||
seenIps: [string];
|
||||
}
|
||||
|
||||
const userSchema = new Schema<IUser>(
|
||||
@ -54,7 +55,8 @@ const userSchema = new Schema<IUser>(
|
||||
type: Number,
|
||||
default: 0,
|
||||
select: false
|
||||
}
|
||||
},
|
||||
seenIps: [String]
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
|
@ -10,7 +10,7 @@ import { ADMIN, MEMBER } from '../../variables';
|
||||
import { body, param } from 'express-validator';
|
||||
import { integrationController } from '../../controllers/v1';
|
||||
|
||||
router.post( // new: add new integration
|
||||
router.post( // new: add new integration for integration auth
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
@ -19,7 +19,13 @@ router.post( // new: add new integration
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
location: 'body'
|
||||
}),
|
||||
body('integrationAuthId').exists().trim(),
|
||||
body('integrationAuthId').exists().isString().trim(),
|
||||
body('app').isString().trim(),
|
||||
body('isActive').exists().isBoolean(),
|
||||
body('appId').trim(),
|
||||
body('sourceEnvironment').trim(),
|
||||
body('targetEnvironment').trim(),
|
||||
body('owner').trim(),
|
||||
validateRequest,
|
||||
integrationController.createIntegration
|
||||
);
|
||||
|
@ -18,6 +18,19 @@ router.get(
|
||||
integrationAuthController.getIntegrationOptions
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:integrationAuthId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireIntegrationAuthorizationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
param('integrationAuthId'),
|
||||
validateRequest,
|
||||
integrationAuthController.getIntegrationAuth
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/oauth-token',
|
||||
requireAuth({
|
||||
|
@ -6,6 +6,7 @@ import secrets from './secrets';
|
||||
import serviceTokenData from './serviceTokenData';
|
||||
import apiKeyData from './apiKeyData';
|
||||
import environment from "./environment"
|
||||
import tags from "./tags"
|
||||
|
||||
export {
|
||||
users,
|
||||
@ -15,5 +16,6 @@ export {
|
||||
secrets,
|
||||
serviceTokenData,
|
||||
apiKeyData,
|
||||
environment
|
||||
environment,
|
||||
tags
|
||||
}
|
@ -30,7 +30,7 @@ router.patch(
|
||||
'/:organizationId/memberships/:membershipId',
|
||||
param('organizationId').exists().trim(),
|
||||
param('membershipId').exists().trim(),
|
||||
body('role').exists().isString().trim().isIn([ADMIN, MEMBER]),
|
||||
body('role').exists().isString().trim().isIn([OWNER, ADMIN, MEMBER]),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
|
50
backend/src/routes/v2/tags.ts
Normal file
50
backend/src/routes/v2/tags.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import express, { Response, Request } from 'express';
|
||||
const router = express.Router();
|
||||
import { body, param } from 'express-validator';
|
||||
import { tagController } from '../../controllers/v2';
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest,
|
||||
} from '../../middleware';
|
||||
import { ADMIN, MEMBER } from '../../variables';
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/tags',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt'],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [MEMBER, ADMIN],
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
tagController.getWorkspaceTags
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/tags/:tagId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt'],
|
||||
}),
|
||||
param('tagId').exists().trim(),
|
||||
validateRequest,
|
||||
tagController.deleteWorkspaceTag
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:workspaceId/tags',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt'],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [MEMBER, ADMIN],
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
body('name').exists().trim(),
|
||||
body('slug').exists().trim(),
|
||||
validateRequest,
|
||||
tagController.createWorkspaceTag
|
||||
);
|
||||
|
||||
export default router;
|
@ -1,7 +1,3 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Integration
|
||||
} from '../models';
|
||||
import {
|
||||
handleOAuthExchangeHelper,
|
||||
syncIntegrationsHelper,
|
||||
@ -10,7 +6,6 @@ import {
|
||||
setIntegrationAuthRefreshHelper,
|
||||
setIntegrationAuthAccessHelper,
|
||||
} from '../helpers/integration';
|
||||
import { exchangeCode } from '../integrations';
|
||||
|
||||
// should sync stuff be here too? Probably.
|
||||
// TODO: move bot functions to IntegrationService.
|
||||
@ -26,11 +21,12 @@ class IntegrationService {
|
||||
* - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
|
||||
* - Add placeholder inactive integration
|
||||
* - Create bot sequence for integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.environment - workspace environment
|
||||
* @param {String} obj.integration - name of integration
|
||||
* @param {String} obj.code - code
|
||||
* @param {Object} obj1
|
||||
* @param {String} obj1.workspaceId - id of workspace
|
||||
* @param {String} obj1.environment - workspace environment
|
||||
* @param {String} obj1.integration - name of integration
|
||||
* @param {String} obj1.code - code
|
||||
* @returns {IntegrationAuth} integrationAuth - integration authorization after OAuth2 code-token exchange
|
||||
*/
|
||||
static async handleOAuthExchange({
|
||||
workspaceId,
|
||||
@ -43,7 +39,7 @@ class IntegrationService {
|
||||
code: string;
|
||||
environment: string;
|
||||
}) {
|
||||
await handleOAuthExchangeHelper({
|
||||
return await handleOAuthExchangeHelper({
|
||||
workspaceId,
|
||||
integration,
|
||||
code,
|
||||
|
@ -1,9 +1,13 @@
|
||||
const ACTION_LOGIN = 'login';
|
||||
const ACTION_LOGOUT = 'logout';
|
||||
const ACTION_ADD_SECRETS = 'addSecrets';
|
||||
const ACTION_DELETE_SECRETS = 'deleteSecrets';
|
||||
const ACTION_UPDATE_SECRETS = 'updateSecrets';
|
||||
const ACTION_READ_SECRETS = 'readSecrets';
|
||||
|
||||
export {
|
||||
ACTION_LOGIN,
|
||||
ACTION_LOGOUT,
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_DELETE_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
ENV_SET
|
||||
} from './environment';
|
||||
import {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
@ -14,6 +15,7 @@ import {
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_SET,
|
||||
INTEGRATION_OAUTH2,
|
||||
INTEGRATION_AZURE_TOKEN_URL,
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
@ -35,6 +37,8 @@ import {
|
||||
import { SECRET_SHARED, SECRET_PERSONAL } from './secret';
|
||||
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event';
|
||||
import {
|
||||
ACTION_LOGIN,
|
||||
ACTION_LOGOUT,
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_DELETE_SECRETS,
|
||||
@ -56,6 +60,7 @@ export {
|
||||
ENV_STAGING,
|
||||
ENV_PROD,
|
||||
ENV_SET,
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
@ -64,6 +69,7 @@ export {
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_SET,
|
||||
INTEGRATION_OAUTH2,
|
||||
INTEGRATION_AZURE_TOKEN_URL,
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
@ -75,6 +81,8 @@ export {
|
||||
INTEGRATION_FLYIO_API_URL,
|
||||
EVENT_PUSH_SECRETS,
|
||||
EVENT_PULL_SECRETS,
|
||||
ACTION_LOGIN,
|
||||
ACTION_LOGOUT,
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_DELETE_SECRETS,
|
||||
|
@ -1,3 +1,7 @@
|
||||
import {
|
||||
CLIENT_ID_AZURE,
|
||||
TENANT_ID_AZURE
|
||||
} from '../config';
|
||||
import {
|
||||
CLIENT_ID_HEROKU,
|
||||
CLIENT_ID_NETLIFY,
|
||||
@ -6,6 +10,7 @@ import {
|
||||
} from '../config';
|
||||
|
||||
// integrations
|
||||
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
|
||||
const INTEGRATION_HEROKU = 'heroku';
|
||||
const INTEGRATION_VERCEL = 'vercel';
|
||||
const INTEGRATION_NETLIFY = 'netlify';
|
||||
@ -13,6 +18,7 @@ const INTEGRATION_GITHUB = 'github';
|
||||
const INTEGRATION_RENDER = 'render';
|
||||
const INTEGRATION_FLYIO = 'flyio';
|
||||
const INTEGRATION_SET = new Set([
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
@ -25,6 +31,7 @@ const INTEGRATION_SET = new Set([
|
||||
const INTEGRATION_OAUTH2 = 'oauth2';
|
||||
|
||||
// integration oauth endpoints
|
||||
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID_AZURE}/oauth2/v2.0/token`;
|
||||
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
|
||||
const INTEGRATION_VERCEL_TOKEN_URL =
|
||||
'https://api.vercel.com/v2/oauth/access_token';
|
||||
@ -40,6 +47,16 @@ const INTEGRATION_RENDER_API_URL = 'https://api.render.com';
|
||||
const INTEGRATION_FLYIO_API_URL = 'https://api.fly.io/graphql';
|
||||
|
||||
const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: 'Azure Key Vault',
|
||||
slug: 'azure-key-vault',
|
||||
image: 'Microsoft Azure.png',
|
||||
isAvailable: true,
|
||||
type: 'oauth',
|
||||
clientId: CLIENT_ID_AZURE,
|
||||
tenantId: TENANT_ID_AZURE,
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Heroku',
|
||||
slug: 'heroku',
|
||||
@ -143,6 +160,7 @@ const INTEGRATION_OPTIONS = [
|
||||
]
|
||||
|
||||
export {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
@ -151,6 +169,7 @@ export {
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_SET,
|
||||
INTEGRATION_OAUTH2,
|
||||
INTEGRATION_AZURE_TOKEN_URL,
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
|
||||
Copyright (c) 2023 Infisical Inc.
|
||||
*/
|
||||
package main
|
||||
|
||||
|
@ -201,21 +201,30 @@ type GetEncryptedSecretsV2Request struct {
|
||||
|
||||
type GetEncryptedSecretsV2Response struct {
|
||||
Secrets []struct {
|
||||
ID string `json:"_id"`
|
||||
Version int `json:"version"`
|
||||
Workspace string `json:"workspace"`
|
||||
Type string `json:"type"`
|
||||
Environment string `json:"environment"`
|
||||
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
|
||||
SecretKeyIV string `json:"secretKeyIV"`
|
||||
SecretKeyTag string `json:"secretKeyTag"`
|
||||
SecretValueCiphertext string `json:"secretValueCiphertext"`
|
||||
SecretValueIV string `json:"secretValueIV"`
|
||||
SecretValueTag string `json:"secretValueTag"`
|
||||
V int `json:"__v"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
User string `json:"user,omitempty"`
|
||||
ID string `json:"_id"`
|
||||
Version int `json:"version"`
|
||||
Workspace string `json:"workspace"`
|
||||
Type string `json:"type"`
|
||||
Environment string `json:"environment"`
|
||||
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
|
||||
SecretKeyIV string `json:"secretKeyIV"`
|
||||
SecretKeyTag string `json:"secretKeyTag"`
|
||||
SecretValueCiphertext string `json:"secretValueCiphertext"`
|
||||
SecretValueIV string `json:"secretValueIV"`
|
||||
SecretValueTag string `json:"secretValueTag"`
|
||||
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
|
||||
SecretCommentIV string `json:"secretCommentIV"`
|
||||
SecretCommentTag string `json:"secretCommentTag"`
|
||||
V int `json:"__v"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
User string `json:"user,omitempty"`
|
||||
Tags []struct {
|
||||
ID string `json:"_id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Workspace string `json:"workspace"`
|
||||
} `json:"tags"`
|
||||
} `json:"secrets"`
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
|
||||
Copyright (c) 2023 Infisical Inc.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
@ -56,7 +56,12 @@ var exportCmd = &cobra.Command{
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables(envName)
|
||||
infisicalToken, err := cmd.Flags().GetString("token")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken})
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to fetch secrets")
|
||||
}
|
||||
@ -91,6 +96,7 @@ func init() {
|
||||
exportCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
||||
exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)")
|
||||
exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
||||
exportCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
||||
}
|
||||
|
||||
// Format according to the format flag
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
|
||||
Copyright (c) 2023 Infisical Inc.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
|
||||
Copyright (c) 2023 Infisical Inc.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
@ -101,6 +101,9 @@ var loginCmd = &cobra.Command{
|
||||
util.HandleError(err, "Unable to write write to Infisical Config file. Please try again")
|
||||
}
|
||||
|
||||
// clear backed up secrets from prev account
|
||||
util.DeleteBackupSecrets()
|
||||
|
||||
color.Green("Nice! You are logged in as: %v", email)
|
||||
|
||||
},
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
|
||||
Copyright (c) 2023 Infisical Inc.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
|
||||
Copyright (c) 2023 Infisical Inc.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
|
||||
Copyright (c) 2023 Infisical Inc.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
@ -12,6 +12,7 @@ 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"
|
||||
@ -58,6 +59,11 @@ var runCmd = &cobra.Command{
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
infisicalToken, err := cmd.Flags().GetString("token")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
// if !util.IsSecretEnvironmentValid(envName) {
|
||||
// util.PrintMessageAndExit("Invalid environment name passed. Environment names can only be prod, dev, test or staging")
|
||||
// }
|
||||
@ -72,7 +78,8 @@ var runCmd = &cobra.Command{
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables(envName)
|
||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken})
|
||||
|
||||
if err != nil {
|
||||
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
|
||||
}
|
||||
@ -140,6 +147,7 @@ var runCmd = &cobra.Command{
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(runCmd)
|
||||
runCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
||||
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
|
||||
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
||||
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
||||
|
@ -1,11 +1,13 @@
|
||||
/*
|
||||
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
|
||||
Copyright (c) 2023 Infisical Inc.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
@ -22,7 +24,7 @@ import (
|
||||
)
|
||||
|
||||
var secretsCmd = &cobra.Command{
|
||||
Example: `infisical secrets"`,
|
||||
Example: `infisical secrets`,
|
||||
Short: "Used to create, read update and delete secrets",
|
||||
Use: "secrets",
|
||||
DisableFlagsInUseLine: true,
|
||||
@ -34,12 +36,17 @@ var secretsCmd = &cobra.Command{
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
infisicalToken, err := cmd.Flags().GetString("token")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables(environmentName)
|
||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken})
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
@ -62,6 +69,16 @@ var secretsGetCmd = &cobra.Command{
|
||||
Run: getSecretsByNames,
|
||||
}
|
||||
|
||||
var secretsGenerateExampleEnvCmd = &cobra.Command{
|
||||
Example: `secrets generate-example-env > .example-env`,
|
||||
Short: "Used to generate a example .env file",
|
||||
Use: "generate-example-env",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.NoArgs,
|
||||
PreRun: toggleDebug,
|
||||
Run: generateExampleEnv,
|
||||
}
|
||||
|
||||
var secretsSetCmd = &cobra.Command{
|
||||
Example: `secrets set <secretName=secretValue> <secretName=secretValue>..."`,
|
||||
Short: "Used set secrets",
|
||||
@ -111,7 +128,7 @@ var secretsSetCmd = &cobra.Command{
|
||||
plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
|
||||
|
||||
// pull current secrets
|
||||
secrets, err := util.GetAllEnvironmentVariables(environmentName)
|
||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName})
|
||||
if err != nil {
|
||||
util.HandleError(err, "unable to retrieve secrets")
|
||||
}
|
||||
@ -267,7 +284,7 @@ var secretsDeleteCmd = &cobra.Command{
|
||||
util.HandleError(err, "Unable to get local project details")
|
||||
}
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables(environmentName)
|
||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName})
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to fetch secrets")
|
||||
}
|
||||
@ -309,30 +326,6 @@ var secretsDeleteCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
secretsCmd.AddCommand(secretsGetCmd)
|
||||
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)
|
||||
}
|
||||
|
||||
func getSecretsByNames(cmd *cobra.Command, args []string) {
|
||||
environmentName, err := cmd.Flags().GetString("env")
|
||||
if err != nil {
|
||||
@ -344,7 +337,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables(environmentName)
|
||||
infisicalToken, err := cmd.Flags().GetString("token")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken})
|
||||
if err != nil {
|
||||
util.HandleError(err, "To fetch all secrets")
|
||||
}
|
||||
@ -371,6 +369,171 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
|
||||
visualize.PrintAllSecretDetails(requestedSecrets)
|
||||
}
|
||||
|
||||
func generateExampleEnv(cmd *cobra.Command, args []string) {
|
||||
environmentName, err := cmd.Flags().GetString("env")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath()
|
||||
if !workspaceFileExists {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
infisicalToken, err := cmd.Flags().GetString("token")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken})
|
||||
if err != nil {
|
||||
util.HandleError(err, "To fetch all secrets")
|
||||
}
|
||||
|
||||
tagsHashToSecretKey := make(map[string]int)
|
||||
|
||||
type TagsAndSecrets struct {
|
||||
Secrets []models.SingleEnvironmentVariable
|
||||
Tags []struct {
|
||||
ID string `json:"_id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Workspace string `json:"workspace"`
|
||||
}
|
||||
}
|
||||
|
||||
// sort secrets by associated tags (most number of tags to least tags)
|
||||
sort.Slice(secrets, func(i, j int) bool {
|
||||
return len(secrets[i].Tags) > len(secrets[j].Tags)
|
||||
})
|
||||
|
||||
for _, secret := range secrets {
|
||||
listOfTagSlugs := []string{}
|
||||
|
||||
for _, tag := range secret.Tags {
|
||||
listOfTagSlugs = append(listOfTagSlugs, tag.Slug)
|
||||
}
|
||||
sort.Strings(listOfTagSlugs)
|
||||
|
||||
tagsHash := util.GetHashFromStringList(listOfTagSlugs)
|
||||
|
||||
tagsHashToSecretKey[tagsHash] += 1
|
||||
}
|
||||
|
||||
finalTagHashToSecretKey := make(map[string]TagsAndSecrets)
|
||||
|
||||
for _, secret := range secrets {
|
||||
listOfTagSlugs := []string{}
|
||||
for _, tag := range secret.Tags {
|
||||
listOfTagSlugs = append(listOfTagSlugs, tag.Slug)
|
||||
}
|
||||
|
||||
// sort the slug so we get the same hash each time
|
||||
sort.Strings(listOfTagSlugs)
|
||||
|
||||
tagsHash := util.GetHashFromStringList(listOfTagSlugs)
|
||||
occurrence, exists := tagsHashToSecretKey[tagsHash]
|
||||
if exists && occurrence > 0 {
|
||||
|
||||
value, exists2 := finalTagHashToSecretKey[tagsHash]
|
||||
allSecretsForTags := append(value.Secrets, secret)
|
||||
|
||||
// sort the the secrets by keys so that they can later be sorted by the first item in the secrets array
|
||||
sort.Slice(allSecretsForTags, func(i, j int) bool {
|
||||
return allSecretsForTags[i].Key < allSecretsForTags[j].Key
|
||||
})
|
||||
|
||||
if exists2 {
|
||||
finalTagHashToSecretKey[tagsHash] = TagsAndSecrets{
|
||||
Tags: secret.Tags,
|
||||
Secrets: allSecretsForTags,
|
||||
}
|
||||
} else {
|
||||
finalTagHashToSecretKey[tagsHash] = TagsAndSecrets{
|
||||
Tags: secret.Tags,
|
||||
Secrets: []models.SingleEnvironmentVariable{secret},
|
||||
}
|
||||
}
|
||||
|
||||
tagsHashToSecretKey[tagsHash] -= 1
|
||||
}
|
||||
}
|
||||
|
||||
// sort the fianl result by secret key fo consistent print order
|
||||
listOfsecretDetails := make([]TagsAndSecrets, 0, len(finalTagHashToSecretKey))
|
||||
for _, secretDetails := range finalTagHashToSecretKey {
|
||||
listOfsecretDetails = append(listOfsecretDetails, secretDetails)
|
||||
}
|
||||
|
||||
// sort the order of the headings by the order of the secrets
|
||||
sort.Slice(listOfsecretDetails, func(i, j int) bool {
|
||||
return len(listOfsecretDetails[i].Tags) < len(listOfsecretDetails[j].Tags)
|
||||
})
|
||||
|
||||
for _, secretDetails := range listOfsecretDetails {
|
||||
listOfKeyValue := []string{}
|
||||
|
||||
for _, secret := range secretDetails.Secrets {
|
||||
re := regexp.MustCompile(`(?s)(.*)DEFAULT:(.*)`)
|
||||
match := re.FindStringSubmatch(secret.Comment)
|
||||
defaultValue := ""
|
||||
comment := secret.Comment
|
||||
|
||||
// Case: Only has default value
|
||||
if len(match) == 2 {
|
||||
defaultValue = strings.TrimSpace(match[1])
|
||||
}
|
||||
|
||||
// Case: has a comment and a default value
|
||||
if len(match) == 3 {
|
||||
comment = match[1]
|
||||
defaultValue = match[2]
|
||||
}
|
||||
|
||||
row := ""
|
||||
if comment != "" {
|
||||
comment = addHash(comment)
|
||||
row = fmt.Sprintf("%s \n%s=%s", strings.TrimSpace(comment), strings.TrimSpace(secret.Key), strings.TrimSpace(defaultValue))
|
||||
} else {
|
||||
row = fmt.Sprintf("%s=%s", strings.TrimSpace(secret.Key), strings.TrimSpace(defaultValue))
|
||||
}
|
||||
|
||||
// each secret row to be added to the file
|
||||
listOfKeyValue = append(listOfKeyValue, row)
|
||||
}
|
||||
|
||||
listOfTagNames := []string{}
|
||||
for _, tag := range secretDetails.Tags {
|
||||
listOfTagNames = append(listOfTagNames, tag.Name)
|
||||
}
|
||||
|
||||
heading := CenterString(strings.Join(listOfTagNames, " & "), 80)
|
||||
|
||||
if len(listOfTagNames) == 0 {
|
||||
fmt.Printf("\n%s \n", strings.Join(listOfKeyValue, "\n \n"))
|
||||
} else {
|
||||
fmt.Printf("\n\n\n%s\n \n%s \n", heading, strings.Join(listOfKeyValue, "\n \n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CenterString(s string, numStars int) string {
|
||||
stars := strings.Repeat("*", numStars)
|
||||
padding := (numStars - len(s)) / 2
|
||||
cenetredTextWithStar := stars[:padding] + " " + strings.ToUpper(s) + " " + stars[padding:]
|
||||
|
||||
hashes := strings.Repeat("#", len(cenetredTextWithStar)+2)
|
||||
return fmt.Sprintf("%s \n# %s \n%s", hashes, cenetredTextWithStar, hashes)
|
||||
}
|
||||
|
||||
func addHash(input string) string {
|
||||
lines := strings.Split(input, "\n")
|
||||
for i, line := range lines {
|
||||
lines[i] = "# " + line
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]models.SingleEnvironmentVariable {
|
||||
secretMapByName := make(map[string]models.SingleEnvironmentVariable)
|
||||
|
||||
@ -380,3 +543,29 @@ func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]mod
|
||||
|
||||
return secretMapByName
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
secretsGenerateExampleEnvCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
||||
secretsCmd.AddCommand(secretsGenerateExampleEnvCmd)
|
||||
|
||||
secretsGetCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
||||
secretsCmd.AddCommand(secretsGetCmd)
|
||||
|
||||
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.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
||||
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)
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
|
||||
Copyright (c) 2023 Infisical Inc.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
|
@ -12,6 +12,11 @@ import (
|
||||
|
||||
// will decrypt cipher text to plain text using iv and tag
|
||||
func DecryptSymmetric(key []byte, cipherText []byte, tag []byte, iv []byte) ([]byte, error) {
|
||||
// Case: empty string
|
||||
if len(cipherText) == 0 && len(tag) == 0 && len(iv) == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -1,6 +1,8 @@
|
||||
package models
|
||||
|
||||
import "github.com/99designs/keyring"
|
||||
import (
|
||||
"github.com/99designs/keyring"
|
||||
)
|
||||
|
||||
type UserCredentials struct {
|
||||
Email string `json:"email"`
|
||||
@ -19,6 +21,13 @@ type SingleEnvironmentVariable struct {
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
ID string `json:"_id"`
|
||||
Tags []struct {
|
||||
ID string `json:"_id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Workspace string `json:"workspace"`
|
||||
} `json:"tags"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
type Workspace struct {
|
||||
@ -34,7 +43,12 @@ type WorkspaceConfigFile struct {
|
||||
}
|
||||
|
||||
type SymmetricEncryptionResult struct {
|
||||
CipherText []byte
|
||||
Nonce []byte
|
||||
AuthTag []byte
|
||||
CipherText []byte `json:"CipherText"`
|
||||
Nonce []byte `json:"Nonce"`
|
||||
AuthTag []byte `json:"AuthTag"`
|
||||
}
|
||||
|
||||
type GetAllSecretsParameters struct {
|
||||
Environment string
|
||||
InfisicalToken string
|
||||
}
|
||||
|
@ -2,10 +2,10 @@ package util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"errors"
|
||||
)
|
||||
|
||||
func CheckForUpdate() {
|
||||
@ -42,5 +42,5 @@ func getLatestTag(repoOwner string, repoName string) (string, error) {
|
||||
|
||||
json.Unmarshal(body, &tags)
|
||||
|
||||
return tags[0].Name, nil
|
||||
return tags[0].Name[1:], nil
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
@ -19,3 +20,11 @@ func WriteToFile(fileName string, dataToWrite []byte, filePerm os.FileMode) erro
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckIsConnectedToInternet() (ok bool) {
|
||||
_, err := http.Get("http://clients3.google.com/generate_204")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
@ -98,3 +99,14 @@ func RequireLocalWorkspaceFile() {
|
||||
PrintMessageAndExit("Your project id is missing in your local config file. Please add it or run again [infisical init]")
|
||||
}
|
||||
}
|
||||
|
||||
func GetHashFromStringList(list []string) string {
|
||||
hash := sha256.New()
|
||||
|
||||
for _, item := range list {
|
||||
hash.Write([]byte(item))
|
||||
}
|
||||
|
||||
sum := sha256.Sum256(hash.Sum(nil))
|
||||
return fmt.Sprintf("%x", sum)
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ func PrintErrorAndExit(exitCode int, err error, messages ...string) {
|
||||
}
|
||||
|
||||
func PrintWarning(message string) {
|
||||
color.Yellow("Warning: %v", message)
|
||||
color.New(color.FgYellow).Fprintf(os.Stderr, "Warning: %v \n", message)
|
||||
}
|
||||
|
||||
func PrintMessageAndExit(messages ...string) {
|
||||
|
@ -2,6 +2,8 @@ package util
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
@ -97,13 +99,26 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
|
||||
return plainTextSecrets, nil
|
||||
}
|
||||
|
||||
func GetAllEnvironmentVariables(envName string) ([]models.SingleEnvironmentVariable, error) {
|
||||
infisicalToken := os.Getenv(INFISICAL_TOKEN_NAME)
|
||||
func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models.SingleEnvironmentVariable, error) {
|
||||
var infisicalToken string
|
||||
if params.InfisicalToken == "" {
|
||||
infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
|
||||
} else {
|
||||
infisicalToken = params.InfisicalToken
|
||||
}
|
||||
|
||||
isConnected := CheckIsConnectedToInternet()
|
||||
var secretsToReturn []models.SingleEnvironmentVariable
|
||||
var errorToReturn error
|
||||
|
||||
if infisicalToken == "" {
|
||||
RequireLocalWorkspaceFile()
|
||||
RequireLogin()
|
||||
log.Debug("Trying to fetch secrets using logged in details")
|
||||
if isConnected {
|
||||
log.Debug("GetAllEnvironmentVariables: Connected to internet, checking logged in creds")
|
||||
RequireLocalWorkspaceFile()
|
||||
RequireLogin()
|
||||
}
|
||||
|
||||
log.Debug("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details")
|
||||
|
||||
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
|
||||
if err != nil {
|
||||
@ -115,13 +130,30 @@ func GetAllEnvironmentVariables(envName string) ([]models.SingleEnvironmentVaria
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secrets, err := GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, envName)
|
||||
return secrets, err
|
||||
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment)
|
||||
log.Debugf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
|
||||
|
||||
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
|
||||
if errorToReturn == nil {
|
||||
WriteBackupSecrets(workspaceFile.WorkspaceId, params.Environment, backupSecretsEncryptionKey, secretsToReturn)
|
||||
}
|
||||
|
||||
// only attempt to serve cached secrets if no internet connection and if at least one secret cached
|
||||
if !isConnected {
|
||||
backedSecrets, err := ReadBackupSecrets(workspaceFile.WorkspaceId, params.Environment, backupSecretsEncryptionKey)
|
||||
if len(backedSecrets) > 0 {
|
||||
PrintWarning("Unable to fetch latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug")
|
||||
secretsToReturn = backedSecrets
|
||||
errorToReturn = err
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
log.Debug("Trying to fetch secrets using service token")
|
||||
return GetPlainTextSecretsViaServiceToken(infisicalToken)
|
||||
secretsToReturn, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken)
|
||||
}
|
||||
|
||||
return secretsToReturn, errorToReturn
|
||||
}
|
||||
|
||||
func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variableWeAreLookingFor string, hashMapOfCompleteVariables map[string]string, hashMapOfSelfRefs map[string]string) string {
|
||||
@ -283,11 +315,34 @@ func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV2R
|
||||
return nil, fmt.Errorf("unable to symmetrically decrypt secret value")
|
||||
}
|
||||
|
||||
// Decrypt comment
|
||||
comment_iv, err := base64.StdEncoding.DecodeString(secret.SecretCommentIV)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode secret IV for secret value")
|
||||
}
|
||||
|
||||
comment_tag, err := base64.StdEncoding.DecodeString(secret.SecretCommentTag)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode secret authentication tag for secret value")
|
||||
}
|
||||
|
||||
comment_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretCommentCiphertext)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode secret cipher text for secret key")
|
||||
}
|
||||
|
||||
plainTextComment, err := crypto.DecryptSymmetric(key, comment_ciphertext, comment_tag, comment_iv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to symmetrically decrypt secret comment")
|
||||
}
|
||||
|
||||
plainTextSecret := models.SingleEnvironmentVariable{
|
||||
Key: string(plainTextKey),
|
||||
Value: string(plainTextValue),
|
||||
Type: string(secret.Type),
|
||||
ID: secret.ID,
|
||||
Key: string(plainTextKey),
|
||||
Value: string(plainTextValue),
|
||||
Type: string(secret.Type),
|
||||
ID: secret.ID,
|
||||
Tags: secret.Tags,
|
||||
Comment: string(plainTextComment),
|
||||
}
|
||||
|
||||
plainTextSecrets = append(plainTextSecrets, plainTextSecret)
|
||||
@ -295,3 +350,100 @@ func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV2R
|
||||
|
||||
return plainTextSecrets, nil
|
||||
}
|
||||
|
||||
func WriteBackupSecrets(workspace string, environment string, encryptionKey []byte, secrets []models.SingleEnvironmentVariable) error {
|
||||
fileName := fmt.Sprintf("secrets_%s_%s", workspace, environment)
|
||||
secrets_backup_folder_name := "secrets-backup"
|
||||
|
||||
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("WriteBackupSecrets: unable to get full config folder path [err=%s]", err)
|
||||
}
|
||||
|
||||
// create secrets backup directory
|
||||
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
|
||||
if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) {
|
||||
err := os.Mkdir(fullPathToSecretsBackupFolder, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var encryptedSecrets []models.SymmetricEncryptionResult
|
||||
for _, secret := range secrets {
|
||||
marshaledSecrets, _ := json.Marshal(secret)
|
||||
result, err := crypto.EncryptSymmetric(marshaledSecrets, encryptionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encryptedSecrets = append(encryptedSecrets, result)
|
||||
}
|
||||
|
||||
listOfSecretsMarshalled, _ := json.Marshal(encryptedSecrets)
|
||||
err = os.WriteFile(fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName), listOfSecretsMarshalled, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("WriteBackupSecrets: Unable to write backup secrets to file [err=%s]", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReadBackupSecrets(workspace string, environment string, encryptionKey []byte) ([]models.SingleEnvironmentVariable, error) {
|
||||
fileName := fmt.Sprintf("secrets_%s_%s", workspace, environment)
|
||||
secrets_backup_folder_name := "secrets-backup"
|
||||
|
||||
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ReadBackupSecrets: unable to write config file because an error occurred when getting config file path [err=%s]", err)
|
||||
}
|
||||
|
||||
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
|
||||
if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
encryptedBackupSecretsFilePath := fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName)
|
||||
|
||||
encryptedBackupSecretsAsBytes, err := os.ReadFile(encryptedBackupSecretsFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var listOfEncryptedBackupSecrets []models.SymmetricEncryptionResult
|
||||
|
||||
_ = json.Unmarshal(encryptedBackupSecretsAsBytes, &listOfEncryptedBackupSecrets)
|
||||
|
||||
var plainTextSecrets []models.SingleEnvironmentVariable
|
||||
for _, encryptedSecret := range listOfEncryptedBackupSecrets {
|
||||
result, err := crypto.DecryptSymmetric(encryptionKey, encryptedSecret.CipherText, encryptedSecret.AuthTag, encryptedSecret.Nonce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var plainTextSecret models.SingleEnvironmentVariable
|
||||
|
||||
err = json.Unmarshal(result, &plainTextSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plainTextSecrets = append(plainTextSecrets, plainTextSecret)
|
||||
}
|
||||
|
||||
return plainTextSecrets, nil
|
||||
|
||||
}
|
||||
|
||||
func DeleteBackupSecrets() error {
|
||||
secrets_backup_folder_name := "secrets-backup"
|
||||
|
||||
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ReadBackupSecrets: unable to write config file because an error occurred when getting config file path [err=%s]", err)
|
||||
}
|
||||
|
||||
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
|
||||
|
||||
return os.RemoveAll(fullPathToSecretsBackupFolder)
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ func fileKeyringPassphrasePrompt(prompt string) (string, error) {
|
||||
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.Println("You may set the environment variable `INFISICAL_VAULT_FILE_PASSPHRASE` with your password to avoid typing it")
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%s:", prompt)
|
||||
@ -65,6 +65,7 @@ func fileKeyringPassphrasePrompt(prompt string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fmt.Println("")
|
||||
return string(b), nil
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ infisical init
|
||||
|
||||
## Description
|
||||
|
||||
Link a local project to the platform
|
||||
Link a local project to your Infisical project. Once connected, you can then access the secrets locally from the connected Infisical project.
|
||||
|
||||
The command creates a `infisical.json` file containing your Project ID.
|
||||
<Info>
|
||||
This command creates a `infisical.json` file containing your Project ID.
|
||||
</Info>
|
||||
|
@ -25,13 +25,58 @@ description: "The command that injects your secrets into local environment"
|
||||
|
||||
## Description
|
||||
|
||||
Inject environment variables from the platform into an application process.
|
||||
Inject secrets from Infisical into your application process.
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description | Default value |
|
||||
| -------------- | ----------------------------------------------------------------------------------------------------------- | ------------- |
|
||||
| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` |
|
||||
| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` |
|
||||
| `--command` | Pass secrets into chained commands (e.g., `"first-command && second-command; more-commands..."`) | None |
|
||||
| `--secret-overriding`| Prioritizes personal secrets with the same name over shared secrets | `true` |
|
||||
## Subcommands & flags
|
||||
|
||||
<Accordion title="infisical run" defaultOpen="true">
|
||||
Use this command to inject secrets into your applications process
|
||||
|
||||
```bash
|
||||
$ infisical run -- <your application command>
|
||||
|
||||
# Example
|
||||
$ infisical run -- npm run dev
|
||||
```
|
||||
|
||||
### flags
|
||||
<Accordion title="--command">
|
||||
Pass secrets into multiple commands at once
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical run --command="npm run build && npm run dev; more-commands..."
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--token">
|
||||
If you are using a [service token](../../getting-started/dashboard/token) to authenticate, you can pass the token as a flag
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical run --token="st.63e03c4a97cb4a747186c71e.ed5b46a34c078a8f94e8228f4ab0ff97.4f7f38034811995997d72badf44b42ec" -- npm run start
|
||||
```
|
||||
|
||||
You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the run command. This will have the same effect as setting the token with `--token` flag
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--expand">
|
||||
Turn on or off the shell parameter expansion in your secrets. If you have used shell parameters in your secret(s), activating this feature will populate them before injecting them into your application process.
|
||||
|
||||
Default value: `true`
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--env">
|
||||
This is used to specify the environment from which secrets should be retrieved. The accepted values are the environment slugs defined for your project, such as `dev`, `staging`, `test`, and `prod`.
|
||||
|
||||
Default value: `dev`
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--secret-overriding">
|
||||
Prioritizes personal secrets with the same name over shared secrets
|
||||
|
||||
Default value: `true`
|
||||
</Accordion>
|
||||
|
||||
</Accordion>
|
||||
|
@ -14,17 +14,8 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
|
||||
<Accordion title="infisical secrets" defaultOpen="true">
|
||||
Use this command to print out all of the secrets in your project
|
||||
|
||||
```
|
||||
```bash
|
||||
$ infisical secrets
|
||||
|
||||
## Example
|
||||
$ infisical secrets
|
||||
┌─────────────┬──────────────┬─────────────┐
|
||||
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
|
||||
├─────────────┼──────────────┼─────────────┤
|
||||
│ DOMAIN │ example.com │ shared │
|
||||
│ HASH │ jebhfbwe │ shared │
|
||||
└─────────────┴──────────────┴─────────────┘
|
||||
```
|
||||
|
||||
### flags
|
||||
@ -45,16 +36,11 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
|
||||
<Accordion title="infisical secrets get">
|
||||
This command allows you selectively print the requested secrets by name
|
||||
|
||||
```
|
||||
```bash
|
||||
$ infisical secrets get <secret-name-a> <secret-name-b> ...
|
||||
|
||||
# Example
|
||||
$ infisical secrets get DOMAIN
|
||||
┌─────────────┬──────────────┬─────────────┐
|
||||
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
|
||||
├─────────────┼──────────────┼─────────────┤
|
||||
│ DOMAIN │ example.com │ shared │
|
||||
└─────────────┴──────────────┴─────────────┘
|
||||
|
||||
```
|
||||
|
||||
@ -70,18 +56,11 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
|
||||
This command allows you to set or update secrets in your environment. If the secret key provided already exists, its value will be updated with the new value.
|
||||
If the secret key does not exist, a new secret will be created using both the key and value provided.
|
||||
|
||||
```
|
||||
```bash
|
||||
$ infisical secrets set <key1=value1> <key2=value2>...
|
||||
|
||||
## Example
|
||||
$ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jebhfbwe
|
||||
┌────────────────┬───────────────┬────────────────────────┐
|
||||
│ SECRET NAME │ SECRET VALUE │ STATUS │
|
||||
├────────────────┼───────────────┼────────────────────────┤
|
||||
│ STRIPE_API_KEY │ sjdgwkeudyjwe │ SECRET VALUE UNCHANGED │
|
||||
│ DOMAIN │ example.com │ SECRET VALUE MODIFIED │
|
||||
│ HASH │ jebhfbwe │ SECRET CREATED │
|
||||
└────────────────┴───────────────┴────────────────────────┘
|
||||
```
|
||||
|
||||
### Flags
|
||||
@ -95,12 +74,11 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
|
||||
<Accordion title="infisical secrets delete">
|
||||
This command allows you to delete secrets by their name(s).
|
||||
|
||||
```
|
||||
```bash
|
||||
$ infisical secrets delete <keyName1> <keyName2>...
|
||||
|
||||
## Example
|
||||
$ infisical secrets delete STRIPE_API_KEY DOMAIN HASH
|
||||
secret name(s) [STRIPE_API_KEY, DOMAIN, HASH] have been deleted from your project
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
@ -13,4 +13,9 @@ If none of the available stores work for you, you can try using the `file` store
|
||||
If you are still experiencing trouble, please seek support.
|
||||
|
||||
[Learn more about vault command](./commands/vault)
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Can I fetch secrets with Infisical if I am offline?">
|
||||
Yes. If you have previously retrieved secrets for a specific project and environment (such as dev, staging, or prod), the `run`/`secret` command will utilize the saved secrets, even when offline, on subsequent fetch attempts.
|
||||
|
||||
</Accordion>
|
BIN
docs/images/k8-diagram.png
Normal file
BIN
docs/images/k8-diagram.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 131 KiB |
34
docs/integrations/cicd/gitlab.mdx
Normal file
34
docs/integrations/cicd/gitlab.mdx
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
title: "Gitlab Pipeline"
|
||||
---
|
||||
|
||||
To integrate Infisical secrets into your Gitlab CI/CD setup, three steps are required.
|
||||
|
||||
## Generate service token
|
||||
To expose Infisical secrets in Gitlab CI/CD, you must generate a service token for the specific project and environment in Infisical. For instructions on how to generate a service token, refer to [this page](../../getting-started/dashboard/token)
|
||||
|
||||
## Set Infisical service token in Gitlab
|
||||
To provide Infisical CLI with the service token generated in the previous step, go to **Settings > CI/CD > Variables** in Gitlab and create a new **INFISICAL_TOKEN** variable. Enter the generated service token as its value.
|
||||
|
||||
## Configure Infisical in your pipeline
|
||||
Edit your .gitlab-ci.yml to include the installation of the Infisical CLI. This will allow you to use the CLI for fetching and injecting secrets into any script or command within your Gitlab CI/CD process.
|
||||
|
||||
#### Example
|
||||
```yaml
|
||||
image: ubuntu
|
||||
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
- deploy
|
||||
|
||||
build-job:
|
||||
stage: build
|
||||
script:
|
||||
- apt update && apt install -y curl
|
||||
- curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash
|
||||
- apt-get update && apt-get install -y infisical
|
||||
- infisical run -- npm run build
|
||||
|
||||
...
|
||||
```
|
@ -12,6 +12,7 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
|
||||
| [Docker](/integrations/platforms/docker) | Platform | Available |
|
||||
| [Docker-Compose](/integrations/platforms/docker-compose) | Platform | Available |
|
||||
| [Kubernetes](/integrations/platforms/kubernetes) | Platform | Available |
|
||||
| [PM2](/integrations/platforms/pm2) | Platform | Available |
|
||||
| [Heroku](/integrations/cloud/heroku) | Cloud | Available |
|
||||
| [Vercel](/integrations/cloud/vercel) | Cloud | Available |
|
||||
| [Netlify](/integrations/cloud/netlify) | Cloud | Available |
|
||||
@ -36,7 +37,7 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
|
||||
| GCP | Cloud | Coming soon |
|
||||
| Azure | Cloud | Coming soon |
|
||||
| DigitalOcean | Cloud | Coming soon |
|
||||
| GitLab | CI/CD | Coming soon |
|
||||
| [GitLab Pipeline](/integrations/cicd/gitlab) | CI/CD | Available |
|
||||
| [CircleCI](/integrations/cicd/circleci) | CI/CD | Coming soon |
|
||||
| TravisCI | CI/CD | Coming soon |
|
||||
| GitHub Actions | CI/CD | Coming soon |
|
||||
|
@ -1,45 +1,38 @@
|
||||
---
|
||||
title: "Docker Compose"
|
||||
description: "How to use Infisical to inject environment variables into container defined in your Docker Compose file."
|
||||
description: "How to use Infisical to inject environment variables into services defined in your Docker Compose file."
|
||||
---
|
||||
|
||||
The Docker Compose integration enables you to inject environment variables from Infisical into the containers defined in your compose file.
|
||||
Prerequisites:
|
||||
|
||||
## Add the CLI to your Dockerfile(s) start command
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||
|
||||
Follow the [guide to configure Infisical CLI](./docker) in your your Dockerfile first.
|
||||
## Configure the Infisical CLI for each service
|
||||
|
||||
## Generate Infisical Token
|
||||
Follow this [guide](./docker) to configure the Infisical CLI for each service that you wish to inject environment variables into; you'll have to update the Dockerfile of each service.
|
||||
|
||||
In order for Infisical CLI to authenticate and retrieve your project's secrets without exposing your login credentials, you must generate a Infisical Token.
|
||||
To learn how, visit [Infisical Token](../../getting-started/dashboard/token). Once you have generated the token, keep it handy.
|
||||
## Generate Infisical Tokens
|
||||
|
||||
<Info>
|
||||
If you have multiple services and they do not use the same secrets, you will
|
||||
have to generate a Infisical Token for each service.
|
||||
</Info>
|
||||
Generate a unique [Infisical Token](https://infisical.com/docs/getting-started/dashboard/token) for each service.
|
||||
|
||||
## Tell Docker Compose your Infisical Token
|
||||
## Add Infisical Tokens to your Docker Compose file
|
||||
|
||||
For each service you want to inject secrets into, set an environment variable called `INFISICAL_TOKEN` equal to a helpful identifier variable.
|
||||
This will ensure that you can set Infisical Tokens for multiple services.
|
||||
For each service you want to inject secrets into, set an environment variable called `INFISICAL_TOKEN` equal to a unique identifier variable.
|
||||
|
||||
For the example below, we have set `INFISICAL_TOKEN_FOR_WEB` and `INFISICAL_TOKEN_FOR_API` as the `INFISICAL_TOKEN` for the corresponding service.
|
||||
In the example below, we set `INFISICAL_TOKEN_FOR_WEB` and `INFISICAL_TOKEN_FOR_API` as the `INFISICAL_TOKEN` for the services.
|
||||
|
||||
```yaml
|
||||
# Example Docker Compose file
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
image: auledge-frontend
|
||||
container_name: auledge-frontend
|
||||
image: example-service-1
|
||||
environment:
|
||||
- INFISICAL_TOKEN=${INFISICAL_TOKEN_FOR_WEB}
|
||||
|
||||
api:
|
||||
build: .
|
||||
image: auledge-backend
|
||||
container_name: auledge-backend
|
||||
image: example-service-2
|
||||
environment:
|
||||
- INFISICAL_TOKEN=${INFISICAL_TOKEN_FOR_API}
|
||||
```
|
||||
|
@ -3,9 +3,11 @@ title: "Docker"
|
||||
description: "How to use Infisical to inject environment variables into a Docker container."
|
||||
---
|
||||
|
||||
Infisical can be used in a Dockerfile to inject environment variables into a Docker container.
|
||||
Prerequisites:
|
||||
|
||||
## Add the CLI to your Dockerfile
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||
|
||||
## Add the Infisical CLI to your Dockerfile
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Alpine">
|
||||
@ -32,32 +34,32 @@ Infisical can be used in a Dockerfile to inject environment variables into a Doc
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Modify your Dockerfile start command
|
||||
## Modify the start command in your Dockerfile
|
||||
|
||||
To make your Docker container consume Infisical secrets, you can start your application with Infisical.
|
||||
This will automatically pull the necessary secrets and make them available to your application as if they were natively exposed within the container.
|
||||
Starting your service with the Infisical CLI pulls your secrets from Infisical and injects them into your service.
|
||||
|
||||
```dockerfile
|
||||
CMD ["infisical", "run", "---", "[your application start command]"]
|
||||
```dockerfile
|
||||
CMD ["infisical", "run", "---", "[your service start command]"]
|
||||
|
||||
# example with single single command
|
||||
CMD ["infisical", "run", "---", "npm run start"]
|
||||
# example with single single command
|
||||
CMD ["infisical", "run", "---", "npm run start"]
|
||||
|
||||
# example with multiple commands
|
||||
CMD ["infisical", "run", "--command", "npm run start && ..."]
|
||||
```
|
||||
# example with multiple commands
|
||||
CMD ["infisical", "run", "--command", "npm run start && ..."]
|
||||
```
|
||||
|
||||
View more options for the `run` command [here](../../cli/commands/run)
|
||||
## Generate an Infisical Token
|
||||
|
||||
Head to your project settings in Infisical Cloud to generate an [Infisical Token](https://infisical.com/docs/getting-started/dashboard/token).
|
||||
|
||||
## Feed Docker your Infisical Token
|
||||
|
||||
The CLI looks out for an environment variable called `INFISICAL_TOKEN`. If the token is detected, the CLI will authenticate, retrieve, and inject the environment variables which the token is authorized for.
|
||||
|
||||
```bash
|
||||
docker run --env INFISICAL_TOKEN=[token]...
|
||||
```
|
||||
|
||||
## Generate an Infisical Token
|
||||
<Info>
|
||||
|
||||
[Generate an Infisical Token](../../getting-started/dashboard/token) and keep it handy.
|
||||
The Infisical CLI uses the detected `INFISICAL_TOKEN` environment variable to authenticate, retrieve, and inject the environment variables which the token is authorized for.
|
||||
|
||||
</Info>
|
||||
|
@ -3,10 +3,12 @@ title: 'Kubernetes'
|
||||
description: "This page explains how to use Infisical to inject secrets into Kubernetes clusters."
|
||||
---
|
||||
|
||||
The Infisical Secrets Operator is a custom Kubernetes controller that helps keep secrets in a cluster up to date by synchronizing them.
|
||||
It is installed in its own namespace within the cluster and follows strict RBAC policies.
|
||||
The operator uses InfisicalSecret custom resources to identify which secrets to sync and where to store them.
|
||||
It is responsible for continuously updating managed secrets, and in the future may also automatically reload deployments that use them as needed.
|
||||

|
||||
|
||||
|
||||
The Infisical Secrets Operator is a Kubernetes controller that retrieves secrets from Infisical and stores them in a designated cluster.
|
||||
It uses an `InfisicalSecret` resource to specify authentication and storage methods.
|
||||
The operator continuously updates secrets and can also reload dependent deployments automatically.
|
||||
|
||||
## Install Operator
|
||||
|
||||
|
52
docs/integrations/platforms/pm2.mdx
Normal file
52
docs/integrations/platforms/pm2.mdx
Normal file
@ -0,0 +1,52 @@
|
||||
---
|
||||
title: "PM2"
|
||||
description: "How to use Infisical to inject environment variables and secrets with PM2 into a Node.js app"
|
||||
---
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||
- [Install the CLI](/cli/overview)
|
||||
|
||||
## Initialize Infisical for your Node.js app
|
||||
|
||||
```bash
|
||||
# navigate to the root of your of your project
|
||||
cd /path/to/project
|
||||
|
||||
# then initialize infisical
|
||||
infisical init
|
||||
```
|
||||
|
||||
## Create a bash or js script
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash infisical-run.sh
|
||||
infisical run -- npm start
|
||||
```
|
||||
|
||||
```js infisical-run.js
|
||||
const spawn = require("child_process").spawn;
|
||||
|
||||
const infisical = spawn("infisical", ["run", "--", "npm", "start"]);
|
||||
|
||||
infisical.stdout.on("data", (data) => console.log(`${data}`));
|
||||
infisical.stderr.on("data", (data) => console.error(`${data}`));
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Start your application as usual but with the script
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash infisical-run.sh
|
||||
pm2 start infisical-run.sh
|
||||
```
|
||||
|
||||
```bash infisical-run.js
|
||||
pm2 start infisical-run.js
|
||||
```
|
||||
|
||||
</CodeGroup>
|
@ -209,7 +209,8 @@
|
||||
"pages": [
|
||||
"integrations/platforms/docker",
|
||||
"integrations/platforms/docker-compose",
|
||||
"integrations/platforms/kubernetes"
|
||||
"integrations/platforms/kubernetes",
|
||||
"integrations/platforms/pm2"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -226,6 +227,7 @@
|
||||
"group": "CI/CD",
|
||||
"pages": [
|
||||
"integrations/cicd/githubactions",
|
||||
"integrations/cicd/gitlab",
|
||||
"integrations/cicd/circleci"
|
||||
]
|
||||
},
|
||||
|
622
frontend/package-lock.json
generated
622
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -21,11 +21,14 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
||||
"@fortawesome/react-fontawesome": "^0.1.19",
|
||||
"@headlessui/react": "^1.6.6",
|
||||
"@hookform/resolvers": "^2.9.10",
|
||||
"@octokit/rest": "^19.0.7",
|
||||
"@radix-ui/react-accordion": "^1.1.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.2",
|
||||
"@radix-ui/react-checkbox": "^1.0.1",
|
||||
"@radix-ui/react-dialog": "^1.0.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.2",
|
||||
"@radix-ui/react-hover-card": "^1.0.3",
|
||||
"@radix-ui/react-label": "^2.0.0",
|
||||
"@radix-ui/react-popover": "^1.0.3",
|
||||
"@radix-ui/react-progress": "^1.0.1",
|
||||
@ -58,6 +61,7 @@
|
||||
"react-code-input": "^3.10.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-hook-form": "^7.43.0",
|
||||
"react-i18next": "^12.1.1",
|
||||
"react-mailchimp-subscribe": "^2.1.3",
|
||||
"react-markdown": "^8.0.3",
|
||||
@ -71,7 +75,8 @@
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"uuid": "^8.3.2",
|
||||
"uuidv4": "^6.2.13",
|
||||
"yaml": "^2.2.0"
|
||||
"yaml": "^2.2.0",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "^7.0.0-beta.30",
|
||||
|
@ -2,6 +2,16 @@ interface Mapping {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
const integrationSlugNameMapping: Mapping = {
|
||||
'azure-key-vault': 'Azure Key Vault',
|
||||
'heroku': 'Heroku',
|
||||
'vercel': 'Vercel',
|
||||
'netlify': 'Netlify',
|
||||
'github': 'GitHub',
|
||||
'render': 'Render',
|
||||
'flyio': 'Fly.io'
|
||||
}
|
||||
|
||||
const envMapping: Mapping = {
|
||||
Development: "dev",
|
||||
Staging: "staging",
|
||||
@ -19,7 +29,7 @@ const reverseEnvMapping: Mapping = {
|
||||
const contextNetlifyMapping: Mapping = {
|
||||
"dev": "Local development",
|
||||
"branch-deploy": "Branch deploys",
|
||||
"deploy-review": "Deploy Previews",
|
||||
"deploy-preview": "Deploy Previews",
|
||||
"production": "Production"
|
||||
}
|
||||
|
||||
@ -49,6 +59,7 @@ const plans = plansProd || plansDev;
|
||||
export {
|
||||
contextNetlifyMapping,
|
||||
envMapping,
|
||||
integrationSlugNameMapping,
|
||||
plans,
|
||||
reverseContextNetlifyMapping,
|
||||
reverseEnvMapping}
|
||||
|
@ -1,3 +1,12 @@
|
||||
export interface Tag {
|
||||
_id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
user: string;
|
||||
workspace: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface SecretDataProps {
|
||||
pos: number;
|
||||
key: string;
|
||||
@ -5,4 +14,5 @@ export interface SecretDataProps {
|
||||
valueOverride: string | undefined;
|
||||
id: string;
|
||||
comment: string;
|
||||
tags: Tag[];
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"head-title": "{{title}} | Infiscal",
|
||||
"head-title": "{{title}} | Infisical",
|
||||
"error_project-already-exists": "A project with this name already exists.",
|
||||
"no-mobile": " To use Infisical, please log in through a device with larger dimensions. ",
|
||||
"email": "Email",
|
||||
@ -14,7 +14,7 @@
|
||||
"save-changes": "Save Changes",
|
||||
"saved": "Saved",
|
||||
"drop-zone": "Drag and drop a .env or .yml file here.",
|
||||
"drop-zone-keys": "Drag and drop a .env or .yml file here to add more keys.",
|
||||
"drop-zone-keys": "Drag and drop a .env or .yml file here to add more secrets.",
|
||||
"role": "Role",
|
||||
"role_admin": "admin",
|
||||
"display-name": "Display Name",
|
||||
@ -28,7 +28,7 @@
|
||||
"select-event": "Select an event",
|
||||
"event": "Event",
|
||||
"user": "User",
|
||||
"source": "Source",
|
||||
"time": "Time",
|
||||
"source": "Source",
|
||||
"time": "Time",
|
||||
"timestamp": "Timestamp"
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
"add-new": "Add New Token",
|
||||
"add-dialog": {
|
||||
"title": "Add a service token for {{target}}",
|
||||
"description": "Specify the name, environment, and expiry period. When a token is generated, you will only be able to see it once before it disappears. Make sure to save it somewhere.",
|
||||
"description": "When a token is generated, you will only be able to see it once before it disappears. Make sure to save it somewhere.",
|
||||
"name": "Service Token Name",
|
||||
"add": "Add Service Token",
|
||||
"copy-service-token": "Copy your service token",
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"head-title": "{{title}} | Infiscal",
|
||||
"head-title": "{{title}} | Infisical",
|
||||
"error_project-already-exists": "Un projet avec ce nom existe déjà.",
|
||||
"no-mobile": " Pour utiliser Infisical, veuillez vous connecter avec un appareil avec des dimensions plus grandes. ",
|
||||
"email": "Email",
|
||||
@ -28,7 +28,7 @@
|
||||
"select-event": "Sélectionnez un événement",
|
||||
"event": "Événement",
|
||||
"user": "Utilisateur",
|
||||
"source": "Source",
|
||||
"time": "Heure",
|
||||
"source": "Source",
|
||||
"time": "Heure",
|
||||
"timestamp": "Horodatage"
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"head-title": "{{title}} | Infiscal",
|
||||
"head-title": "{{title}} | Infisical",
|
||||
"error_project-already-exists": "동일한 이름을 가진 프로젝트가 이미 존재해요.",
|
||||
"no-mobile": " Infisical을 사용하려면, 큰 화면을 가진 디바이스로 로그인하여 주세요.",
|
||||
"email": "메일",
|
||||
@ -23,4 +23,4 @@
|
||||
"language": "언어",
|
||||
"search": "검색하기...",
|
||||
"note": "Note"
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"head-title": "{{title}} | Infiscal",
|
||||
"head-title": "{{title}} | Infisical",
|
||||
"error_project-already-exists": "Já exite um projeto com este nome.",
|
||||
"no-mobile": "Para usar o Infisical, faça o login através de um dispositivo com dimensões maiores.",
|
||||
"email": "Email",
|
||||
|
@ -187,10 +187,10 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
});
|
||||
const userWorkspaces = orgUserProjects;
|
||||
if (
|
||||
userWorkspaces.length === 0 &&
|
||||
router.asPath !== '/noprojects' &&
|
||||
!router.asPath.includes('home') &&
|
||||
!router.asPath.includes('settings') ||
|
||||
(userWorkspaces.length === 0 &&
|
||||
router.asPath !== '/noprojects' &&
|
||||
!router.asPath.includes('home') &&
|
||||
!router.asPath.includes('settings')) ||
|
||||
router.asPath === '/dashboard/undefined'
|
||||
) {
|
||||
router.push('/noprojects');
|
||||
@ -199,13 +199,13 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
.split('/')
|
||||
[router.asPath.split('/').length - 1].split('?')[0];
|
||||
|
||||
if (!['heroku', 'vercel', 'github', 'netlify'].includes(intendedWorkspaceId)) {
|
||||
if (!['callback', 'create', 'authorize'].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 (
|
||||
!['heroku', 'vercel', 'github', 'netlify'].includes(intendedWorkspaceId) &&
|
||||
!['callback', 'create', 'authorize'].includes(intendedWorkspaceId) &&
|
||||
!userWorkspaces
|
||||
.map((workspace: { _id: string }) => workspace._id)
|
||||
.includes(intendedWorkspaceId)
|
||||
@ -246,16 +246,15 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed w-full md:block flex flex-col h-screen">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/3.2.2/cdn.js" defer />
|
||||
<div className="flex h-screen w-full flex-col overflow-x-hidden">
|
||||
<NavBarDashboard />
|
||||
<div className="flex flex-col md:flex-row flex-1">
|
||||
<aside className="bg-bunker-600 border-r border-mineshaft-500 w-full md:w-60 h-screen">
|
||||
<nav className="flex flex-col justify-between items-between h-full">
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<aside className="w-full border-r border-mineshaft-500 bg-bunker-600 md:w-60">
|
||||
<nav className="items-between flex h-full flex-col justify-between">
|
||||
{/* <div className="py-6"></div> */}
|
||||
<div>
|
||||
<div className="flex justify-center w-full mt-[4.5rem] mb-6 bg-bunker-600 h-20 flex-col items-center px-4">
|
||||
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
||||
<div className="mt-6 mb-6 flex h-20 w-full flex-col items-center justify-center bg-bunker-600 px-4">
|
||||
<div className="ml-1 mb-1 self-start text-xs font-semibold tracking-wide text-gray-400">
|
||||
{t('nav:menu.project')}
|
||||
</div>
|
||||
{Object.keys(workspaceMapping).length > 0 ? (
|
||||
@ -279,29 +278,29 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
<ul>
|
||||
{Object.keys(workspaceMapping).length > 0 &&
|
||||
menuItems.map(({ href, title, emoji }) => (
|
||||
<li className="mt-0.5 mx-2" key={title}>
|
||||
<li className="mx-2 mt-0.5" key={title}>
|
||||
{router.asPath.split('/')[1] === href.split('/')[1] &&
|
||||
(['project', 'billing', 'org', 'personal'].includes(
|
||||
router.asPath.split('/')[2]
|
||||
)
|
||||
? router.asPath.split('/')[2] === href.split('/')[2]
|
||||
: true) ? (
|
||||
<div className="flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10">
|
||||
<div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1" />
|
||||
<p className="w-6 ml-4 mr-2 flex items-center justify-center text-lg">
|
||||
<div className="relative flex cursor-pointer rounded bg-primary-50/10 px-0.5 py-2.5 text-sm text-white">
|
||||
<div className="absolute inset-0 top-0 my-1 ml-1 mr-1 w-1 rounded-xl bg-primary" />
|
||||
<p className="ml-4 mr-2 flex w-6 items-center justify-center text-lg">
|
||||
{emoji}
|
||||
</p>
|
||||
{title}
|
||||
</div>
|
||||
) : router.asPath === '/noprojects' ? (
|
||||
<div className="flex p-2.5 text-white text-sm rounded">
|
||||
<p className="w-10 flex items-center justify-center text-lg">{emoji}</p>
|
||||
<div className="flex rounded p-2.5 text-sm text-white">
|
||||
<p className="flex w-10 items-center justify-center text-lg">{emoji}</p>
|
||||
{title}
|
||||
</div>
|
||||
) : (
|
||||
<Link href={href}>
|
||||
<div className="flex p-2.5 text-white text-sm rounded cursor-pointer hover:bg-primary-50/5">
|
||||
<p className="w-10 flex items-center justify-center text-lg">
|
||||
<div className="flex cursor-pointer rounded p-2.5 text-sm text-white hover:bg-primary-50/5">
|
||||
<p className="flex w-10 items-center justify-center text-lg">
|
||||
{emoji}
|
||||
</p>
|
||||
{title}
|
||||
@ -312,11 +311,11 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="w-full mt-40 mb-4 px-2">
|
||||
<div className="mt-40 mb-4 w-full px-2">
|
||||
{router.asPath.split('/')[1] === 'home' ? (
|
||||
<div className="flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10">
|
||||
<div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1" />
|
||||
<p className="w-6 ml-4 mr-2 flex items-center justify-center text-lg">
|
||||
<div className="relative flex cursor-pointer rounded bg-primary-50/10 px-0.5 py-2.5 text-sm text-white">
|
||||
<div className="absolute inset-0 top-0 my-1 ml-1 mr-1 w-1 rounded-xl bg-primary" />
|
||||
<p className="ml-4 mr-2 flex w-6 items-center justify-center text-lg">
|
||||
<FontAwesomeIcon icon={faBookOpen} />
|
||||
</p>
|
||||
Infisical Guide
|
||||
@ -336,8 +335,8 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
</div>
|
||||
) : (
|
||||
<Link href={`/home/${workspaceMapping[workspaceSelected as any]}`}>
|
||||
<div className="relative flex p-2.5 overflow-visible text-white h-10 text-sm rounded cursor-pointer bg-white/10 hover:bg-primary-50/[0.15] mt-max">
|
||||
<p className="w-10 flex items-center justify-center text-lg">
|
||||
<div className="mt-max relative flex h-10 cursor-pointer overflow-visible rounded bg-white/10 p-2.5 text-sm text-white hover:bg-primary-50/[0.15]">
|
||||
<p className="flex w-10 items-center justify-center text-lg">
|
||||
<FontAwesomeIcon icon={faBookOpen} />
|
||||
</p>
|
||||
Infisical Guide
|
||||
@ -369,12 +368,12 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
<main className="flex-1 bg-bunker-800">{children}</main>
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:hidden bg-bunker-800 w-screen h-screen flex flex-col justify-center items-center">
|
||||
<FontAwesomeIcon icon={faMobile} className="text-gray-300 text-7xl mb-8" />
|
||||
<p className="text-gray-200 px-6 text-center text-lg max-w-sm">
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t('common:no-mobile')} `}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -58,7 +58,7 @@ const ListBox = ({
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="border border-mineshaft-700 z-50 p-2 absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||
<Listbox.Options className="border border-mineshaft-700 z-[70] p-2 absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm no-scrollbar no-scrollbar::-webkit-scrollbar">
|
||||
{data.map((person, personIdx) => (
|
||||
<Listbox.Option
|
||||
key={`${person}.${personIdx + 1}`}
|
||||
|
@ -36,7 +36,7 @@ const AddWorkspaceDialog = ({
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={closeModal}>
|
||||
<Dialog as="div" className="relative z-[100]" onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
@ -49,7 +49,7 @@ const AddWorkspaceDialog = ({
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto z-50">
|
||||
<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}
|
||||
@ -60,7 +60,7 @@ const AddWorkspaceDialog = ({
|
||||
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.Panel className="w-full max-w-md transform overflow-hidden 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"
|
||||
@ -69,7 +69,7 @@ const AddWorkspaceDialog = ({
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
This project will contain your environmental variables.
|
||||
This project will contain your secrets and configs.
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-h-28 mt-4">
|
||||
|
@ -15,7 +15,7 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={() => {}}>
|
||||
<Dialog as="div" className="relative z-[80]" onClose={() => {}}>
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
|
@ -1,120 +0,0 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
import Button from "../buttons/Button";
|
||||
import InputField from "../InputField";
|
||||
|
||||
interface IntegrationOption {
|
||||
clientId: string;
|
||||
clientSlug?: string; // vercel-integration specific
|
||||
docsLink: string;
|
||||
image: string;
|
||||
isAvailable: boolean;
|
||||
name: string;
|
||||
slug: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
selectedIntegrationOption: IntegrationOption | null
|
||||
handleIntegrationOption: (arg:{
|
||||
integrationOption: IntegrationOption,
|
||||
accessToken?: string;
|
||||
})=>void;
|
||||
};
|
||||
|
||||
const IntegrationAccessTokenDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
selectedIntegrationOption,
|
||||
handleIntegrationOption
|
||||
}:Props) => {
|
||||
const [accessToken, setAccessToken] = useState('');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const submit = async () => {
|
||||
try {
|
||||
if (selectedIntegrationOption && accessToken !== '') {
|
||||
handleIntegrationOption({
|
||||
integrationOption: selectedIntegrationOption,
|
||||
accessToken
|
||||
});
|
||||
closeModal();
|
||||
setAccessToken('');
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" 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"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||
</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-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"
|
||||
>
|
||||
{`Enter your ${selectedIntegrationOption?.name} API Key`}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 mb-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
{`This integration requires you to obtain an API key from ${selectedIntegrationOption?.name ?? ''} and store it with Infisical. `}
|
||||
You can learn how to do this <a target="_blank" rel="noreferrer" className="text-primary cursor-pointer underline underline-offset-2" href="https://infisical.com/docs/integrations/cloud/render">here</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 max-w-full">
|
||||
<InputField
|
||||
label="API Key"
|
||||
onChangeHandler={setAccessToken}
|
||||
type="varName"
|
||||
value={accessToken}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
<div className="mt-4 max-w-[5.5rem]">
|
||||
<Button
|
||||
onButtonPressed={submit}
|
||||
color="mineshaft"
|
||||
text="Connect"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default IntegrationAccessTokenDialog;
|
@ -41,7 +41,6 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
|
||||
const onEnvCreateCB = async (env: Env) => {
|
||||
try {
|
||||
await onCreateEnv(env);
|
||||
@ -71,25 +70,25 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
|
||||
|
||||
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">
|
||||
<div className="flex w-full flex-row justify-between">
|
||||
<div className="flex w-full flex-col">
|
||||
<p className="mb-3 text-xl font-semibold">Project Environments</p>
|
||||
<p className="mb-4 text-base text-gray-400">
|
||||
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">
|
||||
<p className="mr-1 self-start text-sm text-gray-500">
|
||||
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"
|
||||
text="Add New Environment"
|
||||
onButtonPressed={() => {
|
||||
if (plan !== plans.starter || host !== "https://app.infisical.com") {
|
||||
handlePopUpOpen('createUpdateEnv')
|
||||
if (plan !== plans.starter || host !== 'https://app.infisical.com') {
|
||||
handlePopUpOpen('createUpdateEnv');
|
||||
} else {
|
||||
handlePopUpOpen('upgradePlan')
|
||||
handlePopUpOpen('upgradePlan');
|
||||
}
|
||||
}}
|
||||
color="mineshaft"
|
||||
@ -98,32 +97,30 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
|
||||
/>
|
||||
</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" />
|
||||
<table className="w-full my-1">
|
||||
<div className="table-container relative mb-6 mt-1 w-full rounded-md border border-mineshaft-700 bg-bunker">
|
||||
<div className="absolute h-12 w-full rounded-t-md bg-white/5" />
|
||||
<table className="my-1 w-full">
|
||||
<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 className="pl-6 pt-2.5 pb-2 text-left">Name</th>
|
||||
<th className="pl-6 pt-2.5 pb-2 text-left">Slug</th>
|
||||
<th aria-label="buttons" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.length > 0 ? (
|
||||
data.map(({ name, slug }) => (
|
||||
<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">
|
||||
{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 justify-end">
|
||||
<div className="hover:opacity-100 duration-200 flex items-center mr-2">
|
||||
<tr key={name} className="bg-bunker-800 duration-100 hover:bg-bunker-800/5">
|
||||
<td className="border-t border-mineshaft-700 py-2 pl-6 text-gray-300">{name}</td>
|
||||
<td className="border-t border-mineshaft-700 py-2 pl-6 text-gray-300">{slug}</td>
|
||||
<td className="flex justify-end border-t border-mineshaft-700 py-2">
|
||||
<div className="mr-2 flex items-center duration-200 hover:opacity-100">
|
||||
<Button
|
||||
onButtonPressed={() => {
|
||||
if (plan !== plans.starter || host !== "https://app.infisical.com") {
|
||||
handlePopUpOpen('createUpdateEnv', { name, slug })
|
||||
if (plan !== plans.starter || host !== 'https://app.infisical.com') {
|
||||
handlePopUpOpen('createUpdateEnv', { name, slug });
|
||||
} else {
|
||||
handlePopUpOpen('upgradePlan')
|
||||
handlePopUpOpen('upgradePlan');
|
||||
}
|
||||
}}
|
||||
color="mineshaft"
|
||||
@ -131,13 +128,13 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
|
||||
icon={faPencil}
|
||||
/>
|
||||
</div>
|
||||
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center mr-6">
|
||||
<div className="mr-6 flex items-center opacity-50 duration-200 hover:opacity-100">
|
||||
<Button
|
||||
onButtonPressed={() => {
|
||||
if (plan !== plans.starter || host !== "https://app.infisical.com") {
|
||||
handlePopUpOpen('deleteEnv', { name, slug })
|
||||
if (plan !== plans.starter || host !== 'https://app.infisical.com') {
|
||||
handlePopUpOpen('deleteEnv', { name, slug });
|
||||
} else {
|
||||
handlePopUpOpen('upgradePlan')
|
||||
handlePopUpOpen('upgradePlan');
|
||||
}
|
||||
}}
|
||||
color="red"
|
||||
@ -150,8 +147,8 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={4} className="text-center pt-7 pb-4 text-bunker-400">
|
||||
No environmants found
|
||||
<td colSpan={4} className="pt-7 pb-4 text-center text-bunker-400">
|
||||
No environments found
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
@ -174,7 +171,7 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
|
||||
onCreateSubmit={onEnvCreateCB}
|
||||
onEditSubmit={onEnvUpdateCB}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onClose={() => handlePopUpClose('upgradePlan')}
|
||||
text="You can add custom environments if you switch to Infisical's Team plan."
|
||||
|
@ -2,8 +2,8 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import changeUserRoleInOrganization from '@app/pages/api/organization/changeUserRoleInOrganization';
|
||||
import deleteUserFromOrganization from '@app/pages/api/organization/deleteUserFromOrganization';
|
||||
import changeUserRoleInWorkspace from '@app/pages/api/workspace/changeUserRoleInWorkspace';
|
||||
import deleteUserFromWorkspace from '@app/pages/api/workspace/deleteUserFromWorkspace';
|
||||
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
|
||||
@ -55,9 +55,9 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
||||
]);
|
||||
};
|
||||
|
||||
// Update the rold of a certain user
|
||||
// Update the role of a certain user
|
||||
const handleRoleUpdate = (index: number, e: string) => {
|
||||
changeUserRoleInWorkspace(userData[index].membershipId, e);
|
||||
changeUserRoleInOrganization(String(localStorage.getItem("orgData.id")), userData[index].membershipId, e);
|
||||
changeData([
|
||||
...userData.slice(0, index),
|
||||
...[
|
||||
@ -145,9 +145,9 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
||||
</td>
|
||||
<td className="pl-6 pr-10 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
<div className="justify-start h-full flex flex-row items-center">
|
||||
{row.status === 'granted' &&
|
||||
{row.status === 'accepted' &&
|
||||
((myRole === 'admin' && row.role !== 'owner') || myRole === 'owner') &&
|
||||
myUser !== row.email ? (
|
||||
(myUser !== row.email) ? (
|
||||
<Listbox
|
||||
isSelected={row.role}
|
||||
onChange={(e) => handleRoleUpdate(index, e)}
|
||||
|
61
frontend/src/components/dashboard/AddTagsMenu.tsx
Normal file
61
frontend/src/components/dashboard/AddTagsMenu.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { Fragment } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faCheckSquare, faPlus, faSquare } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import { Tag } from 'public/data/frequentInterfaces';
|
||||
|
||||
/**
|
||||
* This is the menu that is used to download secrets as .env ad .yml files (in future we may have more options)
|
||||
* @param {object} obj
|
||||
* @param {SecretDataProps[]} obj.data -
|
||||
*
|
||||
*/
|
||||
const AddTagsMenu = ({ allTags, currentTags, modifyTags, position }: { allTags: Tag[]; currentTags: Tag[]; modifyTags: (value: Tag[], position: number) => void; position: number; }) => {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<Menu as="div" className="ml-2 relative inline-block text-left">
|
||||
<Menu.Button
|
||||
as="div"
|
||||
className="flex justify-center items-center font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
|
||||
>
|
||||
<div className='bg-mineshaft/30 cursor-pointer rounded-sm text-sm text-mineshaft-200/50 hover:bg-mineshaft/70 duration-200 flex items-center'>
|
||||
<FontAwesomeIcon icon={faPlus} className="p-[0.28rem]"/>
|
||||
{currentTags.length > 2 && <span className='pr-2'>{currentTags.length - 2}</span>}
|
||||
</div>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute z-[90] text-sm drop-shadow-xl right-0 mt-0.5 w-[12rem] origin-top-right rounded-md bg-mineshaft-600 border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-1 space-y-1">
|
||||
{allTags?.map((tag) => { return (
|
||||
<Menu.Item key={tag._id}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${currentTags.map(currentTag => currentTag.name).includes(tag.name) ? "opacity-30 cursor-default" : "hover:bg-mineshaft-700"} w-full text-left bg-mineshaft-800 px-2 py-0.5 text-bunker-200 rounded-sm flex items-center`}
|
||||
onClick={() => {if (!currentTags.map(currentTag => currentTag.name).includes(tag.name)) {modifyTags(currentTags.concat([tag]), position)}}}
|
||||
>
|
||||
{currentTags.map(currentTag => currentTag.name).includes(tag.name) ? <FontAwesomeIcon icon={faCheckSquare} className="text-xs mr-2 text-primary"/> : <FontAwesomeIcon icon={faSquare} className="text-xs mr-2"/>} {tag.name}
|
||||
</button>
|
||||
</Menu.Item>
|
||||
)})}
|
||||
<button
|
||||
type="button"
|
||||
className='w-full text-left bg-mineshaft-800 hover:bg-primary hover:text-black duration-200 px-2 py-0.5 text-bunker-200 rounded-sm'
|
||||
onClick={() => router.push(`/settings/project/${String(router.query.id)}`)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="mr-2 text-xs" />Add more tags
|
||||
</button>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddTagsMenu;
|
88
frontend/src/components/dashboard/CompareSecretsModal.tsx
Normal file
88
frontend/src/components/dashboard/CompareSecretsModal.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import { SetStateAction, useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
import { WorkspaceEnv } from '@app/hooks/api/types';
|
||||
|
||||
import getSecretsForProject from '../utilities/secrets/getSecretsForProject';
|
||||
import { Modal, ModalContent } from '../v2';
|
||||
|
||||
interface Secrets {
|
||||
label: string;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
interface CompareSecretsModalProps {
|
||||
compareModal: boolean;
|
||||
setCompareModal: React.Dispatch<SetStateAction<boolean>>;
|
||||
selectedEnv: WorkspaceEnv;
|
||||
workspaceEnvs: WorkspaceEnv[];
|
||||
workspaceId: string;
|
||||
currentSecret: {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
const CompareSecretsModal = ({
|
||||
compareModal,
|
||||
setCompareModal,
|
||||
selectedEnv,
|
||||
workspaceEnvs,
|
||||
workspaceId,
|
||||
currentSecret
|
||||
}: CompareSecretsModalProps) => {
|
||||
const [secrets, setSecrets] = useState<Secrets[]>([]);
|
||||
|
||||
const getEnvSecrets = async () => {
|
||||
const workspaceEnvironments = workspaceEnvs?.filter((env) => env !== selectedEnv);
|
||||
const newSecrets = await Promise.all(
|
||||
workspaceEnvironments.map(async (env) => {
|
||||
// #TODO: optimize this query somehow...
|
||||
const allSecrets = await getSecretsForProject({ env: env.slug, workspaceId });
|
||||
const secret =
|
||||
allSecrets.find((item) => item.key === currentSecret.key)?.value ?? 'Not found';
|
||||
return { label: env.name, secret };
|
||||
})
|
||||
);
|
||||
setSecrets([{ label: selectedEnv.name, secret: currentSecret.value }, ...newSecrets]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (compareModal) {
|
||||
(async () => {
|
||||
await getEnvSecrets();
|
||||
})();
|
||||
}
|
||||
}, [compareModal]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={compareModal} onOpenChange={setCompareModal}>
|
||||
<ModalContent title={currentSecret?.key} onOpenAutoFocus={(e) => e.preventDefault()}>
|
||||
<div className="space-y-4">
|
||||
{secrets.length === 0 ? (
|
||||
<div className="flex items-center bg-bunker-900 justify-center h-full py-4 rounded-md">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
secrets.map((item) => (
|
||||
<div key={`${currentSecret.key}${item.label}`} className="space-y-0.5">
|
||||
<p className="text-sm text-bunker-300">{item.label}</p>
|
||||
<input
|
||||
defaultValue={item.secret}
|
||||
className="h-no-capture border border-mineshaft-500 text-md min-w-16 no-scrollbar::-webkit-scrollbar peer z-10 w-full rounded-md bg-bunker-800 px-2 py-1.5 font-mono text-gray-400 caret-white outline-none duration-200 no-scrollbar focus:ring-2 focus:ring-primary/50 "
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
export default CompareSecretsModal;
|
@ -1,8 +1,9 @@
|
||||
import { memo, SyntheticEvent, useRef } from 'react';
|
||||
import { faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCircle, faExclamationCircle, faEye, faLayerGroup } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import guidGenerator from '../utilities/randomId';
|
||||
import { HoverObject } from '../v2/HoverCard';
|
||||
|
||||
const REGEX = /([$]{.*?})/g;
|
||||
|
||||
@ -10,10 +11,12 @@ interface DashboardInputFieldProps {
|
||||
position: number;
|
||||
onChangeHandler: (value: string, position: number) => void;
|
||||
value: string | undefined;
|
||||
type: 'varName' | 'value';
|
||||
type: 'varName' | 'value' | 'comment';
|
||||
blurred?: boolean;
|
||||
isDuplicate?: boolean;
|
||||
override?: boolean;
|
||||
overrideEnabled?: boolean;
|
||||
modifyValueOverride?: (value: string | undefined, position: number) => void;
|
||||
isSideBarOpen?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -26,6 +29,8 @@ interface DashboardInputFieldProps {
|
||||
* @param {boolean} obj.blurred - whether the input field should be blurred (behind the gray dots) or not; this can be turned on/off in the dashboard
|
||||
* @param {boolean} obj.isDuplicate - if the key name is duplicated
|
||||
* @param {boolean} obj.override - whether a secret/row should be displalyed as overriden
|
||||
*
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
|
||||
@ -36,7 +41,9 @@ const DashboardInputField = ({
|
||||
value,
|
||||
blurred,
|
||||
isDuplicate,
|
||||
override
|
||||
overrideEnabled,
|
||||
modifyValueOverride,
|
||||
isSideBarOpen
|
||||
}: DashboardInputFieldProps) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
|
||||
@ -51,41 +58,97 @@ const DashboardInputField = ({
|
||||
const error = startsWithNumber || isDuplicate;
|
||||
|
||||
return (
|
||||
<div className="flex-col w-full">
|
||||
<div className={`relative flex-col w-full h-10 ${
|
||||
error && value !== '' ? 'bg-red/[0.15]' : ''
|
||||
} ${
|
||||
isSideBarOpen && 'bg-mineshaft-700 duration-200'
|
||||
}`}>
|
||||
<div
|
||||
className={`group relative flex flex-col justify-center w-full border ${
|
||||
error ? 'border-red' : 'border-mineshaft-500'
|
||||
} rounded-md`}
|
||||
className={`group relative flex flex-col justify-center items-center h-full ${
|
||||
error ? 'w-max' : 'w-full'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
onChange={(e) => onChangeHandler(e.target.value.toUpperCase(), position)}
|
||||
type={type}
|
||||
value={value}
|
||||
className={`z-10 peer font-mono ph-no-capture bg-bunker-800 rounded-md caret-white text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 ${
|
||||
error ? 'focus:ring-red/50' : 'focus:ring-primary/50'
|
||||
className={`z-10 peer font-mono ph-no-capture bg-transparent h-full caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none ${
|
||||
error ? 'text-red-600 focus:text-red-500' : 'text-bunker-300 focus:text-bunker-100'
|
||||
} duration-200`}
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
{startsWithNumber && (
|
||||
<p className="text-red text-xs mt-0.5 mx-1 mb-2 max-w-xs">
|
||||
Should not start with a number
|
||||
</p>
|
||||
<div className='absolute right-2 top-2 text-red z-50'>
|
||||
<HoverObject
|
||||
text="Secret names should not start with a number"
|
||||
icon={faExclamationCircle}
|
||||
color="red"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isDuplicate && !startsWithNumber && (
|
||||
<p className="text-red text-xs mt-0.5 mx-1 mb-2 max-w-xs">
|
||||
Secret names should be unique
|
||||
</p>
|
||||
{isDuplicate && value !== '' && !startsWithNumber && (
|
||||
<div className='absolute right-2 top-2 text-red z-50'>
|
||||
<HoverObject
|
||||
text="Secret names should be unique"
|
||||
icon={faExclamationCircle}
|
||||
color="red"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!error && <div className={`absolute right-0 top-0 text-red z-50 ${
|
||||
overrideEnabled ? 'visible group-hover:bg-mineshaft-700' : 'invisible group-hover:visible bg-mineshaft-700'
|
||||
} cursor-pointer duration-0 h-10 flex items-center px-2`}>
|
||||
<button type="button" onClick={() => {
|
||||
if (modifyValueOverride) {
|
||||
if (overrideEnabled === false) {
|
||||
modifyValueOverride('', position);
|
||||
} else {
|
||||
modifyValueOverride(undefined, position);
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<HoverObject
|
||||
text={overrideEnabled ? 'This secret is overriden with your personal value' : 'You can override this secret with a personal value'}
|
||||
icon={faLayerGroup}
|
||||
color={overrideEnabled ? 'primary' : 'bunker-400'}
|
||||
/>
|
||||
</button>
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (type === 'comment') {
|
||||
const startsWithNumber = !Number.isNaN(Number(value?.charAt(0))) && value !== '';
|
||||
const error = startsWithNumber || isDuplicate;
|
||||
|
||||
return (
|
||||
<div className={`relative flex-col w-full h-10 ${
|
||||
isSideBarOpen && 'bg-mineshaft-700 duration-200'
|
||||
}`}>
|
||||
<div
|
||||
className={`group relative flex flex-col justify-center items-center ${
|
||||
error ? 'w-max' : 'w-full'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
onChange={(e) => onChangeHandler(e.target.value, position)}
|
||||
type={type}
|
||||
value={value}
|
||||
className='z-10 peer font-mono ph-no-capture bg-transparent py-2.5 caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200'
|
||||
spellCheck="false"
|
||||
placeholder='–'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (type === 'value') {
|
||||
return (
|
||||
<div className="flex-col w-full">
|
||||
<div className="group relative whitespace-pre flex flex-col justify-center w-full border border-mineshaft-500 rounded-md">
|
||||
{override === true && (
|
||||
<div className="bg-primary-300 absolute top-[0.1rem] right-[0.1rem] z-10 w-min text-xxs px-1 text-black opacity-80 rounded-md">
|
||||
<div className="group relative whitespace-pre flex flex-col justify-center w-full">
|
||||
{overrideEnabled === true && (
|
||||
<div className="bg-primary-500 rounded-sm absolute top-[0.1rem] right-[0.1rem] z-0 w-min text-xxs px-1 text-black opacity-80">
|
||||
Override enabled
|
||||
</div>
|
||||
)}
|
||||
@ -95,19 +158,19 @@ const DashboardInputField = ({
|
||||
onScroll={syncScroll}
|
||||
className={`${
|
||||
blurred
|
||||
? 'text-transparent group-hover:text-transparent focus:text-transparent active:text-transparent'
|
||||
? 'text-transparent focus:text-transparent active:text-transparent'
|
||||
: ''
|
||||
} z-10 peer font-mono ph-no-capture bg-transparent rounded-md caret-white text-transparent text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
} z-10 peer font-mono ph-no-capture bg-transparent caret-white text-transparent text-sm px-2 py-2 w-full min-w-16 outline-none duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div
|
||||
ref={ref}
|
||||
className={`${
|
||||
blurred && !override
|
||||
? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-400 peer-active:text-gray-400'
|
||||
blurred && !overrideEnabled
|
||||
? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400 duration-200'
|
||||
: ''
|
||||
} ${override ? 'text-primary-300' : 'text-gray-400'}
|
||||
absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture overflow-x-scroll bg-bunker-800 h-9 rounded-md text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
} ${overrideEnabled ? 'text-primary-300' : 'text-gray-400'}
|
||||
absolute flex flex-row whitespace-pre font-mono z-0 ${blurred ? 'invisible' : 'visible'} peer-focus:visible mt-0.5 ph-no-capture overflow-x-scroll bg-transparent h-10 text-sm px-2 py-2 w-full min-w-16 outline-none duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
>
|
||||
{value?.split(REGEX).map((word, id) => {
|
||||
if (word.match(REGEX) !== null) {
|
||||
@ -137,7 +200,9 @@ const DashboardInputField = ({
|
||||
})}
|
||||
</div>
|
||||
{blurred && (
|
||||
<div className="absolute flex flex-row items-center z-20 peer pr-2 bg-bunker-800 group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible h-9 w-full rounded-md text-gray-400/50 text-clip">
|
||||
<div className={`absolute flex flex-row justify-between items-center z-0 peer pr-2 ${
|
||||
isSideBarOpen ? 'bg-mineshaft-700 duration-200' : 'bg-mineshaft-800'
|
||||
} peer-active:hidden peer-focus:hidden group-hover:bg-white/[0.00] duration-100 h-10 w-full text-bunker-400 text-clip`}>
|
||||
<div className="px-2 flex flex-row items-center overflow-x-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
|
||||
{value?.split('').map(() => (
|
||||
<FontAwesomeIcon
|
||||
@ -146,7 +211,9 @@ const DashboardInputField = ({
|
||||
icon={faCircle}
|
||||
/>
|
||||
))}
|
||||
{value?.split('').length === 0 && <span className='text-bunker-400/80'>EMPTY</span>}
|
||||
</div>
|
||||
<div className='invisible group-hover:visible cursor-pointer'><FontAwesomeIcon icon={faEye} /></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -163,8 +230,8 @@ function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInput
|
||||
prev.type === next.type &&
|
||||
prev.position === next.position &&
|
||||
prev.blurred === next.blurred &&
|
||||
prev.override === next.override &&
|
||||
prev.isDuplicate === next.isDuplicate
|
||||
prev.overrideEnabled === next.overrideEnabled &&
|
||||
prev.isDuplicate === next.isDuplicate
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,26 +1,42 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import { DeleteEnvVar } from '../basic/dialog/DeleteEnvVar';
|
||||
|
||||
type Props = {
|
||||
onSubmit: () => void
|
||||
onSubmit: () => void;
|
||||
isPlain?: boolean;
|
||||
}
|
||||
|
||||
export const DeleteActionButton = ({ onSubmit }: Props) => {
|
||||
export const DeleteActionButton = ({ onSubmit, isPlain }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="bg-[#9B3535] opacity-70 hover:opacity-100 w-[4.5rem] h-[2.5rem] rounded-md duration-200 ml-2">
|
||||
<Button
|
||||
<div className={`${
|
||||
!isPlain
|
||||
? 'bg-[#9B3535] opacity-70 hover:opacity-100 w-[4.5rem] h-[2.5rem] rounded-md duration-200 ml-2'
|
||||
: 'cursor-pointer w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center'}`}>
|
||||
{isPlain
|
||||
? <div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setOpen(true)}
|
||||
className="invisible group-hover:visible"
|
||||
>
|
||||
<FontAwesomeIcon className="text-bunker-300 hover:text-red pl-2 pr-6 text-lg mt-0.5" icon={faXmark} />
|
||||
</div>
|
||||
: <Button
|
||||
text={String(t("Delete"))}
|
||||
// onButtonPressed={onSubmit}
|
||||
color="red"
|
||||
size="md"
|
||||
onButtonPressed={() => setOpen(true)}
|
||||
/>
|
||||
/>}
|
||||
<DeleteEnvVar
|
||||
isOpen={open}
|
||||
onClose={() => {
|
||||
|
@ -31,7 +31,7 @@ const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: strin
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[12rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-2 space-y-2">
|
||||
<Menu.Items className="absolute z-[90] drop-shadow-xl right-0 mt-0.5 w-[12rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-2 space-y-2">
|
||||
<Menu.Item>
|
||||
<Button
|
||||
color="mineshaft"
|
||||
|
@ -64,7 +64,8 @@ const DropZone = ({
|
||||
key,
|
||||
value: keyPairs[key as keyof typeof keyPairs].value,
|
||||
comment: keyPairs[key as keyof typeof keyPairs].comments.join('\n'),
|
||||
type: 'shared'
|
||||
type: 'shared',
|
||||
tags: []
|
||||
}));
|
||||
break;
|
||||
}
|
||||
@ -86,7 +87,8 @@ const DropZone = ({
|
||||
key,
|
||||
value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? '',
|
||||
comment,
|
||||
type: 'shared'
|
||||
type: 'shared',
|
||||
tags: []
|
||||
};
|
||||
});
|
||||
break;
|
||||
@ -152,7 +154,7 @@ const DropZone = ({
|
||||
</div>
|
||||
) : keysExist ? (
|
||||
<div
|
||||
className="opacity-60 hover:opacity-100 duration-200 relative bg-mineshaft-900 max-w-[calc(100%-1rem)] w-full outline-dashed outline-chicago-600 rounded-md outline-2 flex flex-col items-center justify-center mb-16 mx-auto mt-1 py-8 px-2"
|
||||
className="opacity-60 hover:opacity-100 duration-200 relative bg-mineshaft-900 max-w-[calc(100%-1rem)] w-full outline-dashed outline-chicago-600 rounded-md outline-2 flex flex-col items-center justify-center mb-4 mx-auto mt-1 py-8 px-2"
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
|
@ -1,21 +1,50 @@
|
||||
import { faEllipsis } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faEllipsis, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
||||
import { SecretDataProps, Tag } from 'public/data/frequentInterfaces';
|
||||
|
||||
import AddTagsMenu from './AddTagsMenu';
|
||||
import DashboardInputField from './DashboardInputField';
|
||||
import { DeleteActionButton } from './DeleteActionButton';
|
||||
|
||||
interface KeyPairProps {
|
||||
keyPair: SecretDataProps;
|
||||
modifyKey: (value: string, position: number) => void;
|
||||
modifyValue: (value: string, position: number) => void;
|
||||
modifyValueOverride: (value: string, position: number) => void;
|
||||
modifyValueOverride: (value: string | undefined, position: number) => void;
|
||||
modifyComment: (value: string, position: number) => void;
|
||||
modifyTags: (value: Tag[], position: number) => void;
|
||||
isBlurred: boolean;
|
||||
isDuplicate: boolean;
|
||||
toggleSidebar: (id: string) => void;
|
||||
sidebarSecretId: string;
|
||||
isSnapshot: boolean;
|
||||
deleteRow?: (props: DeleteRowFunctionProps) => void;
|
||||
tags: Tag[];
|
||||
togglePITSidebar?: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export interface DeleteRowFunctionProps {
|
||||
ids: string[];
|
||||
secretName: string;
|
||||
}
|
||||
|
||||
const colors = [
|
||||
'bg-[#f1c40f]/40',
|
||||
'bg-[#cb1c8d]/40',
|
||||
'bg-[#badc58]/40',
|
||||
'bg-[#ff5400]/40',
|
||||
'bg-[#00bbf9]/40'
|
||||
]
|
||||
|
||||
|
||||
const colorsText = [
|
||||
'text-[#fcf0c3]/70',
|
||||
'text-[#f2c6e3]/70',
|
||||
'text-[#eef6d5]/70',
|
||||
'text-[#ffddcc]/70',
|
||||
'text-[#f0fffd]/70'
|
||||
]
|
||||
|
||||
/**
|
||||
* This component represent a single row for an environemnt variable on the dashboard
|
||||
* @param {object} obj
|
||||
@ -23,11 +52,16 @@ interface KeyPairProps {
|
||||
* @param {function} obj.modifyKey - modify the key of a certain environment variable
|
||||
* @param {function} obj.modifyValue - modify the value of a certain environment variable
|
||||
* @param {function} obj.modifyValueOverride - modify the value of a certain environment variable if it is overriden
|
||||
* @param {function} obj.modifyComment - modify the comment of a certain environment variable
|
||||
* @param {function} obj.modifyTags - modify the tags of a certain environment variable
|
||||
* @param {boolean} obj.isBlurred - if the blurring setting is turned on
|
||||
* @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
|
||||
* @param {function} obj.toggleSidebar - open/close/switch sidebar
|
||||
* @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
|
||||
* @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar
|
||||
* @param {function} obj.deleteRow - a function to delete a certain keyPair
|
||||
* @param {function} obj.togglePITSidebar - open or close the Point-in-time recovery sidebar
|
||||
* @param {Tag[]} obj.tags - tags for a certain secret
|
||||
* @returns
|
||||
*/
|
||||
const KeyPair = ({
|
||||
@ -35,66 +69,108 @@ const KeyPair = ({
|
||||
modifyKey,
|
||||
modifyValue,
|
||||
modifyValueOverride,
|
||||
modifyComment,
|
||||
modifyTags,
|
||||
isBlurred,
|
||||
isDuplicate,
|
||||
toggleSidebar,
|
||||
sidebarSecretId,
|
||||
isSnapshot
|
||||
}: KeyPairProps) => (
|
||||
isSnapshot,
|
||||
deleteRow,
|
||||
togglePITSidebar,
|
||||
tags
|
||||
}: KeyPairProps) => {
|
||||
const tagData = (tags.map((tag, index) => {return {
|
||||
...tag,
|
||||
color: colors[index%colors.length],
|
||||
colorText: colorsText[index%colorsText.length]
|
||||
}}));
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`mx-1 flex flex-col items-center ml-1 ${isSnapshot && 'pointer-events-none'} ${
|
||||
keyPair.id === sidebarSecretId && 'bg-mineshaft-500 duration-200'
|
||||
} rounded-md`}
|
||||
className={`group flex flex-col items-center border-b border-mineshaft-500 hover:bg-white/[0.03] duration-100 ${isSnapshot && 'pointer-events-none'} ${
|
||||
keyPair.id === sidebarSecretId && 'bg-mineshaft-700 duration-200'
|
||||
}`}
|
||||
>
|
||||
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-1">
|
||||
{keyPair.valueOverride && (
|
||||
<div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
|
||||
<div className="w-1 h-1 rounded-full bg-primary z-40" />
|
||||
<span className="absolute z-50 hidden group-hover:flex group-hover:animate-popdown duration-200 w-[10.5rem] -left-[0.4rem] -top-[1.7rem] translate-y-full px-2 py-2 bg-mineshaft-500 rounded-b-md rounded-r-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-0 after:bottom-[100%] after:-translate-x-0 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-mineshaft-500">
|
||||
This secret is overriden
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-xl w-96">
|
||||
<div className="flex pr-1.5 items-center rounded-lg mt-4 md:mt-0 max-h-16">
|
||||
<div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
|
||||
<div className="w-2/12 border-r border-mineshaft-600 flex flex-row items-center">
|
||||
<div className='text-bunker-400 text-xs flex items-center justify-center w-14 h-10 cursor-default'>{keyPair.pos + 1}</div>
|
||||
<div className="flex items-center max-h-16">
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={keyPair.pos}
|
||||
value={keyPair.key}
|
||||
isDuplicate={isDuplicate}
|
||||
overrideEnabled={keyPair.valueOverride !== undefined}
|
||||
modifyValueOverride={modifyValueOverride}
|
||||
isSideBarOpen={keyPair.id === sidebarSecretId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full min-w-xl">
|
||||
<div className="w-5/12 border-r border-mineshaft-600">
|
||||
<div
|
||||
className={`flex min-w-xl items-center ${
|
||||
!isSnapshot && 'pr-1.5'
|
||||
} rounded-lg mt-4 md:mt-0 max-h-10`}
|
||||
className='flex items-center rounded-lg mt-4 md:mt-0 max-h-10'
|
||||
>
|
||||
<DashboardInputField
|
||||
onChangeHandler={keyPair.valueOverride ? modifyValueOverride : modifyValue}
|
||||
onChangeHandler={keyPair.valueOverride !== undefined ? modifyValueOverride : modifyValue}
|
||||
type="value"
|
||||
position={keyPair.pos}
|
||||
value={keyPair.valueOverride ? keyPair.valueOverride : keyPair.value}
|
||||
value={keyPair.valueOverride !== undefined ? keyPair.valueOverride : keyPair.value}
|
||||
blurred={isBlurred}
|
||||
override={Boolean(keyPair.valueOverride)}
|
||||
overrideEnabled={keyPair.valueOverride !== undefined}
|
||||
isSideBarOpen={keyPair.id === sidebarSecretId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!isSnapshot && (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => toggleSidebar(keyPair.id)}
|
||||
className="cursor-pointer w-[2.35rem] h-[2.35rem] bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200"
|
||||
>
|
||||
<FontAwesomeIcon className="text-gray-300 px-2.5 text-lg mt-0.5" icon={faEllipsis} />
|
||||
<div className="w-2/12 border-r border-mineshaft-600">
|
||||
<div className="flex items-center max-h-16">
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyComment}
|
||||
type="comment"
|
||||
position={keyPair.pos}
|
||||
value={keyPair.comment}
|
||||
isDuplicate={isDuplicate}
|
||||
isSideBarOpen={keyPair.id === sidebarSecretId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-2/12 h-10 flex items-center overflow-visible overflow-r-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
|
||||
<div className="flex items-center max-h-16">
|
||||
{keyPair.tags.map((tag, index) => (
|
||||
index < 2 && <div key={keyPair.pos} className={`ml-2 px-1.5 ${tagData.filter(tagDp => tagDp._id === tag._id)[0]?.color} rounded-sm text-sm ${tagData.filter(tagDp => tagDp._id === tag._id)[0]?.colorText} flex items-center`}>
|
||||
<span className='mb-0.5 cursor-default'>{tag.name}</span>
|
||||
<FontAwesomeIcon icon={faXmark} className="ml-1 cursor-pointer p-1" onClick={() => modifyTags(keyPair.tags.filter(ttag => ttag._id !== tag._id), keyPair.pos)}/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<AddTagsMenu allTags={tags} currentTags={keyPair.tags} modifyTags={modifyTags} position={keyPair.pos} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
if (togglePITSidebar) {
|
||||
togglePITSidebar(false);
|
||||
}
|
||||
toggleSidebar(keyPair.id)
|
||||
}}
|
||||
className={`cursor-pointer w-[1.5rem] h-[2.35rem] ml-auto group-hover:bg-mineshaft-700 z-50 rounded-md invisible group-hover:visible flex flex-row justify-center items-center ${isSnapshot ?? 'invisible'}`}
|
||||
>
|
||||
<FontAwesomeIcon className="text-bunker-300 hover:text-primary text-lg" icon={faEllipsis} />
|
||||
</div>
|
||||
<div className={`group-hover:bg-mineshaft-700 z-50 ${isSnapshot ?? 'invisible'}`}>
|
||||
<DeleteActionButton
|
||||
onSubmit={() => { if (deleteRow) {
|
||||
deleteRow({ ids: [keyPair.id], secretName: keyPair?.key })
|
||||
}}}
|
||||
isPlain
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)};
|
||||
|
||||
export default KeyPair;
|
||||
|
@ -2,14 +2,16 @@
|
||||
import { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import SecretVersionList from '@app/ee/components/SecretVersionList';
|
||||
import { WorkspaceEnv } from '@app/hooks/api/types';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import Toggle from '../basic/Toggle';
|
||||
import CommentField from './CommentField';
|
||||
import CompareSecretsModal from './CompareSecretsModal';
|
||||
import DashboardInputField from './DashboardInputField';
|
||||
import { DeleteActionButton } from './DeleteActionButton';
|
||||
import GenerateSecretMenu from './GenerateSecretMenu';
|
||||
@ -40,6 +42,9 @@ interface SideBarProps {
|
||||
sharedToHide: string[];
|
||||
setSharedToHide: (values: string[]) => void;
|
||||
deleteRow: (props: DeleteRowFunctionProps) => void;
|
||||
workspaceEnvs: WorkspaceEnv[];
|
||||
selectedEnv: WorkspaceEnv;
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -63,15 +68,19 @@ const SideBar = ({
|
||||
modifyComment,
|
||||
buttonReady,
|
||||
savePush,
|
||||
deleteRow
|
||||
deleteRow,
|
||||
workspaceEnvs,
|
||||
selectedEnv,
|
||||
workspaceId
|
||||
}: SideBarProps) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [overrideEnabled, setOverrideEnabled] = useState(data[0].valueOverride !== undefined);
|
||||
const [overrideEnabled, setOverrideEnabled] = useState(data[0]?.valueOverride !== undefined);
|
||||
const [compareModal, setCompareModal] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="absolute border-l border-mineshaft-500 bg-bunker h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between">
|
||||
<div className="absolute border-l border-mineshaft-500 bg-bunker h-full w-[28rem] sticky top-0 right-0 z-[70] shadow-xl flex flex-col justify-between">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Image
|
||||
@ -92,19 +101,21 @@ const SideBar = ({
|
||||
className="p-1"
|
||||
onClick={() => toggleSidebar('None')}
|
||||
>
|
||||
<FontAwesomeIcon icon={faX} className="w-4 h-4 text-bunker-300 cursor-pointer" />
|
||||
<FontAwesomeIcon icon={faXmark} className="w-4 h-4 text-bunker-300 cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 px-4 pointer-events-none">
|
||||
<p className="text-sm text-bunker-300">{t('dashboard:sidebar.key')}</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={data[0]?.pos}
|
||||
value={data[0]?.key}
|
||||
isDuplicate={false}
|
||||
blurred={false}
|
||||
/>
|
||||
<div className='rounded-md border overflow-hidden border-mineshaft-600 bg-white/5'>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={data[0]?.pos}
|
||||
value={data[0]?.key}
|
||||
isDuplicate={false}
|
||||
blurred={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{(data[0]?.value || data[0]?.value === "") ? (
|
||||
<div
|
||||
@ -113,14 +124,16 @@ const SideBar = ({
|
||||
} duration-200`}
|
||||
>
|
||||
<p className="text-sm text-bunker-300">{t('dashboard:sidebar.value')}</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={data[0].pos}
|
||||
value={data[0]?.value}
|
||||
isDuplicate={false}
|
||||
blurred
|
||||
/>
|
||||
<div className='rounded-md border overflow-hidden border-mineshaft-600 bg-white/5'>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={data[0].pos}
|
||||
value={data[0]?.value}
|
||||
isDuplicate={false}
|
||||
blurred
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50">
|
||||
<GenerateSecretMenu modifyValue={modifyValue} position={data[0]?.pos} />
|
||||
</div>
|
||||
@ -150,14 +163,16 @@ const SideBar = ({
|
||||
!overrideEnabled && 'opacity-40 pointer-events-none'
|
||||
} duration-200`}
|
||||
>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValueOverride}
|
||||
type="value"
|
||||
position={data[0]?.pos}
|
||||
value={overrideEnabled ? data[0]?.valueOverride : data[0]?.value}
|
||||
isDuplicate={false}
|
||||
blurred
|
||||
/>
|
||||
<div className='rounded-md border overflow-hidden border-mineshaft-600 bg-white/5'>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValueOverride}
|
||||
type="value"
|
||||
position={data[0]?.pos}
|
||||
value={overrideEnabled ? data[0]?.valueOverride : data[0]?.value}
|
||||
isDuplicate={false}
|
||||
blurred
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute right-[0.57rem] top-[0.3rem] z-50">
|
||||
<GenerateSecretMenu modifyValue={modifyValueOverride} position={data[0]?.pos} />
|
||||
</div>
|
||||
@ -171,20 +186,38 @@ const SideBar = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]">
|
||||
<Button
|
||||
text={String(t('common:save-changes'))}
|
||||
onButtonPressed={savePush}
|
||||
color="primary"
|
||||
size="md"
|
||||
active={buttonReady}
|
||||
textDisabled="Saved"
|
||||
/>
|
||||
<DeleteActionButton
|
||||
onSubmit={() =>
|
||||
deleteRow({ ids: data.map((secret) => secret.id), secretName: data[0]?.key })
|
||||
}
|
||||
/>
|
||||
<div className="mt-full mt-4 mb-4 flex max-w-sm flex-col justify-start space-y-2 px-4">
|
||||
<div>
|
||||
<Button
|
||||
text="Compare secret across environments"
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
onButtonPressed={() => setCompareModal(true)}
|
||||
/>
|
||||
<CompareSecretsModal
|
||||
compareModal={compareModal}
|
||||
setCompareModal={setCompareModal}
|
||||
currentSecret={{ key: data[0]?.key, value: data[0]?.value }}
|
||||
workspaceEnvs={workspaceEnvs}
|
||||
selectedEnv={selectedEnv}
|
||||
workspaceId={workspaceId}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button
|
||||
text={String(t('common:save-changes'))}
|
||||
onButtonPressed={savePush}
|
||||
color="primary"
|
||||
size="md"
|
||||
active={buttonReady}
|
||||
textDisabled="Saved"
|
||||
/>
|
||||
<DeleteActionButton
|
||||
onSubmit={() =>
|
||||
deleteRow({ ids: data.map((secret) => secret.id), secretName: data[0]?.key })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -4,7 +4,7 @@ import { useRouter } from 'next/router';
|
||||
import { faArrowRight, faRotate, faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
// TODO: This needs to be moved from public folder
|
||||
import { contextNetlifyMapping, reverseContextNetlifyMapping } from 'public/data/frequentConstants';
|
||||
import { contextNetlifyMapping, integrationSlugNameMapping, reverseContextNetlifyMapping } from 'public/data/frequentConstants';
|
||||
|
||||
import Button from '@app/components/basic/buttons/Button';
|
||||
import ListBox from '@app/components/basic/Listbox';
|
||||
@ -52,6 +52,7 @@ const IntegrationTile = ({
|
||||
environments = [],
|
||||
handleDeleteIntegration
|
||||
}: Props) => {
|
||||
|
||||
// set initial environment. This find will only execute when component is mounting
|
||||
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
|
||||
environments.find(({ slug }) => slug === integration.environment) || {
|
||||
@ -72,7 +73,14 @@ const IntegrationTile = ({
|
||||
});
|
||||
|
||||
setApps(tempApps);
|
||||
setIntegrationApp(integration.app ? integration.app : tempApps[0].name);
|
||||
|
||||
if (integration?.app) {
|
||||
setIntegrationApp(integration.app);
|
||||
} else if (tempApps.length > 0) {
|
||||
setIntegrationApp(tempApps[0].name)
|
||||
} else {
|
||||
setIntegrationApp('');
|
||||
}
|
||||
|
||||
switch (integration.integration) {
|
||||
case 'vercel':
|
||||
@ -174,7 +182,7 @@ const IntegrationTile = ({
|
||||
return <div />;
|
||||
};
|
||||
|
||||
if (!integrationApp || apps.length === 0) return <div />;
|
||||
if (!integrationApp) return <div />;
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between">
|
||||
@ -201,7 +209,8 @@ const IntegrationTile = ({
|
||||
<div className="mr-2">
|
||||
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p>
|
||||
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300">
|
||||
{integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)}
|
||||
{/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */}
|
||||
{integrationSlugNameMapping[integration.integration]}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mr-2">
|
||||
|
@ -93,7 +93,7 @@ export default function Navbar() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-50">
|
||||
<div className="flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-50">
|
||||
<div className="m-auto flex justify-start items-center mx-4">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex justify-center py-4">
|
||||
|
@ -3,8 +3,8 @@ import { useRouter } from 'next/router';
|
||||
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { useWorkspace } from '@app/context';
|
||||
import getOrganization from '@app/pages/api/organization/GetOrg';
|
||||
import getProjectInfo from '@app/pages/api/workspace/getProjectInfo';
|
||||
|
||||
/**
|
||||
* This is the component at the top of almost every page.
|
||||
@ -23,9 +23,9 @@ export default function NavHeader({
|
||||
isProjectRelated?: boolean;
|
||||
}): JSX.Element {
|
||||
const [orgName, setOrgName] = useState('');
|
||||
const [workspaceName, setWorkspaceName] = useState('');
|
||||
const router = useRouter();
|
||||
const projectId = String(router.query.id);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@ -34,29 +34,24 @@ export default function NavHeader({
|
||||
orgId: orgId || ''
|
||||
});
|
||||
setOrgName(org.name);
|
||||
|
||||
const workspace = await getProjectInfo({
|
||||
projectId
|
||||
});
|
||||
setWorkspaceName(workspace.name);
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [projectId]);
|
||||
|
||||
return (
|
||||
<div className="pt-20 ml-6 flex flex-row items-center">
|
||||
<div className="bg-primary-900 h-6 w-6 rounded-md flex items-center justify-center text-mineshaft-100 mr-2">
|
||||
<div className="ml-6 flex flex-row items-center pt-8">
|
||||
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
|
||||
{orgName?.charAt(0)}
|
||||
</div>
|
||||
<div className="text-primary text-sm font-semibold">{orgName}</div>
|
||||
<div className="text-sm font-semibold text-primary">{orgName}</div>
|
||||
{isProjectRelated && (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 text-sm text-gray-400 mr-3" />
|
||||
<div className="font-semibold text-primary text-sm">{workspaceName}</div>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
<div className="text-sm font-semibold text-primary">{currentWorkspace?.name}</div>
|
||||
</>
|
||||
)}
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 text-sm text-gray-400 mr-3" />
|
||||
<div className="text-gray-400 text-sm">{pageName}</div>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
<div className="text-sm text-gray-400">{pageName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user