mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-24 21:44:53 +00:00
Compare commits
455 Commits
Author | SHA1 | Date | |
---|---|---|---|
2bff7bbb5a | |||
b09ae054dd | |||
dc9c6b9d13 | |||
f01e8cb33b | |||
3ba62d1d97 | |||
5816ce3bf7 | |||
2d77fe9ca3 | |||
6bb24933bf | |||
fbc38c553a | |||
6ba4701db8 | |||
c15a9301af | |||
91052df5f9 | |||
6ea26c135a | |||
5444382d5a | |||
200bb12ad8 | |||
1447e055d1 | |||
4dac03ab94 | |||
20ea50bfaf | |||
5474096ca9 | |||
06c1827f38 | |||
9b7f036fd0 | |||
547555591b | |||
516819507a | |||
02e5be20c2 | |||
de11c50563 | |||
33dddd440c | |||
19b909cd12 | |||
cd59ca745d | |||
e013a4ab93 | |||
19daf1410a | |||
4c29c88fde | |||
6af59e47f5 | |||
8183e61403 | |||
b4c616edd6 | |||
c12eeac9b3 | |||
033275ed69 | |||
a799e1bffc | |||
36300cd19d | |||
c8633bf546 | |||
7fe2e15a98 | |||
72bf160f2e | |||
0ef9db99b4 | |||
805f733499 | |||
595dc78e75 | |||
6c7d232a9e | |||
35fd1520e2 | |||
3626ef2ec2 | |||
a49fcf49f1 | |||
787e54fb91 | |||
90537f2e6d | |||
c9448656bf | |||
f3900213b5 | |||
fe17d8459b | |||
2f54c4dd7e | |||
c33b043f5f | |||
5db60c0dad | |||
1a3d3906da | |||
d86c335671 | |||
62f0b3f6df | |||
3e623922b4 | |||
d1c38513f7 | |||
63253d515f | |||
584d309b80 | |||
07bb3496e7 | |||
c83c75db96 | |||
bcd18ab0af | |||
1ea75eb840 | |||
271c810692 | |||
dd05e2ac01 | |||
c6c2cfaaa5 | |||
6f90064400 | |||
397c15d61e | |||
9e3ac6c31d | |||
10d57e9d88 | |||
95a1e9560e | |||
11e0790f13 | |||
099ddd6805 | |||
182db69ee1 | |||
01982b585f | |||
752ebaa3eb | |||
117acce3f6 | |||
f94bf1f206 | |||
74d5586005 | |||
8896e1232b | |||
d456dcef28 | |||
eae2fc813a | |||
7cdafe0eed | |||
d410b42a34 | |||
bacf9f2d91 | |||
3fc6b0c194 | |||
9dc1645559 | |||
158c51ff3c | |||
864757e428 | |||
fa53a9e41d | |||
36372ebef3 | |||
b3a50d657d | |||
c2f5f19f55 | |||
2b6e69ce1b | |||
c4b4829694 | |||
d503102f75 | |||
4a14753b8c | |||
2c834040b4 | |||
6f682250b6 | |||
52285a1f38 | |||
31d6191251 | |||
d14ed06d4f | |||
e2a84ce52e | |||
999f668f39 | |||
6b546034f4 | |||
c2eaea21f0 | |||
436f408fa8 | |||
da999107f3 | |||
00ea296138 | |||
968af64ee7 | |||
589e811e9b | |||
4f312cfd1a | |||
3dccfc5404 | |||
2b3a996114 | |||
93da106dbc | |||
7e544fcac8 | |||
c2eac43b4f | |||
138acd28e8 | |||
1195398f15 | |||
1fff273abb | |||
b586fcfd2e | |||
45ad639eaf | |||
e5342bd757 | |||
834c32aa7e | |||
df4dcc87e7 | |||
d883c7ea96 | |||
5ed02955f8 | |||
064a9eb9cb | |||
f805691c4f | |||
4548931df3 | |||
6d1dc3845b | |||
40b42fdcb5 | |||
ad907fa373 | |||
ae1088d3f6 | |||
9a3caac75f | |||
1808ab6db8 | |||
aa554405c1 | |||
cd70128ff8 | |||
f49fe3962d | |||
9ee0c8f1b7 | |||
a9bd878057 | |||
a58f91f06b | |||
059f15b172 | |||
caddb45394 | |||
8266c4dd6d | |||
9f82220f4e | |||
3d25baa319 | |||
a8dfcae777 | |||
228c8a7609 | |||
a763d8b8ed | |||
d4e0a4992c | |||
1757f0d690 | |||
b25908d91f | |||
68d51d402a | |||
aa218d2ddc | |||
c36aa3591a | |||
58b252a9e9 | |||
dba4c03e37 | |||
7b1be82bac | |||
3c449750d3 | |||
006f61a0e8 | |||
ec85bfca04 | |||
27353848c1 | |||
da6a8ccdea | |||
b5a4e42281 | |||
1212b5a9db | |||
d99e21a91b | |||
112fc77a06 | |||
c23be77738 | |||
0cea019bc2 | |||
1218dc09ed | |||
b89a221a5a | |||
c4124cc865 | |||
94b7a0aead | |||
689f1d0d43 | |||
c75c24d44e | |||
febdf48dea | |||
688b383d8b | |||
9436f40eac | |||
d45eff621b | |||
761a60a216 | |||
0466bf4e3d | |||
16a1366e6c | |||
b06c8d241e | |||
7bbaf4fee8 | |||
91c8fd14df | |||
9e1112eb52 | |||
6050e65a59 | |||
bbc23aca55 | |||
9098bdc751 | |||
251426b559 | |||
793eaee0c6 | |||
349865e6ef | |||
add3075439 | |||
3db1ff2411 | |||
8cd9e20fa6 | |||
29ab13430f | |||
90eb292721 | |||
5e1f6d3884 | |||
1310b176a9 | |||
ccf1010e94 | |||
d46bf54a8d | |||
93703475fe | |||
991b10cc17 | |||
60fcd34af5 | |||
f60e0cf7ee | |||
9071fafd06 | |||
9499aa1097 | |||
389bf0b41f | |||
6ed5b9e706 | |||
209673d744 | |||
baacc310bb | |||
1e16a18469 | |||
df1ade4f5f | |||
944cc5b32c | |||
46fe724012 | |||
a83d536ea4 | |||
1454911085 | |||
9e73b3431e | |||
4a105a72e9 | |||
a47decd31f | |||
c5a422fe64 | |||
bb47f7a92f | |||
13f2ab9425 | |||
ac2c50b161 | |||
afb374ff13 | |||
e98b76cba5 | |||
3e2ed62e50 | |||
8e15dfc3d9 | |||
6fb22b68dd | |||
05a19a2201 | |||
9ee5f3d41b | |||
142a38ae3c | |||
e67620c3ce | |||
e8e6b72422 | |||
4cc4edcb7e | |||
2acea4f085 | |||
0dd546813a | |||
d82dfa5504 | |||
b13b0693ba | |||
e00c3ab9e2 | |||
088668e1b0 | |||
b21cb521da | |||
90ef67399b | |||
5e87a317fa | |||
6cdbc834ab | |||
b677ab6429 | |||
3b002abcb6 | |||
89c625750a | |||
3de79426e2 | |||
02284f350b | |||
0e011da41c | |||
4fca41f62c | |||
f7044d37cb | |||
2299cff7d8 | |||
9c66062e6e | |||
127f77d1ce | |||
040fa815df | |||
0eff4a7389 | |||
43bf99e659 | |||
3bb3fd3531 | |||
2a1cb7c00d | |||
74467320cb | |||
86b12b16bf | |||
a5d509c541 | |||
249635f0cc | |||
20d8d255cb | |||
57762ab73c | |||
656d375af0 | |||
f5035a4169 | |||
5f2d3056f1 | |||
fa41baa8b6 | |||
fe15af4c28 | |||
986a9449cf | |||
de7e5016dd | |||
8bbd5a1184 | |||
212f1feeb6 | |||
4ae88b2e47 | |||
43fb35381f | |||
023c744a8e | |||
da419361cb | |||
42087923e0 | |||
906cedd168 | |||
f659be446d | |||
63c4cfa651 | |||
adf27351a3 | |||
c1d4002551 | |||
1c56c34211 | |||
d8aa5b5ff4 | |||
a5618681df | |||
a84fc847db | |||
f8e7c3c7c6 | |||
f9bf418bf8 | |||
7950085fba | |||
9e0860b9a6 | |||
c3427d110a | |||
0fde680a11 | |||
ef248e3944 | |||
c940e1ad16 | |||
aa700f7670 | |||
f30da163d8 | |||
1f63454f8d | |||
8e2794f6ab | |||
6468b356a6 | |||
c6777e43ed | |||
c68eaa613c | |||
00dde5c2b4 | |||
ad19e33638 | |||
4117781cd1 | |||
11d169ad23 | |||
91827aed3e | |||
fe339d9c0f | |||
e818029b48 | |||
4f5ad07ace | |||
3ece5a0390 | |||
078dbde45a | |||
8953fdf1d8 | |||
bccee0c94d | |||
7447288e5c | |||
7ab2289c99 | |||
1b07199383 | |||
3c7cd01dd5 | |||
4cfb275186 | |||
0a9f4ffc4d | |||
e18a44f723 | |||
62c2be255d | |||
3246d6b6df | |||
5f670cd104 | |||
6722bd7bea | |||
56acc4f888 | |||
798eb67296 | |||
2fe45ec898 | |||
4d0969fbc3 | |||
c4711fc328 | |||
938c7bdb93 | |||
634d5fe5c3 | |||
1961b92340 | |||
ef234a270f | |||
66d2a2724e | |||
2729b409e6 | |||
f5d2836199 | |||
065b37ac11 | |||
ca3b2fa1a0 | |||
4ea284a1c5 | |||
4e58bbb13b | |||
3636e55604 | |||
a027b77479 | |||
802f3678f7 | |||
a18e04a9a2 | |||
b12856363e | |||
c1089497b7 | |||
b9665786c8 | |||
746ded9a53 | |||
dc3255adb7 | |||
b6e94ed9ec | |||
6fcf35a7bc | |||
92c163d2fe | |||
b943264639 | |||
02e969162a | |||
b5f370aa5a | |||
b82eee1cc8 | |||
8be8826e86 | |||
ca9905a1ed | |||
f68468c6db | |||
825ea2aa3d | |||
fa40bdaf17 | |||
568042fac0 | |||
f2329884f8 | |||
22c184840c | |||
001df70e26 | |||
7d289d5180 | |||
1bbe0e48c6 | |||
22e7137e74 | |||
22193bdac1 | |||
00215eeedd | |||
d70d1f23d8 | |||
3dd2ef7475 | |||
ca384aeb1a | |||
f2a9544bbc | |||
d21bb11712 | |||
5e04352725 | |||
ac7351cf21 | |||
7e4b38a2f1 | |||
b0eff2a9d3 | |||
e02fa7bfd6 | |||
a35dedd7bb | |||
094704ccd9 | |||
76f9e3e856 | |||
518872da0d | |||
5db5c6424a | |||
9c9fcde8b1 | |||
2439cbe095 | |||
1c8e95f7e4 | |||
ab5779622a | |||
fd3734192c | |||
74487b5307 | |||
d1198049bf | |||
0d4ce34730 | |||
47e1a81044 | |||
505313c0d0 | |||
f9879ce9af | |||
fd99b10fc4 | |||
0b91fd69d6 | |||
e05473ee8c | |||
b84538f670 | |||
fd988eb63f | |||
3689d75bde | |||
ebe6be201a | |||
4778e1ce6f | |||
e188524a93 | |||
676f5e121a | |||
d3189fda58 | |||
7ce447efe4 | |||
d8b239892e | |||
896760903a | |||
11b7309301 | |||
16061a0b8d | |||
fc49eaae18 | |||
2f1e2acc69 | |||
0f6675942d | |||
a8fbca6625 | |||
2420a41bb7 | |||
47ad4f0620 | |||
5ee323ee26 | |||
e64ba7e0f2 | |||
43c4303b68 | |||
83f56e0621 | |||
067d8ff025 | |||
0f3e29bb26 | |||
870a66cc5b | |||
59ac40b09d | |||
67b21e8705 | |||
af3b1e8359 | |||
2062d667e8 | |||
b164a2f7ac | |||
321b040fe7 | |||
96cbdfdaca | |||
e66c30b855 | |||
7c78b0f443 | |||
f832fdfb0c | |||
0f6756f2f1 | |||
82621e34a8 | |||
94abacbf61 | |||
45466741f1 | |||
f38ec6605d | |||
baa0a21b38 | |||
cf216dfbbf | |||
8cef83a90b | |||
41ce9cea7c | |||
688aa856ab | |||
7924082b70 |
.env.example.eslintignore.gitignore
.github
ISSUE_TEMPLATE
images
resources
workflows
.husky
CONTRIBUTING.mdLICENSEMakefileREADME.mdbackend
.eslintrc.prettierrcDockerfileenvironment.d.tshealthcheck.jspackage-lock.jsonpackage.json
src
config
controllers
authController.tsbotController.tsindex.tsintegrationAuthController.tsintegrationController.tskeyController.tsmembershipController.tsmembershipOrgController.tsorganizationController.tspasswordController.tssecretController.ts
events
helpers
bot.tsevent.tsintegration.tsintegrationAuth.tsmembership.tsnodemailer.tsrateLimiter.tssignup.tsworkspace.ts
index.tsintegrations
json
middleware
index.tsrequireBotAuth.tsrequireIntegrationAuth.tsrequireIntegrationAuthorizationAuth.tsrequireWorkspaceAuth.ts
models
routes
services
templates
types/express
utils
variables.tsvariables
cli
docker-compose.dev.ymldocker-compose.prod.ymldocker-compose.ymldocs
CLI.mdxHeroku.mdxmint.json
cli
contributing
developing.mdxenvars.mdxgetting-started
gettingStarted.mdximages
architecture-diagram.pngarchitecture-diagram2.pngdashboard-name-modal-organization.pngdashboard-name-selected.pngdashboard.pnglanding-page.pngorganization-ic-add.pngorganization-ic.pngorganization-members-add.pngorganization-members.pngorganization.pngproject-download.pngproject-drag-drop.pngproject-envar-toggle-moved.pngproject-envar-toggle-open.pngproject-envar-toggle.pngproject-environment.pngproject-hide.pngproject-integrations.pngproject-quickstart.pngproject-search-typed.pngproject-search.pngproject-sort.pngproject-token-add.pngproject-token-added.pngproject-token-name.pngproject-unhide.pngsignup-box.pngsignup-complete-account.pngsignup-otp.png
infisicalToken.mdxinstallCLI.mdxintegrations
cicd
cloud
frameworks
django.mdxexpress.mdxfiber.mdxflask.mdxgatsby.mdxlaravel.mdxnestjs.mdxnextjs.mdxnuxt.mdxrails.mdxreact.mdxremix.mdxvite.mdxvue.mdx
overview.mdxplatforms
security
self-hosting
self_host_overview.mdxfrontend
.eslintrc.prettierrcDockerfileDockerfile.devDockerfile.prodREADME.md
components
RouteGuard.jsaes-256-gcm.js
const.jsnext-env.d.tsnext.config.jspackage-lock.jsonpackage.jsonanalytics
basic
Error.jsError.tsxInputField.jsInputField.tsxLayout.tsxListbox.jsListbox.tsx
buttons
dialog
ActivateBotDialog.jsAddIncidentContactDialog.jsAddProjectMemberDialog.jsAddServiceTokenDialog.jsAddUserDialog.jsAddWorkspaceDialog.jsDeleteUserDialog.jsIntegrationAccessTokenDialog.js
layout.jspopups
table
billing
context/Notifications
dashboard
integrations
CloudIntegration.tsxCloudIntegrationSection.tsxFrameworkIntegration.tsxFrameworkIntegrationSection.tsxIntegration.tsxIntegrationSection.tsx
navigation
utilities
pages
_app.jsdashboard.js
postcss.config.jsapi
auth
ChangePassword2.jsChangePassword2.tsCheckAuth.jsCheckAuth.tsCheckEmailVerificationCode.jsCheckEmailVerificationCode.tsCompleteAccountInformationSignup.jsCompleteAccountInformationSignup.tsCompleteAccountInformationSignupInvite.jsCompleteAccountInformationSignupInvite.tsEmailVerifyOnPasswordReset.tsIssueBackupPrivateKey.jsIssueBackupPrivateKey.tsLogin1.jsLogin1.tsLogin2.jsLogin2.tsLogout.jsLogout.tsSRP1.jsSRP1.tsSendEmailOnPasswordReset.tsSendVerificationEmail.jsSendVerificationEmail.tsToken.jsToken.tsVerifySignupInvite.jsVerifySignupInvite.tsgetBackupEncryptedPrivateKey.tspublicKeyInfisical.jspublicKeyInfisical.tsresetPasswordOnAccountRecovery.ts
bot
files
integrations
ChangeHerokuConfigVars.jsChangeHerokuConfigVars.tsDeleteIntegration.jsDeleteIntegration.tsDeleteIntegrationAuth.jsDeleteIntegrationAuth.tsGetIntegrationApps.jsGetIntegrationApps.tsGetIntegrationOptions.tsGetIntegrations.jsStartIntegration.jsStartIntegration.tsauthorizeIntegration.jsauthorizeIntegration.tsgetWorkspaceAuthorizations.jsgetWorkspaceAuthorizations.tsgetWorkspaceIntegrations.jsgetWorkspaceIntegrations.tsupdateIntegration.ts
organization
GetOrg.jsGetOrg.tsGetOrgProjects.jsGetOrgProjects.tsGetOrgSubscription.jsGetOrgSubscription.tsGetOrgUserProjects.jsGetOrgUserProjects.tsGetOrgUsers.jsGetOrgUsers.tsStripeRedirect.jsStripeRedirect.tsaddIncidentContact.jsaddIncidentContact.tsaddUserToOrg.jsaddUserToOrg.tsdeleteIncidentContact.jsdeleteIncidentContact.tsdeleteUserFromOrganization.jsdeleteUserFromOrganization.tsgetIncidentContacts.jsgetIncidentContacts.tsgetOrgs.jsgetOrgs.tsrenameOrg.jsrenameOrg.ts
serviceToken
user
userActions
workspace
addUserToWorkspace.jsaddUserToWorkspace.tschangeUserRoleInWorkspace.jschangeUserRoleInWorkspace.tscreateWorkspace.jscreateWorkspace.tsdeleteUserFromWorkspace.jsdeleteUserFromWorkspace.tsdeleteWorkspace.jsdeleteWorkspace.tsgetLatestFileKey.jsgetLatestFileKey.tsgetProjectInfo.tsgetWorkspaceInfo.jsgetWorkspaceKeys.jsgetWorkspaceKeys.tsgetWorkspaceUsers.jsgetWorkspaceUsers.tsgetWorkspaces.jsgetWorkspaces.tsrenameWorkspace.jsrenameWorkspace.tsuploadKeys.jsuploadKeys.ts
dashboard
email-not-verified.jsheroku.jshome
index.jsintegrations
login.jslogin.tsxnetlify.jsnoprojects.jspassword-reset.tsxrequestnewinvite.jssettings
signup.jssignup.tsxsignupinvite.jsusers
vercel.jsverify-email.tsxpublic
data
images
blog1.pngblog3.png
integrations
Django.pngDocker Compose.pngDocker.pngExpress.pngFiber.pngFlask.pngGatsby.pngLaravel.pngNestJS.pngNext.js.pngNuxt.pngRails.pngReact.pngRemix.pngVercel.pngVite.pngVue.png
progress-0.svgprogress-14.svgprogress-28.svgprogress-43.svgprogress-57.svgprogress-71.svgjson
scripts
tailwind.config.jstsconfig.jsonyarn.lockhelm-charts
README.md
infisical
secrets-operator
.helmignoreChart.yaml
upload-to-cloudsmith.shtemplates
_helpers.tpldeployment.yamlinfisicalsecret-crd.yamlleader-election-rbac.yamlmanager-rbac.yamlmetrics-reader-rbac.yamlmetrics-service.yamlproxy-rbac.yaml
values.yamlimg
k8-operator
.dockerignore.gitignoreDockerfileMakefilePROJECTREADME.md
api/v1alpha1
config
crd
default
manager
prometheus
rbac
auth_proxy_client_clusterrole.yamlauth_proxy_role.yamlauth_proxy_role_binding.yamlauth_proxy_service.yamlinfisicalsecret_editor_role.yamlinfisicalsecret_viewer_role.yamlkustomization.yamlleader_election_role.yamlleader_election_role_binding.yamlrole.yamlrole_binding.yamlservice_account.yaml
samples
controllers
go.modgo.sumhack
kubectl-install
main.gopackages
nginx
package-lock.jsonpackage.json
20
.env.example
20
.env.example
@ -27,32 +27,32 @@ EMAIL_TOKEN_LIFETIME=
|
||||
# Required
|
||||
MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
|
||||
|
||||
# Optional credentials for MongoDB container instance
|
||||
# Optional credentials for MongoDB container instance and Mongo-Express
|
||||
MONGO_USERNAME=root
|
||||
MONGO_PASSWORD=example
|
||||
|
||||
# Mongo-Express vars (needed for development only)
|
||||
ME_CONFIG_MONGODB_ADMINUSERNAME=root
|
||||
ME_CONFIG_MONGODB_ADMINPASSWORD=example
|
||||
ME_CONFIG_MONGODB_URL=mongodb://root:example@mongo:27017/
|
||||
|
||||
# Website URL
|
||||
# Required
|
||||
NODE_ENV=development
|
||||
NEXT_PUBLIC_WEBSITE_URL=http://localhost:8080
|
||||
|
||||
SITE_URL=http://localhost:8080
|
||||
|
||||
# Mail/SMTP
|
||||
# Required to send emails
|
||||
# By default, SMTP_HOST is set to smtp.gmail.com
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_NAME=Team
|
||||
SMTP_USERNAME=team@infisical.com
|
||||
SMTP_PASSWORD=
|
||||
|
||||
# Integration
|
||||
# Optional only if integration is used
|
||||
OAUTH_CLIENT_SECRET_HEROKU=
|
||||
OAUTH_TOKEN_URL_HEROKU=
|
||||
CLIENT_ID_HEROKU=
|
||||
CLIENT_ID_VERCEL=
|
||||
CLIENT_ID_NETLIFY=
|
||||
CLIENT_SECRET_HEROKU=
|
||||
CLIENT_SECRET_VERCEL=
|
||||
CLIENT_SECRET_NETLIFY=
|
||||
|
||||
# Sentry (optional) for monitoring errors
|
||||
SENTRY_DSN=
|
||||
|
3
.eslintignore
Normal file
3
.eslintignore
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
built
|
||||
healthcheck.js
|
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Describe the bug
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
### To Reproduce
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
### Expected behavior
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
### Screenshots
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
### Platform you are having the issue on:
|
||||
|
||||
### Additional context
|
||||
Add any other context about the problem here.
|
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Let us now what feature you would want to have in Infisical
|
||||
title: ''
|
||||
labels: 'feature request'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Feature description
|
||||
A clear and concise description of what the the feature should be.
|
||||
|
||||
### Why would it be useful?
|
||||
Why would this feature be useful for Infisical users?
|
||||
|
||||
### Additional context
|
||||
Add any other context about the problem here.
|
BIN
.github/images/star-infisical.gif
vendored
Normal file
BIN
.github/images/star-infisical.gif
vendored
Normal file
Binary file not shown.
After ![]() (image error) Size: 106 KiB |
30
.github/resources/docker-compose.be-test.yml
vendored
Normal file
30
.github/resources/docker-compose.be-test.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
backend:
|
||||
container_name: infisical-backend-test
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- mongo
|
||||
image: infisical/backend:test
|
||||
command: npm run start
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- MONGO_URL=mongodb://test:example@mongo:27017/?authSource=admin
|
||||
- MONGO_USERNAME=test
|
||||
- MONGO_PASSWORD=example
|
||||
networks:
|
||||
- infisical-test
|
||||
|
||||
mongo:
|
||||
container_name: infisical-mongo-test
|
||||
image: mongo
|
||||
restart: always
|
||||
environment:
|
||||
- MONGO_INITDB_ROOT_USERNAME=test
|
||||
- MONGO_INITDB_ROOT_PASSWORD=example
|
||||
networks:
|
||||
- infisical-test
|
||||
|
||||
networks:
|
||||
infisical-test:
|
26
.github/resources/healthcheck.sh
vendored
Executable file
26
.github/resources/healthcheck.sh
vendored
Executable file
@ -0,0 +1,26 @@
|
||||
# Name of the target container to check
|
||||
container_name="$1"
|
||||
# Timeout in seconds. Default: 60
|
||||
timeout=$((${2:-60}));
|
||||
|
||||
if [ -z $container_name ]; then
|
||||
echo "No container name specified";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
echo "Container: $container_name";
|
||||
echo "Timeout: $timeout sec";
|
||||
|
||||
try=0;
|
||||
is_healthy="false";
|
||||
while [ $is_healthy != "true" ];
|
||||
do
|
||||
try=$(($try + 1));
|
||||
printf "■";
|
||||
is_healthy=$(docker inspect --format='{{json .State.Health}}' $container_name | jq '.Status == "healthy"');
|
||||
sleep 1;
|
||||
if [[ $try -eq $timeout ]]; then
|
||||
echo " Container was not ready within timeout";
|
||||
exit 1;
|
||||
fi
|
||||
done
|
41
.github/workflows/check-be-pull-request.yml
vendored
Normal file
41
.github/workflows/check-be-pull-request.yml
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
name: Check Backend Pull Request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ opened, synchronize ]
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- '!backend/README.md'
|
||||
- '!backend/.*'
|
||||
- 'backend/.eslintrc.js'
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
check-be-pr:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
-
|
||||
name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: 🔧 Setup Node 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: backend/package-lock.json
|
||||
-
|
||||
name: 📦 Install dependencies
|
||||
run: npm ci --only-production --ignore-scripts
|
||||
working-directory: backend
|
||||
# -
|
||||
# name: 🧪 Run tests
|
||||
# run: npm run test:ci
|
||||
# working-directory: backend
|
||||
-
|
||||
name: 🏗️ Run build
|
||||
run: npm run build
|
||||
working-directory: backend
|
41
.github/workflows/check-fe-pull-request.yml
vendored
Normal file
41
.github/workflows/check-fe-pull-request.yml
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
name: Check Frontend Pull Request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ opened, synchronize ]
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
- '!frontend/README.md'
|
||||
- '!frontend/.*'
|
||||
- 'frontend/.eslintrc.js'
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
check-fe-pr:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
-
|
||||
name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: 🔧 Setup Node 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
-
|
||||
name: 📦 Install dependencies
|
||||
run: npm ci --only-production --ignore-scripts
|
||||
working-directory: frontend
|
||||
# -
|
||||
# name: 🧪 Run tests
|
||||
# run: npm run test:ci
|
||||
# working-directory: frontend
|
||||
-
|
||||
name: 🏗️ Run build
|
||||
run: npm run build
|
||||
working-directory: frontend
|
22
.github/workflows/close_inactive_issues.yml
vendored
Normal file
22
.github/workflows/close_inactive_issues.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
name: Close inactive issues
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v4
|
||||
with:
|
||||
days-before-issue-stale: 30
|
||||
days-before-issue-close: 14
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
86
.github/workflows/docker-image.yml
vendored
Normal file
86
.github/workflows/docker-image.yml
vendored
Normal file
@ -0,0 +1,86 @@
|
||||
name: Push to Docker Hub
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
backend-image:
|
||||
name: Build backend image
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- name: 🔧 Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 📦 Build backend and export to Docker
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
load: true
|
||||
context: backend
|
||||
tags: infisical/backend:test
|
||||
- name: ⏻ Spawn backend container and dependencies
|
||||
run: |
|
||||
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
|
||||
- name: 🧪 Test backend image
|
||||
run: |
|
||||
./.github/resources/healthcheck.sh infisical-backend-test
|
||||
- name: ⏻ Shut down backend container and dependencies
|
||||
run: |
|
||||
docker compose -f .github/resources/docker-compose.be-test.yml down
|
||||
- name: 🏗️ Build backend and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
context: backend
|
||||
tags: infisical/backend:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
frontend-image:
|
||||
name: Build frontend image
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- name: 🔧 Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 📦 Build frontend and export to Docker
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
load: true
|
||||
context: frontend
|
||||
tags: infisical/frontend:test
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
- name: ⏻ Spawn frontend container
|
||||
run: |
|
||||
docker run -d --rm --name infisical-frontend-test infisical/frontend:test
|
||||
- name: 🧪 Test frontend image
|
||||
run: |
|
||||
./.github/resources/healthcheck.sh infisical-frontend-test
|
||||
- name: ⏻ Shut down frontend container
|
||||
run: |
|
||||
docker stop infisical-frontend-test
|
||||
- name: 🏗️ Build frontend and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
context: frontend
|
||||
tags: infisical/frontend:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
22
.github/workflows/helm_chart_release.yml
vendored
Normal file
22
.github/workflows/helm_chart_release.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
name: Release Helm Charts
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: v3.10.0
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v4
|
||||
- name: Install Cloudsmith CLI
|
||||
run: pip install --upgrade cloudsmith-cli
|
||||
- name: Build and push helm package to Cloudsmith
|
||||
run: cd helm-charts && sh upload-to-cloudsmith.sh
|
||||
env:
|
||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
4
.github/workflows/release_build.yml
vendored
4
.github/workflows/release_build.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: goreleaser
|
||||
name: Go releaser
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -35,7 +35,7 @@ jobs:
|
||||
- uses: actions/setup-python@v4
|
||||
- run: pip install --upgrade cloudsmith-cli
|
||||
- name: Publish to CloudSmith
|
||||
run: sh cli/upload_to_cloudfront.sh
|
||||
run: sh cli/upload_to_cloudsmith.sh
|
||||
env:
|
||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||
|
||||
|
29
.github/workflows/release_docker_k8_operator.yaml
vendored
Normal file
29
.github/workflows/release_docker_k8_operator.yaml
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
name: Release Docker image for K8 operator
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: 🔧 Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: k8-operator
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: infisical/kubernetes-operator:latest
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -49,3 +49,6 @@ yarn-error.log*
|
||||
.env.production.local
|
||||
.vercel
|
||||
.env.infisical
|
||||
|
||||
# Infisical init
|
||||
.infisical.json
|
||||
|
5
.husky/pre-commit
Executable file
5
.husky/pre-commit
Executable file
@ -0,0 +1,5 @@
|
||||
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
@ -1,5 +1,7 @@
|
||||
# Contributing to Infisical
|
||||
|
||||
Thanks for taking the time to contribute!
|
||||
Thanks for taking the time to contribute! 😃 🚀
|
||||
|
||||
Please refer to our Contributing Guide for instructions on how to contribute.
|
||||
Please refer to our [Contributing Guide](https://infisical.com/docs/contributing/overview) for instructions on how to contribute.
|
||||
|
||||
We also have some 🔥amazing🔥 merch for our contributors. Please reach out to tony@infisical.com for more info 👀
|
||||
|
25
LICENSE
Normal file
25
LICENSE
Normal file
@ -0,0 +1,25 @@
|
||||
Copyright (c) 2022 Infisical Inc.
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
- All content that resides under any "ee/" directory of this repository, if such directories exists, are licensed under the license defined in "ee/LICENSE".
|
||||
- All third party components incorporated into the Infisical Software are licensed under the original license provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
10
Makefile
10
Makefile
@ -1,14 +1,14 @@
|
||||
build:
|
||||
docker-compose -f docker-compose.yml -f docker-compose.prod.yml build
|
||||
docker-compose -f docker-compose.yml build
|
||||
|
||||
push:
|
||||
docker-compose -f docker-compose.yml -f docker-compose.prod.yml push
|
||||
docker-compose -f docker-compose.yml push
|
||||
|
||||
up-dev:
|
||||
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up --build
|
||||
docker-compose -f docker-compose.dev.yml up --build
|
||||
|
||||
up-prod:
|
||||
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up --build
|
||||
docker-compose -f docker-compose.yml up --build
|
||||
|
||||
down:
|
||||
docker-compose down
|
||||
docker-compose down
|
||||
|
314
README.md
314
README.md
@ -1,15 +1,16 @@
|
||||
<h1 align="center">
|
||||
<img width="300" src="/img/logoname-black.svg#gh-light-mode-only" alt="ifnisical">
|
||||
<img width="300" src="/img/logoname-black.svg#gh-light-mode-only" alt="infisical">
|
||||
<img width="300" src="/img/logoname-white.svg#gh-dark-mode-only" alt="infisical">
|
||||
</h1>
|
||||
<p align="center">
|
||||
<p align="center">Open-source, end-to-end encrypted, 1-line-of-code tool to sync environment variables across you team and infrastructure.</p>
|
||||
<p align="center">Open-source, E2EE, simple tool to manage and sync environment variables across your team and infrastructure.</p>
|
||||
</p>
|
||||
|
||||
<h4 align="center">
|
||||
<a href="https://infisical.com/signup">Get Started - we host (Infisical Cloud)</a> |
|
||||
<a href="https://infisical.com/docs/self_host_overview">Get Started - you host</a> |
|
||||
<a href="https://infisical.com/docs/gettingStarted">Docs</a> |
|
||||
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">Slack</a> |
|
||||
<a href="https://infisical.com/">Infisical Cloud</a> |
|
||||
<a href="https://infisical.com/docs/self-hosting/overview">Self-Hosting</a> |
|
||||
<a href="https://infisical.com/docs/getting-started/introduction">Docs</a> |
|
||||
<a href="https://www.infisical.com">Website</a>
|
||||
</h4>
|
||||
|
||||
@ -20,91 +21,300 @@
|
||||
<a href="https://github.com/infisical/infisical/blob/main/CONTRIBUTING.md">
|
||||
<img src="https://img.shields.io/badge/PRs-Welcome-brightgreen" alt="PRs welcome!" />
|
||||
</a>
|
||||
<a href="https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA">
|
||||
<a href="">
|
||||
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
|
||||
</a>
|
||||
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">
|
||||
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
|
||||
</a>
|
||||
<a href="https://twitter.com/infisical">
|
||||
<img src="https://img.shields.io/twitter/follow/infisical?label=Follow" alt="Infisical Twitter" />
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
<img src="/img/infisical_github_repo.png" width="100%" alt="Dashboard" />
|
||||
|
||||
**[Infisical](https://infisical.com)** is an open source tool to help teams manage and sync environment variables across their development workflow and infrastructure. It's designed to be simple and end-to-end encrypted. You can start with just 1 line of code within 10 minutes.
|
||||
**[Infisical](https://infisical.com)** is an open source, E2EE tool to help teams manage and sync environment variables across their development workflow and infrastructure. It's designed to be simple and take minutes to get going.
|
||||
|
||||
- **User-Friendly Dashboard** to manage your organization's environment variables within projects
|
||||
- **[Language-Agnostic CLI](https://infisical.com/docs/CLI)** that pulls and injects environment variables into your local workflow
|
||||
- **[Complete control over your data](https://infisical.com/docs/self_host_overview)** - host it yourself on any infrastructure
|
||||
- **[User-Friendly Dashboard](https://infisical.com/docs/getting-started/dashboard/project)** to manage your team's environment variables within projects
|
||||
- **[Language-Agnostic CLI](https://infisical.com/docs/cli/overview)** that pulls and injects environment variables into your local workflow
|
||||
- **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure
|
||||
- **Navigate Multiple Environments** per project (e.g. development, staging, production, etc.)
|
||||
- **Personal/Shared** scoping for environment variables
|
||||
- **[Integrations](https://infisical.com/docs/Heroku)** with CI/CD and production infrastructure (Heroku available, more coming soon)
|
||||
- **[1-Click Deploy](https://infisical.com/docs/linux)** to Digital Ocean (other providers coming soon)
|
||||
- 🔜 **Authentication/Authorization** for projects (read/write controls coming soon)
|
||||
- 🔜 **Automatic Secret Rotation** (coming soon)
|
||||
- 🔜 **2FA** (coming soon)
|
||||
- 🔜 **Access Logs** (coming soon)
|
||||
- 🔜 **Slack Integration & MS Teams** integrations (coming soon)
|
||||
- **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure (Heroku available, more coming soon)
|
||||
- 🔜 **1-Click Deploy** to Digital Ocean and Heroku
|
||||
- 🔜 **Authentication/Authorization** for projects (read/write controls soon)
|
||||
- 🔜 **Automatic Secret Rotation**
|
||||
- 🔜 **2FA**
|
||||
- 🔜 **Access Logs**
|
||||
- 🔜 **Slack Integration & MS Teams** integrations
|
||||
|
||||
And more.
|
||||
|
||||
## What's cool about this?
|
||||
## 🚀 Get started
|
||||
|
||||
Infisical is the first open-source end-to-end encrypted secret manager that takes less than 10 minutes to setup.
|
||||
To quickly get started, visit our [get started guide](https://infisical.com/docs/getting-started/introduction).
|
||||
|
||||
Yes. There are other secret managers out there. Some of them are incredibly complicated - they were built for security teams, not developers. The other ones are not end-to-end encrypted, and because of that they can read your secrets. If you care about efficiency and security at the same time - Infisical is right for you.
|
||||
<p>
|
||||
<a href="https://infisical.com/docs/self-hosting/overview" target="_blank"><img src="https://user-images.githubusercontent.com/78047717/206356882-2b773eed-b0da-4725-ae2f-83e3cd7f2713.png" height=120 /> </a>
|
||||
<a href="https://www.youtube.com/watch?v=JS3OKYU2078" target="_blank"><img src="https://user-images.githubusercontent.com/78047717/206356600-8833b128-6cae-408c-a703-07b2fc6aff4b.png" height=120 /> </a>
|
||||
<a href="https://app.infisical.com/signup" target="_blank"><img src="https://user-images.githubusercontent.com/78047717/206355970-f4c09062-b88f-452a-94e0-9c61a0651170.png" height=120></a>
|
||||
</p>
|
||||
|
||||
On top of that, Infisical is one of the few open source solutions. Need any integrations or want a new feature? You can [create an issue for us](https://github.com/Infisical/infisical/issues) or contribute directly! This is the power of open-source. :)
|
||||
## 🔥 What's cool about this?
|
||||
|
||||
## Contributing
|
||||
Infisical makes secret management simple and end-to-end encrypted by default. We're on a mission to make it more accessible to all developers, <i>not just security teams</i>.
|
||||
|
||||
For full documentation, visit [infisical.com/docs](https://infisical.com/docs).
|
||||
According to a [report](https://www.ekransystem.com/en/blog/secrets-management) in 2019, only 10% of organizations use secret management solutions despite all using digital secrets to some extent.
|
||||
|
||||
Whether it's big or small, we ❤️ contributions. Check out our guide to see how to [get started](./DEVELOPERS.md).
|
||||
If you care about efficiency and security, then Infisical is right for you.
|
||||
|
||||
Not sure where to get started? [Book a free, non-pressure pairing sessions with one of our teammates](mailto:tony@infisical.com?subject=Pairing%20session&body=I'd%20like%20to%20do%20a%20pairing%20session!)!
|
||||
We are currently working hard to make Infisical more extensive. Need any integrations or want a new feature? Feel free to [create an issue](https://github.com/Infisical/infisical/issues) or [contribute](https://infisical.com/docs/contributing/overview) directly to the repository.
|
||||
|
||||
## Community & Support
|
||||
## 🌱 Contributing
|
||||
|
||||
- [GitHub Discussions](https://github.com/Infisical/infisical/discussions) for help with building and discussion.
|
||||
- [GitHub Issues](https://github.com/Infisical/infisical-cli/issues) for any bugs and errors you encounter using Infisical.
|
||||
- [Community Slack](https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA) for hanging out with the community and quick communication with the team.
|
||||
Whether it's big or small, we love contributions ❤️ Check out our guide to see how to [get started](https://infisical.com/docs/contributing/overview).
|
||||
|
||||
## Status
|
||||
Not sure where to get started? You can:
|
||||
|
||||
- [Book a free, non-pressure pairing sessions with one of our teammates](mailto:tony@infisical.com?subject=Pairing%20session&body=I'd%20like%20to%20do%20a%20pairing%20session!)!
|
||||
- Join our <a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">Slack</a>, and ask us any questions there.
|
||||
|
||||
## 💚 Community & Support
|
||||
|
||||
- [Slack](https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g) (For live discussion with the community and the Infisical team)
|
||||
- [GitHub Discussions](https://github.com/Infisical/infisical/discussions) (For help with building and deeper conversations about features)
|
||||
- [GitHub Issues](https://github.com/Infisical/infisical-cli/issues) (For any bugs and errors you encounter using Infisical)
|
||||
- [Twitter](https://twitter.com/infisical) (Get news fast)
|
||||
|
||||
## 🐥 Status
|
||||
|
||||
- [x] Public Alpha: Anyone can sign up over at [infisical.com](https://infisical.com) but go easy on us, there are kinks and we're just getting started.
|
||||
- [ ] Public Beta: Stable enough for most non-enterprise use-cases.
|
||||
- [ ] Public: Production-ready.
|
||||
|
||||
## Integrations
|
||||
|
||||
We're currently setting the foundation and building integrations so secrets can be synced everywhere. Any help is welcome! :)
|
||||
|
||||
- [x] Docker
|
||||
- [x] Docker Compose
|
||||
- [x] Heroku
|
||||
- [ ] Vercel
|
||||
- [ ] Kubernetes
|
||||
- [ ] AWS
|
||||
- [ ] GCP
|
||||
- [ ] Azure
|
||||
- [ ] Digital Ocean
|
||||
- [ ] GitLab
|
||||
- [ ] CircleCI
|
||||
|
||||
|
||||
We're currently in Public Alpha.
|
||||
|
||||
## Open-source vs. paid
|
||||
## 🔌 Integrations
|
||||
|
||||
This repo is entirely MIT licensed, with the exception of the `ee` directory which will contain premium enterprise features requring a Infisical license in the future. We're currently focused on developing non-enterprise offerings first that should suit most use-cases.
|
||||
We're currently setting the foundation and building [integrations](https://infisical.com/docs/integrations/overview) so secrets can be synced everywhere. Any help is welcome! :)
|
||||
|
||||
## Security
|
||||
<table>
|
||||
<tr>
|
||||
<th>Platforms </th>
|
||||
<th>Frameworks</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/platforms/docker?ref=github.com">
|
||||
✔️ Docker
|
||||
</a>
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/platforms/docker-compose?ref=github.com">
|
||||
✔️ Docker Compose
|
||||
</a>
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/cloud/heroku?ref=github.com">
|
||||
✔️ Heroku
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/cloud/vercel?ref=github.com">
|
||||
✔️ Vercel
|
||||
</a>
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/platforms/kubernetes?ref=github.com">
|
||||
✔️ Kubernetes
|
||||
</a>
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
🔜 Fly.io
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" valign="middle">
|
||||
🔜 AWS
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
🔜 GitHub Actions (https://github.com/Infisical/infisical/issues/54)
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
🔜 Railway
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" valign="middle">
|
||||
🔜 GCP
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
🔜 GitLab CI/CD
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
🔜 CircleCI
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" valign="middle">
|
||||
🔜 Jenkins
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
🔜 Digital Ocean
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
🔜 Azure
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" valign="middle">
|
||||
🔜 TravisCI
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
🔜 Netlify (https://github.com/Infisical/infisical/issues/55)
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
🔜 Railway
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" valign="middle">
|
||||
🔜 Bitbucket
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
🔜 Supabase
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
🔜 Serverless
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/frameworks/react?ref=github.com">
|
||||
✔️ React
|
||||
</a>
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/frameworks/express?ref=github.com">
|
||||
✔️ Express
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/frameworks/gatsby?ref=github.com">
|
||||
✔️ Gatsby
|
||||
</a>
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/frameworks/flask?ref=github.com">
|
||||
✔️ Flask
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/frameworks/django?ref=github.com">
|
||||
✔️ Django
|
||||
</a>
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/frameworks/laravel?ref=github.com">
|
||||
✔️ Laravel
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/frameworks/nestjs?ref=github.com">
|
||||
✔️ NestJS
|
||||
</a>
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/frameworks/remix?ref=github.com">
|
||||
✔️ Remix
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/frameworks/nextjs?ref=github.com">
|
||||
✔️ Next.js
|
||||
</a>
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/frameworks/vite?ref=github.com">
|
||||
✔️ Vite
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/frameworks/rails?ref=github.com">
|
||||
✔️ Ruby on Rails
|
||||
</a>
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/frameworks/vue?ref=github.com">
|
||||
✔️ Vue
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/frameworks/fiber?ref=github.com">
|
||||
✔️ Fiber
|
||||
</a>
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/frameworks/nuxt?ref=github.com">
|
||||
✔️ Nuxt
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 🏘 Open-source vs. paid
|
||||
|
||||
This repo is entirely MIT licensed, with the exception of the `ee` directory which will contain premium enterprise features requiring a Infisical license in the future. We're currently focused on developing non-enterprise offerings first that should suit most use-cases.
|
||||
|
||||
## 🛡 Security
|
||||
|
||||
Looking to report a security vulnerability? Please don't post about it in GitHub issue. Instead, refer to our [SECURITY.md](./SECURITY.md) file.
|
||||
|
||||
## 🚨 Stay Up-to-Date
|
||||
|
||||
## Contributors 🦸
|
||||
Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of new features are coming very quickly. Watch **releases** of this repository to be notified about future updates:
|
||||
|
||||

|
||||
|
||||
## 🦸 Contributors
|
||||
|
||||
[//]: contributor-faces
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/vlad-matsiiako"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a>
|
||||
|
||||
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
|
||||
|
@ -1,18 +1,12 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"prettier"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"no-console": 2,
|
||||
"prettier/prettier": 2
|
||||
}
|
||||
}
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"no-console": 2
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "none",
|
||||
"singleQuote": true,
|
||||
"printWidth": 80,
|
||||
"useTabs": true
|
||||
}
|
@ -2,11 +2,14 @@ FROM node:16-bullseye-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json .
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
RUN npm install
|
||||
RUN npm ci --only-production --ignore-scripts
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
||||
CMD node healthcheck.js
|
||||
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
|
10
backend/environment.d.ts
vendored
10
backend/environment.d.ts
vendored
@ -14,13 +14,18 @@ declare global {
|
||||
JWT_SIGNUP_SECRET: string;
|
||||
MONGO_URL: string;
|
||||
NODE_ENV: 'development' | 'staging' | 'testing' | 'production';
|
||||
OAUTH_CLIENT_SECRET_HEROKU: string;
|
||||
OAUTH_TOKEN_URL_HEROKU: string;
|
||||
CLIENT_ID_HEROKU: string;
|
||||
CLIENT_ID_VERCEL: string;
|
||||
CLIENT_ID_NETLIFY: string;
|
||||
CLIENT_SECRET_HEROKU: string;
|
||||
CLIENT_SECRET_VERCEL: string;
|
||||
CLIENT_SECRET_NETLIFY: string;
|
||||
POSTHOG_HOST: string;
|
||||
POSTHOG_PROJECT_API_KEY: string;
|
||||
PRIVATE_KEY: string;
|
||||
PUBLIC_KEY: string;
|
||||
SENTRY_DSN: string;
|
||||
SITE_URL: string;
|
||||
SMTP_HOST: string;
|
||||
SMTP_NAME: string;
|
||||
SMTP_PASSWORD: string;
|
||||
@ -31,7 +36,6 @@ declare global {
|
||||
STRIPE_PUBLISHABLE_KEY: string;
|
||||
STRIPE_SECRET_KEY: string;
|
||||
STRIPE_WEBHOOK_SECRET: string;
|
||||
WEBSITE_URL: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
24
backend/healthcheck.js
Normal file
24
backend/healthcheck.js
Normal file
@ -0,0 +1,24 @@
|
||||
const http = require('http');
|
||||
const PORT = process.env.PORT || 4000;
|
||||
const options = {
|
||||
host: 'localhost',
|
||||
port: PORT,
|
||||
timeout: 2000,
|
||||
path: '/healthcheck'
|
||||
};
|
||||
|
||||
const healthCheck = http.request(options, (res) => {
|
||||
console.log(`HEALTHCHECK STATUS: ${res.statusCode}`);
|
||||
if (res.statusCode == 200) {
|
||||
process.exit(0);
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
healthCheck.on('error', function (err) {
|
||||
console.error(`HEALTH CHECK ERROR: ${err}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
healthCheck.end();
|
608
backend/package-lock.json
generated
608
backend/package-lock.json
generated
@ -9,8 +9,9 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@godaddy/terminus": "^4.11.2",
|
||||
"@sentry/node": "^7.14.0",
|
||||
"@sentry/tracing": "^7.14.0",
|
||||
"@sentry/tracing": "^7.19.0",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"axios": "^1.1.3",
|
||||
"bigint-conversion": "^2.2.2",
|
||||
@ -19,21 +20,21 @@
|
||||
"crypto-js": "^4.1.1",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.1",
|
||||
"express-rate-limit": "^6.5.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.1",
|
||||
"mongoose": "^6.7.2",
|
||||
"nodemailer": "^6.8.0",
|
||||
"posthog-node": "^2.1.0",
|
||||
"query-string": "^7.1.1",
|
||||
"query-string": "^7.1.3",
|
||||
"rimraf": "^3.0.2",
|
||||
"stripe": "^10.7.0",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"typescript": "^4.8.4"
|
||||
"typescript": "^4.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@posthog/plugin-scaffold": "^1.3.4",
|
||||
@ -43,17 +44,15 @@
|
||||
"@types/jsonwebtoken": "^8.5.9",
|
||||
"@types/node": "^18.11.3",
|
||||
"@types/nodemailer": "^6.4.6",
|
||||
"@types/swagger-jsdoc": "^6.0.1",
|
||||
"@types/swagger-ui-express": "^4.1.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"eslint": "^8.26.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"husky": "^8.0.1",
|
||||
"install": "^0.13.0",
|
||||
"jest": "^29.3.1",
|
||||
"nodemon": "^2.0.19",
|
||||
"npm": "^8.19.3",
|
||||
"prettier": "^2.7.1",
|
||||
"ts-node": "^10.9.1"
|
||||
}
|
||||
},
|
||||
@ -2027,6 +2026,14 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@godaddy/terminus": {
|
||||
"version": "4.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@godaddy/terminus/-/terminus-4.11.2.tgz",
|
||||
"integrity": "sha512-e/kbOWpGKME42eltM/wXM3RxSUOrfureZxEd6Dt6NXyFoJ7E8lnmm7znXydJsL3B7ky4HRFZI+eHrep54NZbeQ==",
|
||||
"dependencies": {
|
||||
"stoppable": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz",
|
||||
@ -2592,27 +2599,14 @@
|
||||
"@maxmind/geoip2-node": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "7.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.17.4.tgz",
|
||||
"integrity": "sha512-U3ABSJBKGK8dJ01nEG2+qNOb6Wv7U3VqoajiZxfV4lpPWNFGCoEhiTytxBlFTOCmdUH8209zSZiWJZaDLy+TSA==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.17.4",
|
||||
"@sentry/utils": "7.17.4",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/node": {
|
||||
"version": "7.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.17.4.tgz",
|
||||
"integrity": "sha512-cR+Gsir9c/tzFWxvk4zXkMQy6tNRHEYixHrb88XIjZVYDqDS9l2/bKs5nJusdmaUeLtmPp5Et2o7RJyS7gvKTQ==",
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.19.0.tgz",
|
||||
"integrity": "sha512-yG7Tx32WqOkEHVotFLrumCcT9qlaSDTkFNZ+yLSvZXx74ifsE781DzBA9W7K7bBdYO3op+p2YdsOKzf3nPpAyQ==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.17.4",
|
||||
"@sentry/types": "7.17.4",
|
||||
"@sentry/utils": "7.17.4",
|
||||
"@sentry/core": "7.19.0",
|
||||
"@sentry/types": "7.19.0",
|
||||
"@sentry/utils": "7.19.0",
|
||||
"cookie": "^0.4.1",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"lru_map": "^0.3.3",
|
||||
@ -2622,34 +2616,80 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/tracing": {
|
||||
"version": "7.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.17.4.tgz",
|
||||
"integrity": "sha512-9Fz6DI16ddnd970mlB5MiCNRSmSXp4SVZ1Yv3L22oS3kQeNxjBTE+htYNwJzSPrQp9aL/LqTYwlnrCy24u9XQA==",
|
||||
"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/core": "7.17.4",
|
||||
"@sentry/types": "7.17.4",
|
||||
"@sentry/utils": "7.17.4",
|
||||
"@sentry/types": "7.19.0",
|
||||
"@sentry/utils": "7.19.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/types": {
|
||||
"version": "7.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.17.4.tgz",
|
||||
"integrity": "sha512-QJj8vO4AtxuzQfJIzDnECSmoxwnS+WJsm1Ta2Cwdy+TUCBJyWpW7aIJJGta76zb9gNPGb3UcAbeEjhMJBJeRMQ==",
|
||||
"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/utils": {
|
||||
"version": "7.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.17.4.tgz",
|
||||
"integrity": "sha512-ioG0ANy8uiWzig82/e7cc+6C9UOxkyBzJDi1luoQVDH6P0/PvM8GzVU+1iUVUipf8+OL1Jh09GrWnd5wLm3XNQ==",
|
||||
"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.17.4",
|
||||
"@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==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.19.0",
|
||||
"@sentry/types": "7.19.0",
|
||||
"@sentry/utils": "7.19.0",
|
||||
"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"
|
||||
},
|
||||
"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==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.19.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -2875,12 +2915,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prettier": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz",
|
||||
"integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
|
||||
@ -2915,6 +2949,22 @@
|
||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/swagger-jsdoc": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.1.tgz",
|
||||
"integrity": "sha512-+MUpcbyxD528dECUBCEVm6abNuORdbuGjbrUdHDeAQ+rkPuo2a+L4N02WJHF3bonSSE6SJ3dUJwF2V6+cHnf0w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/swagger-ui-express": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.3.tgz",
|
||||
"integrity": "sha512-jqCjGU/tGEaqIplPy3WyQg+Nrp6y80DCFnDEAvVKWkJyv0VivSSDCChkppHRHAablvInZe6pijDFMnavtN0vqA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/express": "*",
|
||||
"@types/serve-static": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
@ -3951,9 +4001,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/decode-uri-component": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==",
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
@ -4207,39 +4257,6 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-prettier": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz",
|
||||
"integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
|
||||
"integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"prettier-linter-helpers": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=7.28.0",
|
||||
"prettier": ">=2.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint-config-prettier": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
||||
@ -4499,9 +4516,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.6.0.tgz",
|
||||
"integrity": "sha512-HFN2+4ZGdkQOS8Qli4z6knmJFnw6lZed67o6b7RGplWeb1Z0s8VXaj3dUgPIdm9hrhZXTRpCTHXA0/2Eqex0vA==",
|
||||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz",
|
||||
"integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==",
|
||||
"engines": {
|
||||
"node": ">= 12.9.0"
|
||||
},
|
||||
@ -4557,12 +4574,6 @@
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-diff": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
|
||||
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.2.12",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
|
||||
@ -5062,21 +5073,6 @@
|
||||
"node": ">=10.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/husky": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz",
|
||||
"integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"husky": "lib/bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/typicode"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
@ -5836,7 +5832,6 @@
|
||||
"@jest/transform": "^29.3.1",
|
||||
"@jest/types": "^29.3.1",
|
||||
"@types/babel__traverse": "^7.0.6",
|
||||
"@types/prettier": "^2.1.5",
|
||||
"babel-preset-current-node-syntax": "^1.0.0",
|
||||
"chalk": "^4.0.0",
|
||||
"expect": "^29.3.1",
|
||||
@ -6434,9 +6429,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.7.1.tgz",
|
||||
"integrity": "sha512-qbagtqSyvIhUz4EWzXC00EA0DJHFrQwlzTlNGX5DjiESoJiPKqkEga1k9hviFKRFgBna+OlW54mkdi+0+AqxCw==",
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.7.2.tgz",
|
||||
"integrity": "sha512-lrP2V5U1qhaf+z33fiIn7aYAZZ1fVDly+TkFRjTujNBF/FIHESATj2RbgAOSlWqv32fsZXkXejXzeVfjbv35Ow==",
|
||||
"dependencies": {
|
||||
"bson": "^4.7.0",
|
||||
"kareem": "2.4.1",
|
||||
@ -6696,7 +6691,129 @@
|
||||
"treeverse",
|
||||
"validate-npm-package-name",
|
||||
"which",
|
||||
"write-file-atomic"
|
||||
"write-file-atomic",
|
||||
"@colors/colors",
|
||||
"@gar/promisify",
|
||||
"@npmcli/disparity-colors",
|
||||
"@npmcli/git",
|
||||
"@npmcli/installed-package-contents",
|
||||
"@npmcli/metavuln-calculator",
|
||||
"@npmcli/move-file",
|
||||
"@npmcli/name-from-folder",
|
||||
"@npmcli/node-gyp",
|
||||
"@npmcli/promise-spawn",
|
||||
"@npmcli/query",
|
||||
"@tootallnate/once",
|
||||
"agent-base",
|
||||
"agentkeepalive",
|
||||
"aggregate-error",
|
||||
"ansi-regex",
|
||||
"ansi-styles",
|
||||
"aproba",
|
||||
"are-we-there-yet",
|
||||
"asap",
|
||||
"balanced-match",
|
||||
"bin-links",
|
||||
"binary-extensions",
|
||||
"brace-expansion",
|
||||
"builtins",
|
||||
"cidr-regex",
|
||||
"clean-stack",
|
||||
"clone",
|
||||
"cmd-shim",
|
||||
"color-convert",
|
||||
"color-name",
|
||||
"color-support",
|
||||
"common-ancestor-path",
|
||||
"concat-map",
|
||||
"console-control-strings",
|
||||
"cssesc",
|
||||
"debug",
|
||||
"debuglog",
|
||||
"defaults",
|
||||
"delegates",
|
||||
"depd",
|
||||
"dezalgo",
|
||||
"diff",
|
||||
"emoji-regex",
|
||||
"encoding",
|
||||
"env-paths",
|
||||
"err-code",
|
||||
"fs.realpath",
|
||||
"function-bind",
|
||||
"gauge",
|
||||
"has",
|
||||
"has-flag",
|
||||
"has-unicode",
|
||||
"http-cache-semantics",
|
||||
"http-proxy-agent",
|
||||
"https-proxy-agent",
|
||||
"humanize-ms",
|
||||
"iconv-lite",
|
||||
"ignore-walk",
|
||||
"imurmurhash",
|
||||
"indent-string",
|
||||
"infer-owner",
|
||||
"inflight",
|
||||
"inherits",
|
||||
"ip",
|
||||
"ip-regex",
|
||||
"is-core-module",
|
||||
"is-fullwidth-code-point",
|
||||
"is-lambda",
|
||||
"isexe",
|
||||
"json-stringify-nice",
|
||||
"jsonparse",
|
||||
"just-diff",
|
||||
"just-diff-apply",
|
||||
"lru-cache",
|
||||
"minipass-collect",
|
||||
"minipass-fetch",
|
||||
"minipass-flush",
|
||||
"minipass-json-stream",
|
||||
"minipass-sized",
|
||||
"minizlib",
|
||||
"mute-stream",
|
||||
"negotiator",
|
||||
"normalize-package-data",
|
||||
"npm-bundled",
|
||||
"npm-normalize-package-bin",
|
||||
"npm-packlist",
|
||||
"once",
|
||||
"path-is-absolute",
|
||||
"postcss-selector-parser",
|
||||
"promise-all-reject-late",
|
||||
"promise-call-limit",
|
||||
"promise-inflight",
|
||||
"promise-retry",
|
||||
"promzard",
|
||||
"read-cmd-shim",
|
||||
"readable-stream",
|
||||
"retry",
|
||||
"safe-buffer",
|
||||
"safer-buffer",
|
||||
"set-blocking",
|
||||
"signal-exit",
|
||||
"smart-buffer",
|
||||
"socks",
|
||||
"socks-proxy-agent",
|
||||
"spdx-correct",
|
||||
"spdx-exceptions",
|
||||
"spdx-expression-parse",
|
||||
"spdx-license-ids",
|
||||
"string_decoder",
|
||||
"string-width",
|
||||
"strip-ansi",
|
||||
"supports-color",
|
||||
"unique-filename",
|
||||
"unique-slug",
|
||||
"util-deprecate",
|
||||
"validate-npm-package-license",
|
||||
"walk-up-path",
|
||||
"wcwidth",
|
||||
"wide-align",
|
||||
"wrappy",
|
||||
"yallist"
|
||||
],
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
@ -9507,33 +9624,6 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
|
||||
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin-prettier.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-linter-helpers": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
|
||||
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fast-diff": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-format": {
|
||||
"version": "29.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz",
|
||||
@ -9619,11 +9709,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/query-string": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz",
|
||||
"integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==",
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
|
||||
"integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
|
||||
"dependencies": {
|
||||
"decode-uri-component": "^0.2.0",
|
||||
"decode-uri-component": "^0.2.2",
|
||||
"filter-obj": "^1.1.0",
|
||||
"split-on-first": "^1.0.0",
|
||||
"strict-uri-encode": "^2.0.0"
|
||||
@ -10157,6 +10247,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/stoppable": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
|
||||
"integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==",
|
||||
"engines": {
|
||||
"node": ">=4",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/strict-uri-encode": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
|
||||
@ -10490,9 +10589,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
|
||||
"integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
|
||||
"version": "4.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
|
||||
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@ -12572,6 +12671,14 @@
|
||||
"strip-json-comments": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"@godaddy/terminus": {
|
||||
"version": "4.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@godaddy/terminus/-/terminus-4.11.2.tgz",
|
||||
"integrity": "sha512-e/kbOWpGKME42eltM/wXM3RxSUOrfureZxEd6Dt6NXyFoJ7E8lnmm7znXydJsL3B7ky4HRFZI+eHrep54NZbeQ==",
|
||||
"requires": {
|
||||
"stoppable": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"@humanwhocodes/config-array": {
|
||||
"version": "0.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz",
|
||||
@ -13029,53 +13136,81 @@
|
||||
"@maxmind/geoip2-node": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"@sentry/core": {
|
||||
"version": "7.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.17.4.tgz",
|
||||
"integrity": "sha512-U3ABSJBKGK8dJ01nEG2+qNOb6Wv7U3VqoajiZxfV4lpPWNFGCoEhiTytxBlFTOCmdUH8209zSZiWJZaDLy+TSA==",
|
||||
"requires": {
|
||||
"@sentry/types": "7.17.4",
|
||||
"@sentry/utils": "7.17.4",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/node": {
|
||||
"version": "7.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.17.4.tgz",
|
||||
"integrity": "sha512-cR+Gsir9c/tzFWxvk4zXkMQy6tNRHEYixHrb88XIjZVYDqDS9l2/bKs5nJusdmaUeLtmPp5Et2o7RJyS7gvKTQ==",
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.19.0.tgz",
|
||||
"integrity": "sha512-yG7Tx32WqOkEHVotFLrumCcT9qlaSDTkFNZ+yLSvZXx74ifsE781DzBA9W7K7bBdYO3op+p2YdsOKzf3nPpAyQ==",
|
||||
"requires": {
|
||||
"@sentry/core": "7.17.4",
|
||||
"@sentry/types": "7.17.4",
|
||||
"@sentry/utils": "7.17.4",
|
||||
"@sentry/core": "7.19.0",
|
||||
"@sentry/types": "7.19.0",
|
||||
"@sentry/utils": "7.19.0",
|
||||
"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.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.17.4.tgz",
|
||||
"integrity": "sha512-9Fz6DI16ddnd970mlB5MiCNRSmSXp4SVZ1Yv3L22oS3kQeNxjBTE+htYNwJzSPrQp9aL/LqTYwlnrCy24u9XQA==",
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.19.0.tgz",
|
||||
"integrity": "sha512-SWY17M3TsgBePaGowUcSqBwaT0TJQzuNexVnLojuU0k6F57L9hubvP9zaoosoCfARXQ/3NypAFWnlJyf570rFQ==",
|
||||
"requires": {
|
||||
"@sentry/core": "7.17.4",
|
||||
"@sentry/types": "7.17.4",
|
||||
"@sentry/utils": "7.17.4",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/types": {
|
||||
"version": "7.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.17.4.tgz",
|
||||
"integrity": "sha512-QJj8vO4AtxuzQfJIzDnECSmoxwnS+WJsm1Ta2Cwdy+TUCBJyWpW7aIJJGta76zb9gNPGb3UcAbeEjhMJBJeRMQ=="
|
||||
},
|
||||
"@sentry/utils": {
|
||||
"version": "7.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.17.4.tgz",
|
||||
"integrity": "sha512-ioG0ANy8uiWzig82/e7cc+6C9UOxkyBzJDi1luoQVDH6P0/PvM8GzVU+1iUVUipf8+OL1Jh09GrWnd5wLm3XNQ==",
|
||||
"requires": {
|
||||
"@sentry/types": "7.17.4",
|
||||
"@sentry/core": "7.19.0",
|
||||
"@sentry/types": "7.19.0",
|
||||
"@sentry/utils": "7.19.0",
|
||||
"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": {
|
||||
@ -13297,12 +13432,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/prettier": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz",
|
||||
"integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/qs": {
|
||||
"version": "6.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
|
||||
@ -13337,6 +13466,22 @@
|
||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/swagger-jsdoc": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.1.tgz",
|
||||
"integrity": "sha512-+MUpcbyxD528dECUBCEVm6abNuORdbuGjbrUdHDeAQ+rkPuo2a+L4N02WJHF3bonSSE6SJ3dUJwF2V6+cHnf0w==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/swagger-ui-express": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.3.tgz",
|
||||
"integrity": "sha512-jqCjGU/tGEaqIplPy3WyQg+Nrp6y80DCFnDEAvVKWkJyv0VivSSDCChkppHRHAablvInZe6pijDFMnavtN0vqA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/express": "*",
|
||||
"@types/serve-static": "*"
|
||||
}
|
||||
},
|
||||
"@types/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
@ -14067,9 +14212,9 @@
|
||||
}
|
||||
},
|
||||
"decode-uri-component": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og=="
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="
|
||||
},
|
||||
"dedent": {
|
||||
"version": "0.7.0",
|
||||
@ -14277,22 +14422,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslint-config-prettier": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz",
|
||||
"integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"eslint-plugin-prettier": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
|
||||
"integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"prettier-linter-helpers": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"eslint-scope": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
||||
@ -14489,9 +14618,9 @@
|
||||
}
|
||||
},
|
||||
"express-rate-limit": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.6.0.tgz",
|
||||
"integrity": "sha512-HFN2+4ZGdkQOS8Qli4z6knmJFnw6lZed67o6b7RGplWeb1Z0s8VXaj3dUgPIdm9hrhZXTRpCTHXA0/2Eqex0vA==",
|
||||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz",
|
||||
"integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==",
|
||||
"requires": {}
|
||||
},
|
||||
"express-validator": {
|
||||
@ -14515,12 +14644,6 @@
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
},
|
||||
"fast-diff": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
|
||||
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
|
||||
"dev": true
|
||||
},
|
||||
"fast-glob": {
|
||||
"version": "3.2.12",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
|
||||
@ -14882,12 +15005,6 @@
|
||||
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
|
||||
"dev": true
|
||||
},
|
||||
"husky": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz",
|
||||
"integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==",
|
||||
"dev": true
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
@ -15451,7 +15568,6 @@
|
||||
"@jest/transform": "^29.3.1",
|
||||
"@jest/types": "^29.3.1",
|
||||
"@types/babel__traverse": "^7.0.6",
|
||||
"@types/prettier": "^2.1.5",
|
||||
"babel-preset-current-node-syntax": "^1.0.0",
|
||||
"chalk": "^4.0.0",
|
||||
"expect": "^29.3.1",
|
||||
@ -15930,9 +16046,9 @@
|
||||
}
|
||||
},
|
||||
"mongoose": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.7.1.tgz",
|
||||
"integrity": "sha512-qbagtqSyvIhUz4EWzXC00EA0DJHFrQwlzTlNGX5DjiESoJiPKqkEga1k9hviFKRFgBna+OlW54mkdi+0+AqxCw==",
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.7.2.tgz",
|
||||
"integrity": "sha512-lrP2V5U1qhaf+z33fiIn7aYAZZ1fVDly+TkFRjTujNBF/FIHESATj2RbgAOSlWqv32fsZXkXejXzeVfjbv35Ow==",
|
||||
"requires": {
|
||||
"bson": "^4.7.0",
|
||||
"kareem": "2.4.1",
|
||||
@ -18094,21 +18210,6 @@
|
||||
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
||||
"dev": true
|
||||
},
|
||||
"prettier": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
|
||||
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
|
||||
"dev": true
|
||||
},
|
||||
"prettier-linter-helpers": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
|
||||
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-diff": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"pretty-format": {
|
||||
"version": "29.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz",
|
||||
@ -18172,11 +18273,11 @@
|
||||
}
|
||||
},
|
||||
"query-string": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz",
|
||||
"integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==",
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
|
||||
"integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
|
||||
"requires": {
|
||||
"decode-uri-component": "^0.2.0",
|
||||
"decode-uri-component": "^0.2.2",
|
||||
"filter-obj": "^1.1.0",
|
||||
"split-on-first": "^1.0.0",
|
||||
"strict-uri-encode": "^2.0.0"
|
||||
@ -18558,6 +18659,11 @@
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
|
||||
},
|
||||
"stoppable": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
|
||||
"integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="
|
||||
},
|
||||
"strict-uri-encode": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
|
||||
@ -18791,9 +18897,9 @@
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
|
||||
"integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ=="
|
||||
"version": "4.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
|
||||
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.17.4",
|
||||
|
@ -1,7 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@godaddy/terminus": "^4.11.2",
|
||||
"@sentry/node": "^7.14.0",
|
||||
"@sentry/tracing": "^7.14.0",
|
||||
"@sentry/tracing": "^7.19.0",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"axios": "^1.1.3",
|
||||
"bigint-conversion": "^2.2.2",
|
||||
@ -10,32 +11,33 @@
|
||||
"crypto-js": "^4.1.1",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.1",
|
||||
"express-rate-limit": "^6.5.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.1",
|
||||
"mongoose": "^6.7.2",
|
||||
"nodemailer": "^6.8.0",
|
||||
"posthog-node": "^2.1.0",
|
||||
"query-string": "^7.1.1",
|
||||
"query-string": "^7.1.3",
|
||||
"rimraf": "^3.0.2",
|
||||
"stripe": "^10.7.0",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"typescript": "^4.8.4"
|
||||
"typescript": "^4.9.3"
|
||||
},
|
||||
"name": "infisical-api",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"prepare": "cd .. && npm install",
|
||||
"start": "npm run build && node build/index.js",
|
||||
"dev": "nodemon",
|
||||
"build": "rimraf ./build && tsc && cp -R ./src/templates ./src/json ./build",
|
||||
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint-and-fix": "eslint . --ext .ts --fix",
|
||||
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write"
|
||||
"lint-staged": "lint-staged"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -56,17 +58,15 @@
|
||||
"@types/jsonwebtoken": "^8.5.9",
|
||||
"@types/node": "^18.11.3",
|
||||
"@types/nodemailer": "^6.4.6",
|
||||
"@types/swagger-jsdoc": "^6.0.1",
|
||||
"@types/swagger-ui-express": "^4.1.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"eslint": "^8.26.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"husky": "^8.0.1",
|
||||
"install": "^0.13.0",
|
||||
"jest": "^29.3.1",
|
||||
"nodemon": "^2.0.19",
|
||||
"npm": "^8.19.3",
|
||||
"prettier": "^2.7.1",
|
||||
"ts-node": "^10.9.1"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
const PORT = process.env.PORT || 4000;
|
||||
const EMAIL_TOKEN_LIFETIME = process.env.EMAIL_TOKEN_LIFETIME! || '86400'; // investigate
|
||||
const EMAIL_TOKEN_LIFETIME = process.env.EMAIL_TOKEN_LIFETIME! || '86400';
|
||||
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!;
|
||||
const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d';
|
||||
const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!;
|
||||
@ -10,14 +10,23 @@ const JWT_SIGNUP_LIFETIME = process.env.JWT_SIGNUP_LIFETIME! || '15m';
|
||||
const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!;
|
||||
const MONGO_URL = process.env.MONGO_URL!;
|
||||
const NODE_ENV = process.env.NODE_ENV! || 'production';
|
||||
const OAUTH_CLIENT_SECRET_HEROKU = process.env.OAUTH_CLIENT_SECRET_HEROKU!;
|
||||
const OAUTH_TOKEN_URL_HEROKU = process.env.OAUTH_TOKEN_URL_HEROKU!;
|
||||
const POSTHOG_HOST = process.env.POSTHOG_HOST!;
|
||||
const POSTHOG_PROJECT_API_KEY = process.env.POSTHOG_PROJECT_API_KEY!;
|
||||
const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
|
||||
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
|
||||
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
|
||||
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!;
|
||||
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
|
||||
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
|
||||
const CLIENT_SLUG_VERCEL= process.env.CLIENT_SLUG_VERCEL!;
|
||||
const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com';
|
||||
const POSTHOG_PROJECT_API_KEY =
|
||||
process.env.POSTHOG_PROJECT_API_KEY! ||
|
||||
'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE';
|
||||
const PRIVATE_KEY = process.env.PRIVATE_KEY!;
|
||||
const PUBLIC_KEY = process.env.PUBLIC_KEY!;
|
||||
const SENTRY_DSN = process.env.SENTRY_DSN!;
|
||||
const SITE_URL = process.env.SITE_URL!;
|
||||
const SMTP_HOST = process.env.SMTP_HOST! || 'smtp.gmail.com';
|
||||
const SMTP_PORT = process.env.SMTP_PORT! || 587;
|
||||
const SMTP_NAME = process.env.SMTP_NAME!;
|
||||
const SMTP_USERNAME = process.env.SMTP_USERNAME!;
|
||||
const SMTP_PASSWORD = process.env.SMTP_PASSWORD!;
|
||||
@ -27,37 +36,44 @@ const STRIPE_PRODUCT_STARTER = process.env.STRIPE_PRODUCT_STARTER!;
|
||||
const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY!;
|
||||
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY!;
|
||||
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!;
|
||||
const WEBSITE_URL = 'http://frontend:3000';
|
||||
const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED! !== 'false' && true;
|
||||
|
||||
export {
|
||||
PORT,
|
||||
EMAIL_TOKEN_LIFETIME,
|
||||
ENCRYPTION_KEY,
|
||||
JWT_AUTH_LIFETIME,
|
||||
JWT_AUTH_SECRET,
|
||||
JWT_REFRESH_LIFETIME,
|
||||
JWT_REFRESH_SECRET,
|
||||
JWT_SERVICE_SECRET,
|
||||
JWT_SIGNUP_LIFETIME,
|
||||
JWT_SIGNUP_SECRET,
|
||||
MONGO_URL,
|
||||
NODE_ENV,
|
||||
OAUTH_CLIENT_SECRET_HEROKU,
|
||||
OAUTH_TOKEN_URL_HEROKU,
|
||||
POSTHOG_HOST,
|
||||
POSTHOG_PROJECT_API_KEY,
|
||||
PRIVATE_KEY,
|
||||
PUBLIC_KEY,
|
||||
SENTRY_DSN,
|
||||
SMTP_HOST,
|
||||
SMTP_NAME,
|
||||
SMTP_USERNAME,
|
||||
SMTP_PASSWORD,
|
||||
STRIPE_PRODUCT_CARD_AUTH,
|
||||
STRIPE_PRODUCT_PRO,
|
||||
STRIPE_PRODUCT_STARTER,
|
||||
STRIPE_PUBLISHABLE_KEY,
|
||||
STRIPE_SECRET_KEY,
|
||||
STRIPE_WEBHOOK_SECRET,
|
||||
WEBSITE_URL
|
||||
PORT,
|
||||
EMAIL_TOKEN_LIFETIME,
|
||||
ENCRYPTION_KEY,
|
||||
JWT_AUTH_LIFETIME,
|
||||
JWT_AUTH_SECRET,
|
||||
JWT_REFRESH_LIFETIME,
|
||||
JWT_REFRESH_SECRET,
|
||||
JWT_SERVICE_SECRET,
|
||||
JWT_SIGNUP_LIFETIME,
|
||||
JWT_SIGNUP_SECRET,
|
||||
MONGO_URL,
|
||||
NODE_ENV,
|
||||
CLIENT_ID_HEROKU,
|
||||
CLIENT_ID_VERCEL,
|
||||
CLIENT_ID_NETLIFY,
|
||||
CLIENT_SECRET_HEROKU,
|
||||
CLIENT_SECRET_VERCEL,
|
||||
CLIENT_SECRET_NETLIFY,
|
||||
CLIENT_SLUG_VERCEL,
|
||||
POSTHOG_HOST,
|
||||
POSTHOG_PROJECT_API_KEY,
|
||||
PRIVATE_KEY,
|
||||
PUBLIC_KEY,
|
||||
SENTRY_DSN,
|
||||
SITE_URL,
|
||||
SMTP_HOST,
|
||||
SMTP_PORT,
|
||||
SMTP_NAME,
|
||||
SMTP_USERNAME,
|
||||
SMTP_PASSWORD,
|
||||
STRIPE_PRODUCT_CARD_AUTH,
|
||||
STRIPE_PRODUCT_PRO,
|
||||
STRIPE_PRODUCT_STARTER,
|
||||
STRIPE_PUBLISHABLE_KEY,
|
||||
STRIPE_SECRET_KEY,
|
||||
STRIPE_WEBHOOK_SECRET,
|
||||
TELEMETRY_ENABLED
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import { Request, Response } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import * as Sentry from '@sentry/node';
|
||||
@ -5,17 +6,17 @@ import * as bigintConversion from 'bigint-conversion';
|
||||
const jsrp = require('jsrp');
|
||||
import { User } from '../models';
|
||||
import { createToken, issueTokens, clearTokens } from '../helpers/auth';
|
||||
import {
|
||||
NODE_ENV,
|
||||
JWT_AUTH_LIFETIME,
|
||||
JWT_AUTH_SECRET,
|
||||
JWT_REFRESH_SECRET
|
||||
import {
|
||||
NODE_ENV,
|
||||
JWT_AUTH_LIFETIME,
|
||||
JWT_AUTH_SECRET,
|
||||
JWT_REFRESH_SECRET
|
||||
} from '../config';
|
||||
|
||||
declare module 'jsonwebtoken' {
|
||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||
userId: string;
|
||||
}
|
||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||
userId: string;
|
||||
}
|
||||
}
|
||||
|
||||
const clientPublicKeys: any = {};
|
||||
@ -27,47 +28,45 @@ const clientPublicKeys: any = {};
|
||||
* @returns
|
||||
*/
|
||||
export const login1 = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
email,
|
||||
clientPublicKey
|
||||
}: { email: string; clientPublicKey: string } = req.body;
|
||||
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select('+salt +verifier');
|
||||
|
||||
try {
|
||||
const {
|
||||
email,
|
||||
clientPublicKey
|
||||
}: { email: string; clientPublicKey: string } = req.body;
|
||||
|
||||
if (!user) throw new Error('Failed to find user');
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select('+salt +verifier');
|
||||
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier
|
||||
},
|
||||
() => {
|
||||
// generate server-side public key
|
||||
const serverPublicKey = server.getPublicKey();
|
||||
clientPublicKeys[email] = {
|
||||
clientPublicKey,
|
||||
serverBInt: bigintConversion.bigintToBuf(server.bInt)
|
||||
};
|
||||
|
||||
if (!user) throw new Error('Failed to find user');
|
||||
|
||||
return res.status(200).send({
|
||||
serverPublicKey,
|
||||
salt: user.salt
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to start authentication process'
|
||||
});
|
||||
}
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier
|
||||
},
|
||||
() => {
|
||||
// generate server-side public key
|
||||
const serverPublicKey = server.getPublicKey();
|
||||
clientPublicKeys[email] = {
|
||||
clientPublicKey,
|
||||
serverBInt: bigintConversion.bigintToBuf(server.bInt)
|
||||
};
|
||||
|
||||
return res.status(200).send({
|
||||
serverPublicKey,
|
||||
salt: user.salt
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to start authentication process'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -78,59 +77,59 @@ export const login1 = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const login2 = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { email, clientProof } = req.body;
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
|
||||
try {
|
||||
const { email, clientProof } = req.body;
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
|
||||
|
||||
if (!user) throw new Error('Failed to find user');
|
||||
if (!user) throw new Error('Failed to find user');
|
||||
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier,
|
||||
b: clientPublicKeys[email].serverBInt
|
||||
},
|
||||
async () => {
|
||||
server.setClientPublicKey(clientPublicKeys[email].clientPublicKey);
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier,
|
||||
b: clientPublicKeys[email].serverBInt
|
||||
},
|
||||
async () => {
|
||||
server.setClientPublicKey(clientPublicKeys[email].clientPublicKey);
|
||||
|
||||
// compare server and client shared keys
|
||||
if (server.checkClientProof(clientProof)) {
|
||||
// issue tokens
|
||||
const tokens = await issueTokens({ userId: user._id.toString() });
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie('jid', tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: '/token',
|
||||
sameSite: "strict",
|
||||
secure: NODE_ENV === 'production' ? true : false
|
||||
});
|
||||
// compare server and client shared keys
|
||||
if (server.checkClientProof(clientProof)) {
|
||||
// issue tokens
|
||||
const tokens = await issueTokens({ userId: user._id.toString() });
|
||||
|
||||
// return (access) token in response
|
||||
return res.status(200).send({
|
||||
token: tokens.token,
|
||||
publicKey: user.publicKey,
|
||||
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||
iv: user.iv,
|
||||
tag: user.tag
|
||||
});
|
||||
}
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie('jid', tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: NODE_ENV === 'production' ? true : false
|
||||
});
|
||||
|
||||
return res.status(400).send({
|
||||
message: 'Failed to authenticate. Try again?'
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to authenticate. Try again?'
|
||||
});
|
||||
}
|
||||
// return (access) token in response
|
||||
return res.status(200).send({
|
||||
token: tokens.token,
|
||||
publicKey: user.publicKey,
|
||||
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||
iv: user.iv,
|
||||
tag: user.tag
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
message: 'Failed to authenticate. Try again?'
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to authenticate. Try again?'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -140,29 +139,29 @@ export const login2 = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const logout = async (req: Request, res: Response) => {
|
||||
try {
|
||||
await clearTokens({
|
||||
userId: req.user._id.toString()
|
||||
});
|
||||
|
||||
// clear httpOnly cookie
|
||||
res.cookie('jid', '', {
|
||||
httpOnly: true,
|
||||
path: '/token',
|
||||
sameSite: "strict",
|
||||
secure: NODE_ENV === 'production' ? true : false
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to logout'
|
||||
});
|
||||
}
|
||||
try {
|
||||
await clearTokens({
|
||||
userId: req.user._id.toString()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully logged out.'
|
||||
});
|
||||
// clear httpOnly cookie
|
||||
res.cookie('jid', '', {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: NODE_ENV === 'production' ? true : false
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to logout'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully logged out.'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@ -172,9 +171,9 @@ export const logout = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const checkAuth = async (req: Request, res: Response) =>
|
||||
res.status(200).send({
|
||||
message: 'Authenticated'
|
||||
});
|
||||
res.status(200).send({
|
||||
message: 'Authenticated'
|
||||
});
|
||||
|
||||
/**
|
||||
* Return new token by redeeming refresh token
|
||||
@ -183,42 +182,41 @@ export const checkAuth = async (req: Request, res: Response) =>
|
||||
* @returns
|
||||
*/
|
||||
export const getNewToken = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const refreshToken = req.cookies.jid;
|
||||
|
||||
if (!refreshToken) {
|
||||
throw new Error('Failed to find token in request cookies');
|
||||
}
|
||||
|
||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(refreshToken, JWT_REFRESH_SECRET)
|
||||
);
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: decodedToken.userId
|
||||
}).select('+publicKey');
|
||||
try {
|
||||
const refreshToken = req.cookies.jid;
|
||||
|
||||
if (!user) throw new Error('Failed to authenticate unfound user');
|
||||
if (!user?.publicKey)
|
||||
throw new Error('Failed to authenticate not fully set up account');
|
||||
|
||||
const token = createToken({
|
||||
payload: {
|
||||
userId: decodedToken.userId
|
||||
},
|
||||
expiresIn: JWT_AUTH_LIFETIME,
|
||||
secret: JWT_AUTH_SECRET
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
token
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Invalid request'
|
||||
});
|
||||
}
|
||||
if (!refreshToken) {
|
||||
throw new Error('Failed to find token in request cookies');
|
||||
}
|
||||
|
||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(refreshToken, JWT_REFRESH_SECRET)
|
||||
);
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: decodedToken.userId
|
||||
}).select('+publicKey');
|
||||
|
||||
if (!user) throw new Error('Failed to authenticate unfound user');
|
||||
if (!user?.publicKey)
|
||||
throw new Error('Failed to authenticate not fully set up account');
|
||||
|
||||
const token = createToken({
|
||||
payload: {
|
||||
userId: decodedToken.userId
|
||||
},
|
||||
expiresIn: JWT_AUTH_LIFETIME,
|
||||
secret: JWT_AUTH_SECRET
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
token
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Invalid request'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
107
backend/src/controllers/botController.ts
Normal file
107
backend/src/controllers/botController.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Bot, BotKey } from '../models';
|
||||
import { createBot } from '../helpers/bot';
|
||||
|
||||
interface BotKey {
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return bot for workspace with id [workspaceId]. If a workspace bot doesn't exist,
|
||||
* then create and return a new bot.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
||||
let bot;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
bot = await Bot.findOne({
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
// case: bot doesn't exist for workspace with id [workspaceId]
|
||||
// -> create a new bot and return it
|
||||
bot = await createBot({
|
||||
name: 'Infisical Bot',
|
||||
workspaceId
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get bot for workspace'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
bot
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return bot with id [req.bot._id] with active state set to [isActive].
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const setBotActiveState = async (req: Request, res: Response) => {
|
||||
let bot;
|
||||
try {
|
||||
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
|
||||
|
||||
if (isActive) {
|
||||
// bot state set to active -> share workspace key with bot
|
||||
if (!botKey?.encryptedKey || !botKey?.nonce) {
|
||||
return res.status(400).send({
|
||||
message: 'Failed to set bot state to active - missing bot key'
|
||||
});
|
||||
}
|
||||
|
||||
await BotKey.findOneAndUpdate({
|
||||
workspace: req.bot.workspace
|
||||
}, {
|
||||
encryptedKey: botKey.encryptedKey,
|
||||
nonce: botKey.nonce,
|
||||
sender: req.user._id,
|
||||
bot: req.bot._id,
|
||||
workspace: req.bot.workspace
|
||||
}, {
|
||||
upsert: true,
|
||||
new: true
|
||||
});
|
||||
} else {
|
||||
// case: bot state set to inactive -> delete bot's workspace key
|
||||
await BotKey.deleteOne({
|
||||
bot: req.bot._id
|
||||
});
|
||||
}
|
||||
|
||||
bot = await Bot.findOneAndUpdate({
|
||||
_id: req.bot._id
|
||||
}, {
|
||||
isActive
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
|
||||
if (!bot) throw new Error('Failed to update bot active state');
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to update bot active state'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
bot
|
||||
});
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
import * as authController from './authController';
|
||||
import * as botController from './botController';
|
||||
import * as integrationAuthController from './integrationAuthController';
|
||||
import * as integrationController from './integrationController';
|
||||
import * as keyController from './keyController';
|
||||
@ -16,6 +17,7 @@ import * as workspaceController from './workspaceController';
|
||||
|
||||
export {
|
||||
authController,
|
||||
botController,
|
||||
integrationAuthController,
|
||||
integrationController,
|
||||
keyController,
|
||||
|
@ -3,69 +3,45 @@ import * as Sentry from '@sentry/node';
|
||||
import axios from 'axios';
|
||||
import { readFileSync } from 'fs';
|
||||
import { IntegrationAuth, Integration } from '../models';
|
||||
import { processOAuthTokenRes } from '../helpers/integrationAuth';
|
||||
import { INTEGRATION_SET, ENV_DEV } from '../variables';
|
||||
import { OAUTH_CLIENT_SECRET_HEROKU, OAUTH_TOKEN_URL_HEROKU } from '../config';
|
||||
import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../variables';
|
||||
import { IntegrationService } from '../services';
|
||||
import { getApps, revokeAccess } from '../integrations';
|
||||
|
||||
export const getIntegrationOptions = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
return res.status(200).send({
|
||||
integrationOptions: INTEGRATION_OPTIONS
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId]
|
||||
* Note: integration [integration] must be set up compatible/designed for OAuth2
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const integrationAuthOauthExchange = async (
|
||||
export const oAuthExchange = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
let clientSecret;
|
||||
|
||||
const { workspaceId, code, integration } = req.body;
|
||||
|
||||
if (!INTEGRATION_SET.has(integration))
|
||||
throw new Error('Failed to validate integration');
|
||||
|
||||
// use correct client secret
|
||||
switch (integration) {
|
||||
case 'heroku':
|
||||
clientSecret = OAUTH_CLIENT_SECRET_HEROKU;
|
||||
}
|
||||
|
||||
// TODO: unfinished - make compatible with other integration types
|
||||
const res = await axios.post(
|
||||
OAUTH_TOKEN_URL_HEROKU!,
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
client_secret: clientSecret
|
||||
} as any)
|
||||
);
|
||||
|
||||
const integrationAuth = await processOAuthTokenRes({
|
||||
|
||||
await IntegrationService.handleOAuthExchange({
|
||||
workspaceId,
|
||||
integration,
|
||||
res
|
||||
code
|
||||
});
|
||||
|
||||
// create or replace integration
|
||||
const integrationObj = await Integration.findOneAndUpdate(
|
||||
{ workspace: workspaceId, integration },
|
||||
{
|
||||
workspace: workspaceId,
|
||||
environment: ENV_DEV,
|
||||
isActive: false,
|
||||
app: null,
|
||||
integration,
|
||||
integrationAuth: integrationAuth._id
|
||||
},
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get OAuth2 token'
|
||||
message: 'Failed to get OAuth2 code-token exchange'
|
||||
});
|
||||
}
|
||||
|
||||
@ -75,26 +51,25 @@ export const integrationAuthOauthExchange = async (
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of applications allowed for integration with id [integrationAuthId]
|
||||
* Return list of applications allowed for integration with integration authorization id [integrationAuthId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
||||
// TODO: unfinished - make compatible with other integration types
|
||||
let apps;
|
||||
try {
|
||||
const res = await axios.get('https://api.heroku.com/apps', {
|
||||
headers: {
|
||||
Accept: 'application/vnd.heroku+json; version=3',
|
||||
Authorization: 'Bearer ' + req.accessToken
|
||||
}
|
||||
apps = await getApps({
|
||||
integrationAuth: req.integrationAuth,
|
||||
accessToken: req.accessToken
|
||||
});
|
||||
|
||||
apps = res.data.map((a: any) => ({
|
||||
name: a.name
|
||||
}));
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get integration authorization applications'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
apps
|
||||
@ -108,46 +83,22 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
||||
// TODO: unfinished - disable application via Heroku API and make compatible with other integration types
|
||||
try {
|
||||
const { integrationAuthId } = req.params;
|
||||
|
||||
// TODO: disable application via Heroku API; figure out what authorization id is
|
||||
|
||||
const integrations = JSON.parse(
|
||||
readFileSync('./src/json/integrations.json').toString()
|
||||
);
|
||||
|
||||
let authorizationId;
|
||||
switch (req.integrationAuth.integration) {
|
||||
case 'heroku':
|
||||
authorizationId = integrations.heroku.clientId;
|
||||
}
|
||||
|
||||
// not sure what authorizationId is?
|
||||
// // revoke authorization
|
||||
// const res2 = await axios.delete(
|
||||
// `https://api.heroku.com/oauth/authorizations/${authorizationId}`,
|
||||
// {
|
||||
// headers: {
|
||||
// 'Accept': 'application/vnd.heroku+json; version=3',
|
||||
// 'Authorization': 'Bearer ' + req.accessToken
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
|
||||
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
||||
_id: integrationAuthId
|
||||
await revokeAccess({
|
||||
integrationAuth: req.integrationAuth,
|
||||
accessToken: req.accessToken
|
||||
});
|
||||
|
||||
if (deletedIntegrationAuth) {
|
||||
await Integration.deleteMany({
|
||||
integrationAuth: deletedIntegrationAuth._id
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to delete integration authorization'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully deleted integration authorization'
|
||||
});
|
||||
}
|
@ -1,11 +1,9 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { readFileSync } from 'fs';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import axios from 'axios';
|
||||
import { Integration } from '../models';
|
||||
import { decryptAsymmetric } from '../utils/crypto';
|
||||
import { decryptSecrets } from '../helpers/secret';
|
||||
import { PRIVATE_KEY } from '../config';
|
||||
import { Integration, Bot, BotKey } from '../models';
|
||||
import { EventService } from '../services';
|
||||
import { eventPushSecrets } from '../events';
|
||||
|
||||
interface Key {
|
||||
encryptedKey: string;
|
||||
@ -24,104 +22,58 @@ interface PushSecret {
|
||||
type: 'shared' | 'personal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of all available integrations on Infisical
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getIntegrations = async (req: Request, res: Response) => {
|
||||
let integrations;
|
||||
try {
|
||||
integrations = JSON.parse(
|
||||
readFileSync('./src/json/integrations.json').toString()
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get integrations'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
integrations
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync secrets [secrets] to integration with id [integrationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const syncIntegration = async (req: Request, res: Response) => {
|
||||
// TODO: unfinished - make more versatile to accomodate for other integrations
|
||||
try {
|
||||
const { key, secrets }: { key: Key; secrets: PushSecret[] } = req.body;
|
||||
const symmetricKey = decryptAsymmetric({
|
||||
ciphertext: key.encryptedKey,
|
||||
nonce: key.nonce,
|
||||
publicKey: req.user.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
// decrypt secrets with symmetric key
|
||||
const content = decryptSecrets({
|
||||
secrets,
|
||||
key: symmetricKey,
|
||||
format: 'object'
|
||||
});
|
||||
|
||||
// TODO: make integration work for other integrations as well
|
||||
const res = await axios.patch(
|
||||
`https://api.heroku.com/apps/${req.integration.app}/config-vars`,
|
||||
content,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/vnd.heroku+json; version=3',
|
||||
Authorization: 'Bearer ' + req.accessToken
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to sync secrets with integration'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully synced secrets with integration'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Change environment or name of integration with id [integrationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const modifyIntegration = async (req: Request, res: Response) => {
|
||||
export const updateIntegration = async (req: Request, res: Response) => {
|
||||
let integration;
|
||||
|
||||
// TODO: add integration-specific validation to ensure that each
|
||||
// integration has the correct fields populated in [Integration]
|
||||
|
||||
try {
|
||||
const { update } = req.body;
|
||||
|
||||
const {
|
||||
app,
|
||||
environment,
|
||||
isActive,
|
||||
target, // vercel-specific integration param
|
||||
context, // netlify-specific integration param
|
||||
siteId // netlify-specific integration param
|
||||
} = req.body;
|
||||
|
||||
integration = await Integration.findOneAndUpdate(
|
||||
{
|
||||
_id: req.integration._id
|
||||
},
|
||||
update,
|
||||
{
|
||||
environment,
|
||||
isActive,
|
||||
app,
|
||||
target,
|
||||
context,
|
||||
siteId
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
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);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to modify integration'
|
||||
message: 'Failed to update integration'
|
||||
});
|
||||
}
|
||||
|
||||
@ -131,7 +83,8 @@ export const modifyIntegration = async (req: Request, res: Response) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete integration with id [integrationId]
|
||||
* Delete integration with id [integrationId] and deactivate bot if there are
|
||||
* no integrations left
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
@ -144,6 +97,29 @@ export const deleteIntegration = async (req: Request, res: Response) => {
|
||||
deletedIntegration = await Integration.findOneAndDelete({
|
||||
_id: integrationId
|
||||
});
|
||||
|
||||
if (!deletedIntegration) throw new Error('Failed to find integration');
|
||||
|
||||
const integrations = await Integration.find({
|
||||
workspace: deletedIntegration.workspace
|
||||
});
|
||||
|
||||
if (integrations.length === 0) {
|
||||
// case: no integrations left, deactivate bot
|
||||
const bot = await Bot.findOneAndUpdate({
|
||||
workspace: deletedIntegration.workspace
|
||||
}, {
|
||||
isActive: false
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
|
||||
if (bot) {
|
||||
await BotKey.deleteOne({
|
||||
bot: bot._id
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
|
@ -17,16 +17,6 @@ export const uploadKey = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
const { key } = req.body;
|
||||
|
||||
// validate membership of sender
|
||||
const senderMembership = await findMembership({
|
||||
user: req.user._id,
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
if (!senderMembership) {
|
||||
throw new Error('Failed sender membership validation for workspace');
|
||||
}
|
||||
|
||||
// validate membership of receiver
|
||||
const receiverMembership = await findMembership({
|
||||
user: key.userId,
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
deleteMembership as deleteMember
|
||||
} from '../helpers/membership';
|
||||
import { sendMail } from '../helpers/nodemailer';
|
||||
import { WEBSITE_URL } from '../config';
|
||||
import { SITE_URL } from '../config';
|
||||
import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../variables';
|
||||
|
||||
/**
|
||||
@ -217,11 +217,10 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
|
||||
inviterFirstName: req.user.firstName,
|
||||
inviterEmail: req.user.email,
|
||||
workspaceName: req.membership.workspace.name,
|
||||
callback_url: WEBSITE_URL + '/login'
|
||||
callback_url: SITE_URL + '/login'
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import crypto from 'crypto';
|
||||
import { WEBSITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config';
|
||||
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config';
|
||||
import { MembershipOrg, Organization, User, Token } from '../models';
|
||||
import { deleteMembershipOrg as deleteMemberFromOrg } from '../helpers/membershipOrg';
|
||||
import { checkEmailVerification } from '../helpers/signup';
|
||||
@ -186,7 +186,7 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
||||
organizationName: organization.name,
|
||||
email: inviteeEmail,
|
||||
token,
|
||||
callback_url: WEBSITE_URL + '/signupinvite'
|
||||
callback_url: SITE_URL + '/signupinvite'
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -217,7 +217,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { email, code } = req.body;
|
||||
|
||||
user = await User.findOne({ email });
|
||||
user = await User.findOne({ email }).select('+publicKey');
|
||||
if (user && user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
return res.status(403).send({
|
||||
@ -257,7 +257,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
error: 'Failed email magic link confirmation'
|
||||
error: 'Failed email magic link verification for organization invitation'
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
SITE_URL,
|
||||
STRIPE_SECRET_KEY,
|
||||
STRIPE_PRODUCT_STARTER,
|
||||
STRIPE_PRODUCT_PRO,
|
||||
STRIPE_PRODUCT_CARD_AUTH,
|
||||
WEBSITE_URL
|
||||
STRIPE_PRODUCT_CARD_AUTH
|
||||
} from '../config';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
@ -350,13 +351,13 @@ export const createOrganizationPortalSession = async (
|
||||
customer: req.membershipOrg.organization.customerId,
|
||||
mode: 'setup',
|
||||
payment_method_types: ['card'],
|
||||
success_url: WEBSITE_URL + '/dashboard',
|
||||
cancel_url: WEBSITE_URL + '/dashboard'
|
||||
success_url: SITE_URL + '/dashboard',
|
||||
cancel_url: SITE_URL + '/dashboard'
|
||||
});
|
||||
} else {
|
||||
session = await stripe.billingPortal.sessions.create({
|
||||
customer: req.membershipOrg.organization.customerId,
|
||||
return_url: WEBSITE_URL + '/dashboard'
|
||||
return_url: SITE_URL + '/dashboard'
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,121 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import crypto from 'crypto';
|
||||
const jsrp = require('jsrp');
|
||||
import * as bigintConversion from 'bigint-conversion';
|
||||
import { User, BackupPrivateKey } from '../models';
|
||||
import { User, Token, BackupPrivateKey } from '../models';
|
||||
import { checkEmailVerification } from '../helpers/signup';
|
||||
import { createToken } from '../helpers/auth';
|
||||
import { sendMail } from '../helpers/nodemailer';
|
||||
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../config';
|
||||
|
||||
const clientPublicKeys: any = {};
|
||||
|
||||
/**
|
||||
* Password reset step 1: Send email verification link to email [email]
|
||||
* for account recovery.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const emailPasswordReset = async (req: Request, res: Response) => {
|
||||
let email: string;
|
||||
try {
|
||||
email = req.body.email;
|
||||
|
||||
const user = await User.findOne({ email }).select('+publicKey');
|
||||
if (!user || !user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
|
||||
return res.status(403).send({
|
||||
error: 'Failed to send email verification for password reset'
|
||||
});
|
||||
}
|
||||
|
||||
const token = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
await Token.findOneAndUpdate(
|
||||
{ email },
|
||||
{
|
||||
email,
|
||||
token,
|
||||
createdAt: new Date()
|
||||
},
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
|
||||
await sendMail({
|
||||
template: 'passwordReset.handlebars',
|
||||
subjectLine: 'Infisical password reset',
|
||||
recipients: [email],
|
||||
substitutions: {
|
||||
email,
|
||||
token,
|
||||
callback_url: SITE_URL + '/password-reset'
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to send email for account recovery'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: `Sent an email for account recovery to ${email}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Password reset step 2: Verify email verification link sent to email [email]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
||||
let user, token;
|
||||
try {
|
||||
const { email, code } = req.body;
|
||||
|
||||
user = await User.findOne({ email }).select('+publicKey');
|
||||
if (!user || !user?.publicKey) {
|
||||
// case: user doesn't exist with email [email] or
|
||||
// hasn't even completed their account
|
||||
return res.status(403).send({
|
||||
error: 'Failed email verification for password reset'
|
||||
});
|
||||
}
|
||||
|
||||
await checkEmailVerification({
|
||||
email,
|
||||
code
|
||||
});
|
||||
|
||||
// generate temporary password-reset token
|
||||
token = createToken({
|
||||
payload: {
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: JWT_SIGNUP_LIFETIME,
|
||||
secret: JWT_SIGNUP_SECRET
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed email verification for password reset'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully verified email',
|
||||
user,
|
||||
token
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
||||
* @param req
|
||||
@ -43,7 +153,7 @@ export const srp1 = async (req: Request, res: Response) => {
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
error: 'Failed to start change password process'
|
||||
@ -110,7 +220,7 @@ export const changePassword = async (req: Request, res: Response) => {
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
error: 'Failed to change password. Try again?'
|
||||
@ -180,10 +290,73 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to update backup private key'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return backup private key for user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getBackupPrivateKey = async (req: Request, res: Response) => {
|
||||
let backupPrivateKey;
|
||||
try {
|
||||
backupPrivateKey = await BackupPrivateKey.findOne({
|
||||
user: req.user._id
|
||||
}).select('+encryptedPrivateKey +iv +tag');
|
||||
|
||||
if (!backupPrivateKey) throw new Error('Failed to find backup private key');
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email});
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get backup private key'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
backupPrivateKey
|
||||
});
|
||||
}
|
||||
|
||||
export const resetPassword = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier,
|
||||
} = req.body;
|
||||
|
||||
await User.findByIdAndUpdate(
|
||||
req.user._id.toString(),
|
||||
{
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email});
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get backup private key'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully reset password'
|
||||
});
|
||||
}
|
@ -7,16 +7,10 @@ import {
|
||||
reformatPullSecrets
|
||||
} from '../helpers/secret';
|
||||
import { pushKeys } from '../helpers/key';
|
||||
import { PostHog } from 'posthog-node';
|
||||
import { eventPushSecrets } from '../events';
|
||||
import { EventService } from '../services';
|
||||
import { ENV_SET } from '../variables';
|
||||
import { NODE_ENV, POSTHOG_PROJECT_API_KEY, POSTHOG_HOST } from '../config';
|
||||
|
||||
let client: any;
|
||||
if (NODE_ENV === 'production' && POSTHOG_PROJECT_API_KEY && POSTHOG_HOST) {
|
||||
client = new PostHog(POSTHOG_PROJECT_API_KEY, {
|
||||
host: POSTHOG_HOST
|
||||
});
|
||||
}
|
||||
import { postHogClient } from '../services';
|
||||
|
||||
interface PushSecret {
|
||||
ciphertextKey: string;
|
||||
@ -67,12 +61,12 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
||||
workspaceId,
|
||||
keys
|
||||
});
|
||||
|
||||
if (client) {
|
||||
// capture secrets pushed event in production
|
||||
client.capture({
|
||||
distinctId: req.user.email,
|
||||
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets pushed',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
@ -81,6 +75,14 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId
|
||||
})
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
@ -131,9 +133,9 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
||||
secrets = reformatPullSecrets({ secrets });
|
||||
}
|
||||
|
||||
if (client) {
|
||||
if (postHogClient) {
|
||||
// capture secrets pushed event in production
|
||||
client.capture({
|
||||
postHogClient.capture({
|
||||
distinctId: req.user.email,
|
||||
event: 'secrets pulled',
|
||||
properties: {
|
||||
@ -198,9 +200,9 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||
workspace: req.serviceToken.workspace
|
||||
};
|
||||
|
||||
if (client) {
|
||||
// capture secrets pushed event in production
|
||||
client.capture({
|
||||
if (postHogClient) {
|
||||
// capture secrets pulled event in production
|
||||
postHogClient.capture({
|
||||
distinctId: req.serviceToken.user.email,
|
||||
event: 'secrets pulled',
|
||||
properties: {
|
||||
|
5
backend/src/events/index.ts
Normal file
5
backend/src/events/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { eventPushSecrets } from "./secret"
|
||||
|
||||
export {
|
||||
eventPushSecrets
|
||||
}
|
37
backend/src/events/secret.ts
Normal file
37
backend/src/events/secret.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { EVENT_PUSH_SECRETS } from '../variables';
|
||||
|
||||
interface PushSecret {
|
||||
ciphertextKey: string;
|
||||
ivKey: string;
|
||||
tagKey: string;
|
||||
hashKey: string;
|
||||
ciphertextValue: string;
|
||||
ivValue: string;
|
||||
tagValue: string;
|
||||
hashValue: string;
|
||||
type: 'shared' | 'personal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return event for pushing secrets
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace to push secrets to
|
||||
* @returns
|
||||
*/
|
||||
const eventPushSecrets = ({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) => {
|
||||
return ({
|
||||
name: EVENT_PUSH_SECRETS,
|
||||
workspaceId,
|
||||
payload: {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
eventPushSecrets
|
||||
}
|
230
backend/src/helpers/bot.ts
Normal file
230
backend/src/helpers/bot.ts
Normal file
@ -0,0 +1,230 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Bot,
|
||||
BotKey,
|
||||
Secret,
|
||||
ISecret,
|
||||
IUser
|
||||
} from '../models';
|
||||
import {
|
||||
generateKeyPair,
|
||||
encryptSymmetric,
|
||||
decryptSymmetric,
|
||||
decryptAsymmetric
|
||||
} from '../utils/crypto';
|
||||
import { decryptSecrets } from '../helpers/secret';
|
||||
import { ENCRYPTION_KEY } from '../config';
|
||||
import { SECRET_SHARED } from '../variables';
|
||||
|
||||
/**
|
||||
* Create an inactive bot with name [name] for workspace with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.name - name of bot
|
||||
* @param {String} obj.workspaceId - id of workspace that bot belongs to
|
||||
*/
|
||||
const createBot = async ({
|
||||
name,
|
||||
workspaceId,
|
||||
}: {
|
||||
name: string;
|
||||
workspaceId: string;
|
||||
}) => {
|
||||
let bot;
|
||||
try {
|
||||
const { publicKey, privateKey } = generateKeyPair();
|
||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||
plaintext: privateKey,
|
||||
key: ENCRYPTION_KEY
|
||||
});
|
||||
|
||||
bot = await new Bot({
|
||||
name,
|
||||
workspace: workspaceId,
|
||||
isActive: false,
|
||||
publicKey,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
iv,
|
||||
tag
|
||||
}).save();
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create bot');
|
||||
}
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return decrypted secrets for workspace with id [workspaceId]
|
||||
* and [environment] using bot
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.environment - environment
|
||||
*/
|
||||
const getSecretsHelper = async ({
|
||||
workspaceId,
|
||||
environment
|
||||
}: {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
}) => {
|
||||
let content = {} as any;
|
||||
try {
|
||||
const key = await getKey({ workspaceId });
|
||||
const secrets = await Secret.find({
|
||||
workspaceId,
|
||||
environment,
|
||||
type: SECRET_SHARED
|
||||
});
|
||||
|
||||
secrets.forEach((secret: ISecret) => {
|
||||
const secretKey = decryptSymmetric({
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
tag: secret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
|
||||
const secretValue = decryptSymmetric({
|
||||
ciphertext: secret.secretValueCiphertext,
|
||||
iv: secret.secretValueIV,
|
||||
tag: secret.secretValueTag,
|
||||
key
|
||||
});
|
||||
|
||||
content[secretKey] = secretValue;
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get secrets');
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return bot's copy of the workspace key for workspace
|
||||
* with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @returns {String} key - decrypted workspace key
|
||||
*/
|
||||
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
|
||||
let key;
|
||||
try {
|
||||
const botKey = await BotKey.findOne({
|
||||
workspace: workspaceId
|
||||
}).populate<{ sender: IUser }>('sender', 'publicKey');
|
||||
|
||||
if (!botKey) throw new Error('Failed to find bot key');
|
||||
|
||||
const bot = await Bot.findOne({
|
||||
workspace: workspaceId
|
||||
}).select('+encryptedPrivateKey +iv +tag');
|
||||
|
||||
if (!bot) throw new Error('Failed to find bot');
|
||||
if (!bot.isActive) throw new Error('Bot is not active');
|
||||
|
||||
const privateKeyBot = decryptSymmetric({
|
||||
ciphertext: bot.encryptedPrivateKey,
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
key: ENCRYPTION_KEY
|
||||
});
|
||||
|
||||
key = decryptAsymmetric({
|
||||
ciphertext: botKey.encryptedKey,
|
||||
nonce: botKey.nonce,
|
||||
publicKey: botKey.sender.publicKey as string,
|
||||
privateKey: privateKeyBot
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get workspace key');
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return symmetrically encrypted [plaintext] using the
|
||||
* key for workspace with id [workspaceId]
|
||||
* @param {Object} obj1
|
||||
* @param {String} obj1.workspaceId - id of workspace
|
||||
* @param {String} obj1.plaintext - plaintext to encrypt
|
||||
*/
|
||||
const encryptSymmetricHelper = async ({
|
||||
workspaceId,
|
||||
plaintext
|
||||
}: {
|
||||
workspaceId: string;
|
||||
plaintext: string;
|
||||
}) => {
|
||||
|
||||
try {
|
||||
const key = await getKey({ workspaceId });
|
||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||
plaintext,
|
||||
key
|
||||
});
|
||||
|
||||
return ({
|
||||
ciphertext,
|
||||
iv,
|
||||
tag
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to perform symmetric encryption with bot');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Return symmetrically decrypted [ciphertext] using the
|
||||
* key for workspace with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||
* @param {String} obj.iv - iv
|
||||
* @param {String} obj.tag - tag
|
||||
*/
|
||||
const decryptSymmetricHelper = async ({
|
||||
workspaceId,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag
|
||||
}: {
|
||||
workspaceId: string;
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}) => {
|
||||
let plaintext;
|
||||
try {
|
||||
const key = await getKey({ workspaceId });
|
||||
const plaintext = decryptSymmetric({
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
key
|
||||
});
|
||||
|
||||
return plaintext;
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to perform symmetric decryption with bot');
|
||||
}
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
export {
|
||||
createBot,
|
||||
getSecretsHelper,
|
||||
encryptSymmetricHelper,
|
||||
decryptSymmetricHelper
|
||||
}
|
51
backend/src/helpers/event.ts
Normal file
51
backend/src/helpers/event.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { Bot, IBot } from '../models';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { EVENT_PUSH_SECRETS } from '../variables';
|
||||
import { IntegrationService } from '../services';
|
||||
|
||||
interface Event {
|
||||
name: string;
|
||||
workspaceId: string;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle event [event]
|
||||
* @param {Object} obj
|
||||
* @param {Event} obj.event - an event
|
||||
* @param {String} obj.event.name - name of event
|
||||
* @param {String} obj.event.workspaceId - id of workspace that event is part of
|
||||
* @param {Object} obj.event.payload - payload of event (depends on event)
|
||||
*/
|
||||
const handleEventHelper = async ({
|
||||
event
|
||||
}: {
|
||||
event: Event;
|
||||
}) => {
|
||||
const { workspaceId } = event;
|
||||
|
||||
// TODO: moduralize bot check into separate function
|
||||
const bot = await Bot.findOne({
|
||||
workspace: workspaceId,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!bot) return;
|
||||
|
||||
try {
|
||||
switch (event.name) {
|
||||
case EVENT_PUSH_SECRETS:
|
||||
IntegrationService.syncIntegrations({
|
||||
workspaceId
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
handleEventHelper
|
||||
}
|
@ -0,0 +1,350 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Bot,
|
||||
Integration,
|
||||
IIntegration,
|
||||
IntegrationAuth,
|
||||
IIntegrationAuth
|
||||
} from '../models';
|
||||
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
|
||||
import { BotService, IntegrationService } from '../services';
|
||||
import {
|
||||
ENV_DEV,
|
||||
EVENT_PUSH_SECRETS,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY
|
||||
} from '../variables';
|
||||
|
||||
interface Update {
|
||||
workspace: string;
|
||||
integration: string;
|
||||
teamId?: string;
|
||||
accountId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
|
||||
* named [integration]
|
||||
* - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
|
||||
* - Add placeholder inactive integration
|
||||
* - Create bot sequence for integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.integration - name of integration
|
||||
* @param {String} obj.code - code
|
||||
*/
|
||||
const handleOAuthExchangeHelper = async ({
|
||||
workspaceId,
|
||||
integration,
|
||||
code
|
||||
}: {
|
||||
workspaceId: string;
|
||||
integration: string;
|
||||
code: string;
|
||||
}) => {
|
||||
let action;
|
||||
let integrationAuth;
|
||||
try {
|
||||
const bot = await Bot.findOne({
|
||||
workspace: workspaceId,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
|
||||
|
||||
// exchange code for access and refresh tokens
|
||||
let res = await exchangeCode({
|
||||
integration,
|
||||
code
|
||||
});
|
||||
|
||||
let update: Update = {
|
||||
workspace: workspaceId,
|
||||
integration
|
||||
}
|
||||
|
||||
switch (integration) {
|
||||
case INTEGRATION_VERCEL:
|
||||
update.teamId = res.teamId;
|
||||
break;
|
||||
case INTEGRATION_NETLIFY:
|
||||
update.accountId = res.accountId;
|
||||
break;
|
||||
}
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||
workspace: workspaceId,
|
||||
integration
|
||||
}, update, {
|
||||
new: true,
|
||||
upsert: true
|
||||
});
|
||||
|
||||
if (res.refreshToken) {
|
||||
// case: refresh token returned from exchange
|
||||
// set integration auth refresh token
|
||||
await setIntegrationAuthRefreshHelper({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
refreshToken: res.refreshToken
|
||||
});
|
||||
}
|
||||
|
||||
if (res.accessToken) {
|
||||
// case: access token returned from exchange
|
||||
// set integration auth access token
|
||||
await setIntegrationAuthAccessHelper({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
accessToken: res.accessToken,
|
||||
accessExpiresAt: res.accessExpiresAt
|
||||
});
|
||||
}
|
||||
|
||||
// initialize new integration after exchange
|
||||
await new Integration({
|
||||
workspace: workspaceId,
|
||||
environment: ENV_DEV,
|
||||
isActive: false,
|
||||
app: null,
|
||||
integration,
|
||||
integrationAuth: integrationAuth._id
|
||||
}).save();
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to handle OAuth2 code-token exchange')
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Sync/push environment variables in workspace with id [workspaceId] to
|
||||
* all active integrations for that workspace
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.workspaceId - id of workspace
|
||||
*/
|
||||
const syncIntegrationsHelper = async ({
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) => {
|
||||
let integrations;
|
||||
try {
|
||||
|
||||
integrations = await Integration.find({
|
||||
workspace: workspaceId,
|
||||
isActive: true,
|
||||
app: { $ne: null }
|
||||
});
|
||||
|
||||
// for each workspace integration, sync/push secrets
|
||||
// to that integration
|
||||
for await (const integration of integrations) {
|
||||
// get workspace, environment (shared) secrets
|
||||
const secrets = await BotService.getSecrets({
|
||||
workspaceId: integration.workspace.toString(),
|
||||
environment: integration.environment
|
||||
});
|
||||
|
||||
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
// get integration auth access token
|
||||
const accessToken = await getIntegrationAuthAccessHelper({
|
||||
integrationAuthId: integration.integrationAuth.toString()
|
||||
});
|
||||
|
||||
// sync secrets to integration
|
||||
await syncSecrets({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessToken
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to integrations');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return decrypted refresh token using the bot's copy
|
||||
* of the workspace key for workspace belonging to integration auth
|
||||
* with id [integrationAuthId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @param {String} refreshToken - decrypted refresh token
|
||||
*/
|
||||
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
|
||||
let refreshToken;
|
||||
try {
|
||||
const integrationAuth = await IntegrationAuth
|
||||
.findById(integrationAuthId)
|
||||
.select('+refreshCiphertext +refreshIV +refreshTag');
|
||||
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
refreshToken = await BotService.decryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
ciphertext: integrationAuth.refreshCiphertext as string,
|
||||
iv: integrationAuth.refreshIV as string,
|
||||
tag: integrationAuth.refreshTag as string
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get integration refresh token');
|
||||
}
|
||||
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return decrypted access token using the bot's copy
|
||||
* of the workspace key for workspace belonging to integration auth
|
||||
* with id [integrationAuthId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @returns {String} accessToken - decrypted access token
|
||||
*/
|
||||
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
|
||||
let accessToken;
|
||||
try {
|
||||
const integrationAuth = await IntegrationAuth
|
||||
.findById(integrationAuthId)
|
||||
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext');
|
||||
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
accessToken = await BotService.decryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
ciphertext: integrationAuth.accessCiphertext as string,
|
||||
iv: integrationAuth.accessIV as string,
|
||||
tag: integrationAuth.accessTag as string
|
||||
});
|
||||
|
||||
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
|
||||
// there is a access token expiration date
|
||||
// and refresh token to exchange with the OAuth2 server
|
||||
|
||||
if (integrationAuth.accessExpiresAt < new Date()) {
|
||||
// access token is expired
|
||||
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
|
||||
accessToken = await exchangeRefresh({
|
||||
integration: integrationAuth.integration,
|
||||
refreshToken
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get integration access token');
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt refresh token [refreshToken] using the bot's copy
|
||||
* of the workspace key for workspace belonging to integration auth
|
||||
* with id [integrationAuthId] and store it
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @param {String} obj.refreshToken - refresh token
|
||||
*/
|
||||
const setIntegrationAuthRefreshHelper = async ({
|
||||
integrationAuthId,
|
||||
refreshToken
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
|
||||
let integrationAuth;
|
||||
try {
|
||||
integrationAuth = await IntegrationAuth
|
||||
.findById(integrationAuthId);
|
||||
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
const obj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
plaintext: refreshToken
|
||||
});
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||
_id: integrationAuthId
|
||||
}, {
|
||||
refreshCiphertext: obj.ciphertext,
|
||||
refreshIV: obj.iv,
|
||||
refreshTag: obj.tag
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to set integration auth refresh token');
|
||||
}
|
||||
|
||||
return integrationAuth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt access token [accessToken] using the bot's copy
|
||||
* of the workspace key for workspace belonging to integration auth
|
||||
* with id [integrationAuthId] and store it along with [accessExpiresAt]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @param {String} obj.accessToken - access token
|
||||
* @param {Date} obj.accessExpiresAt - expiration date of access token
|
||||
*/
|
||||
const setIntegrationAuthAccessHelper = async ({
|
||||
integrationAuthId,
|
||||
accessToken,
|
||||
accessExpiresAt
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
accessToken: string;
|
||||
accessExpiresAt: Date;
|
||||
}) => {
|
||||
let integrationAuth;
|
||||
try {
|
||||
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
||||
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
const obj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
plaintext: accessToken
|
||||
});
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||
_id: integrationAuthId
|
||||
}, {
|
||||
accessCiphertext: obj.ciphertext,
|
||||
accessIV: obj.iv,
|
||||
accessTag: obj.tag,
|
||||
accessExpiresAt
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to save integration auth access token');
|
||||
}
|
||||
|
||||
return integrationAuth;
|
||||
}
|
||||
|
||||
export {
|
||||
handleOAuthExchangeHelper,
|
||||
syncIntegrationsHelper,
|
||||
getIntegrationAuthRefreshHelper,
|
||||
getIntegrationAuthAccessHelper,
|
||||
setIntegrationAuthRefreshHelper,
|
||||
setIntegrationAuthAccessHelper
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import axios from 'axios';
|
||||
import { IntegrationAuth } from '../models';
|
||||
import { encryptSymmetric, decryptSymmetric } from '../utils/crypto';
|
||||
import { IIntegrationAuth } from '../models';
|
||||
import {
|
||||
ENCRYPTION_KEY,
|
||||
OAUTH_CLIENT_SECRET_HEROKU,
|
||||
OAUTH_TOKEN_URL_HEROKU
|
||||
} from '../config';
|
||||
|
||||
/**
|
||||
* Process token exchange and refresh responses from respective OAuth2 authorization servers by
|
||||
* encrypting access and refresh tokens, computing new access token expiration times [accessExpiresAt],
|
||||
* and upserting them into the DB for workspace with id [workspaceId] and integration [integration].
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.integration - name of integration (e.g. heroku)
|
||||
* @param {Object} obj.res - response from OAuth2 authorization server
|
||||
*/
|
||||
const processOAuthTokenRes = async ({
|
||||
workspaceId,
|
||||
integration,
|
||||
res
|
||||
}: {
|
||||
workspaceId: string;
|
||||
integration: string;
|
||||
res: any;
|
||||
}): Promise<IIntegrationAuth> => {
|
||||
let integrationAuth;
|
||||
try {
|
||||
// encrypt refresh + access tokens
|
||||
const {
|
||||
ciphertext: refreshCiphertext,
|
||||
iv: refreshIV,
|
||||
tag: refreshTag
|
||||
} = encryptSymmetric({
|
||||
plaintext: res.data.refresh_token,
|
||||
key: ENCRYPTION_KEY
|
||||
});
|
||||
|
||||
const {
|
||||
ciphertext: accessCiphertext,
|
||||
iv: accessIV,
|
||||
tag: accessTag
|
||||
} = encryptSymmetric({
|
||||
plaintext: res.data.access_token,
|
||||
key: ENCRYPTION_KEY
|
||||
});
|
||||
|
||||
// compute access token expiration date
|
||||
const accessExpiresAt = new Date();
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + res.data.expires_in
|
||||
);
|
||||
|
||||
// create or replace integration authorization with encrypted tokens
|
||||
// and access token expiration date
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate(
|
||||
{ workspace: workspaceId, integration },
|
||||
{
|
||||
workspace: workspaceId,
|
||||
integration,
|
||||
refreshCiphertext,
|
||||
refreshIV,
|
||||
refreshTag,
|
||||
accessCiphertext,
|
||||
accessIV,
|
||||
accessTag,
|
||||
accessExpiresAt
|
||||
},
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error(
|
||||
'Failed to process OAuth2 authorization server token response'
|
||||
);
|
||||
}
|
||||
|
||||
return integrationAuth;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return access token for integration either by decrypting a non-expired access token [accessCiphertext] on
|
||||
* the integration authorization document or by requesting a new one by decrypting and exchanging the
|
||||
* refresh token [refreshCiphertext] with the respective OAuth2 authorization server.
|
||||
* @param {Object} obj
|
||||
* @param {IIntegrationAuth} obj.integrationAuth - an integration authorization document
|
||||
* @returns {String} access token - new access token
|
||||
*/
|
||||
const getOAuthAccessToken = async ({
|
||||
integrationAuth
|
||||
}: {
|
||||
integrationAuth: IIntegrationAuth;
|
||||
}) => {
|
||||
let accessToken;
|
||||
try {
|
||||
const {
|
||||
refreshCiphertext,
|
||||
refreshIV,
|
||||
refreshTag,
|
||||
accessCiphertext,
|
||||
accessIV,
|
||||
accessTag,
|
||||
accessExpiresAt
|
||||
} = integrationAuth;
|
||||
|
||||
if (
|
||||
refreshCiphertext &&
|
||||
refreshIV &&
|
||||
refreshTag &&
|
||||
accessCiphertext &&
|
||||
accessIV &&
|
||||
accessTag &&
|
||||
accessExpiresAt
|
||||
) {
|
||||
if (accessExpiresAt < new Date()) {
|
||||
// case: access token expired
|
||||
// TODO: fetch another access token
|
||||
|
||||
let clientSecret;
|
||||
switch (integrationAuth.integration) {
|
||||
case 'heroku':
|
||||
clientSecret = OAUTH_CLIENT_SECRET_HEROKU;
|
||||
}
|
||||
|
||||
// record new access token and refresh token
|
||||
// encrypt refresh + access tokens
|
||||
const refreshToken = decryptSymmetric({
|
||||
ciphertext: refreshCiphertext,
|
||||
iv: refreshIV,
|
||||
tag: refreshTag,
|
||||
key: ENCRYPTION_KEY
|
||||
});
|
||||
|
||||
// TODO: make route compatible with other integration types
|
||||
const res = await axios.post(
|
||||
OAUTH_TOKEN_URL_HEROKU, // maybe shouldn't be a config variable?
|
||||
new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
client_secret: clientSecret
|
||||
} as any)
|
||||
);
|
||||
|
||||
accessToken = res.data.access_token;
|
||||
|
||||
await processOAuthTokenRes({
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
integration: integrationAuth.integration,
|
||||
res
|
||||
});
|
||||
} else {
|
||||
// case: access token still works
|
||||
accessToken = decryptSymmetric({
|
||||
ciphertext: accessCiphertext,
|
||||
iv: accessIV,
|
||||
tag: accessTag,
|
||||
key: ENCRYPTION_KEY
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get OAuth2 access token');
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
};
|
||||
|
||||
export { processOAuthTokenRes, getOAuthAccessToken };
|
||||
|
@ -1,6 +1,51 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Membership, Key } from '../models';
|
||||
|
||||
/**
|
||||
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
|
||||
* and has at least one of the roles in [acceptedRoles] and statuses in [acceptedStatuses]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.userId - id of user to validate
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
*/
|
||||
const validateMembership = async ({
|
||||
userId,
|
||||
workspaceId,
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
acceptedRoles: string[];
|
||||
acceptedStatuses: string[];
|
||||
}) => {
|
||||
|
||||
let membership;
|
||||
try {
|
||||
membership = await Membership.findOne({
|
||||
user: userId,
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
if (!membership) throw new Error('Failed to find membership');
|
||||
|
||||
if (!acceptedRoles.includes(membership.role)) {
|
||||
throw new Error('Failed to validate membership role');
|
||||
}
|
||||
|
||||
if (!acceptedStatuses.includes(membership.status)) {
|
||||
throw new Error('Failed to validate membership status');
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to validate membership');
|
||||
}
|
||||
|
||||
return membership;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return membership matching criteria specified in query [queryObj]
|
||||
* @param {Object} queryObj - query object
|
||||
@ -97,4 +142,9 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
||||
return deletedMembership;
|
||||
};
|
||||
|
||||
export { addMemberships, findMembership, deleteMembership };
|
||||
export {
|
||||
validateMembership,
|
||||
addMemberships,
|
||||
findMembership,
|
||||
deleteMembership
|
||||
};
|
||||
|
@ -2,21 +2,40 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import handlebars from 'handlebars';
|
||||
import nodemailer from 'nodemailer';
|
||||
import { SMTP_HOST, SMTP_NAME, SMTP_USERNAME, SMTP_PASSWORD } from '../config';
|
||||
import {
|
||||
SMTP_HOST,
|
||||
SMTP_PORT,
|
||||
SMTP_NAME,
|
||||
SMTP_USERNAME,
|
||||
SMTP_PASSWORD
|
||||
} from '../config';
|
||||
import SMTPConnection from 'nodemailer/lib/smtp-connection';
|
||||
import * as Sentry from '@sentry/node';
|
||||
|
||||
const mailOpts: SMTPConnection.Options = {
|
||||
host: SMTP_HOST,
|
||||
port: SMTP_PORT as number
|
||||
};
|
||||
if (SMTP_USERNAME && SMTP_PASSWORD) {
|
||||
mailOpts.auth = {
|
||||
user: SMTP_USERNAME,
|
||||
pass: SMTP_PASSWORD
|
||||
};
|
||||
}
|
||||
// create nodemailer transporter
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: SMTP_HOST,
|
||||
port: 587,
|
||||
auth: {
|
||||
user: SMTP_USERNAME,
|
||||
pass: SMTP_PASSWORD
|
||||
}
|
||||
});
|
||||
const transporter = nodemailer.createTransport(mailOpts);
|
||||
transporter
|
||||
.verify()
|
||||
.then(() => console.log('SMTP - Successfully connected'))
|
||||
.catch((err) => console.log('SMTP - Failed to connect'));
|
||||
.verify()
|
||||
.then(() => {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureMessage('SMTP - Successfully connected');
|
||||
})
|
||||
.catch((err) => {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(
|
||||
`SMTP - Failed to connect to ${SMTP_HOST}:${SMTP_PORT} \n\t${err}`
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {Object} obj
|
||||
@ -26,33 +45,34 @@ transporter
|
||||
* @param {Object} obj.substitutions - object containing template substitutions
|
||||
*/
|
||||
const sendMail = async ({
|
||||
template,
|
||||
subjectLine,
|
||||
recipients,
|
||||
substitutions
|
||||
template,
|
||||
subjectLine,
|
||||
recipients,
|
||||
substitutions
|
||||
}: {
|
||||
template: string;
|
||||
subjectLine: string;
|
||||
recipients: string[];
|
||||
substitutions: any;
|
||||
template: string;
|
||||
subjectLine: string;
|
||||
recipients: string[];
|
||||
substitutions: any;
|
||||
}) => {
|
||||
try {
|
||||
const html = fs.readFileSync(
|
||||
path.resolve(__dirname, '../templates/' + template),
|
||||
'utf8'
|
||||
);
|
||||
const temp = handlebars.compile(html);
|
||||
const htmlToSend = temp(substitutions);
|
||||
try {
|
||||
const html = fs.readFileSync(
|
||||
path.resolve(__dirname, '../templates/' + template),
|
||||
'utf8'
|
||||
);
|
||||
const temp = handlebars.compile(html);
|
||||
const htmlToSend = temp(substitutions);
|
||||
|
||||
await transporter.sendMail({
|
||||
from: `"${SMTP_NAME}" <${SMTP_USERNAME}>`,
|
||||
to: recipients.join(', '),
|
||||
subject: subjectLine,
|
||||
html: htmlToSend
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
await transporter.sendMail({
|
||||
from: `"${SMTP_NAME}" <${SMTP_USERNAME}>`,
|
||||
to: recipients.join(', '),
|
||||
subject: subjectLine,
|
||||
html: htmlToSend
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
};
|
||||
|
||||
export { sendMail };
|
||||
|
@ -2,34 +2,35 @@ import rateLimit from 'express-rate-limit';
|
||||
|
||||
// 300 requests per 15 minutes
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 300,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 400,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skip: (request) => request.path === '/healthcheck'
|
||||
});
|
||||
|
||||
// 5 requests per hour
|
||||
const signupLimiter = rateLimit({
|
||||
windowMs: 60 * 60 * 1000,
|
||||
max: 5,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false
|
||||
windowMs: 60 * 60 * 1000,
|
||||
max: 10,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false
|
||||
});
|
||||
|
||||
// 10 requests per hour
|
||||
const loginLimiter = rateLimit({
|
||||
windowMs: 60 * 60 * 1000,
|
||||
max: 10,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false
|
||||
windowMs: 60 * 60 * 1000,
|
||||
max: 20,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false
|
||||
});
|
||||
|
||||
// 5 requests per hour
|
||||
const passwordLimiter = rateLimit({
|
||||
windowMs: 60 * 60 * 1000,
|
||||
max: 5,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false
|
||||
windowMs: 60 * 60 * 1000,
|
||||
max: 10,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false
|
||||
});
|
||||
|
||||
export { apiLimiter, signupLimiter, loginLimiter, passwordLimiter };
|
||||
|
@ -33,7 +33,7 @@ const sendEmailVerification = async ({ email }: { email: string }) => {
|
||||
// send mail
|
||||
await sendMail({
|
||||
template: 'emailVerification.handlebars',
|
||||
subjectLine: 'Infisical workspace invitation',
|
||||
subjectLine: 'Infisical confirmation code',
|
||||
recipients: [email],
|
||||
substitutions: {
|
||||
code: token
|
||||
@ -66,7 +66,7 @@ const checkEmailVerification = async ({
|
||||
email,
|
||||
token: code
|
||||
});
|
||||
|
||||
|
||||
if (!token) throw new Error('Failed to find email verification token');
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
@ -106,7 +106,7 @@ const initializeDefaultOrg = async ({
|
||||
|
||||
// initialize a default workspace inside the new organization
|
||||
const workspace = await createWorkspace({
|
||||
name: `${user.firstName}'s Project`,
|
||||
name: `Example Project`,
|
||||
organizationId: organization._id.toString()
|
||||
});
|
||||
|
||||
|
@ -1,13 +1,16 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Workspace,
|
||||
Bot,
|
||||
Membership,
|
||||
Key,
|
||||
Secret
|
||||
} from '../models';
|
||||
import { createBot } from '../helpers/bot';
|
||||
|
||||
/**
|
||||
* Create a workspace with name [name] in organization with id [organizationId]
|
||||
* and a bot for it.
|
||||
* @param {String} name - name of workspace to create.
|
||||
* @param {String} organizationId - id of organization to create workspace in
|
||||
* @param {Object} workspace - new workspace
|
||||
@ -21,10 +24,16 @@ const createWorkspace = async ({
|
||||
}) => {
|
||||
let workspace;
|
||||
try {
|
||||
// create workspace
|
||||
workspace = await new Workspace({
|
||||
name,
|
||||
organization: organizationId
|
||||
}).save();
|
||||
|
||||
const bot = await createBot({
|
||||
name: 'Infisical Bot',
|
||||
workspaceId: workspace._id.toString()
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
@ -43,6 +52,9 @@ const createWorkspace = async ({
|
||||
const deleteWorkspace = async ({ id }: { id: string }) => {
|
||||
try {
|
||||
await Workspace.deleteOne({ _id: id });
|
||||
await Bot.deleteOne({
|
||||
workspace: id
|
||||
});
|
||||
await Membership.deleteMany({
|
||||
workspace: id
|
||||
});
|
||||
|
@ -1,26 +1,31 @@
|
||||
/* eslint-disable no-console */
|
||||
import http from 'http';
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import cors from 'cors';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import mongoose from 'mongoose';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { PORT, SENTRY_DSN, NODE_ENV, MONGO_URL, WEBSITE_URL } from './config';
|
||||
import { PORT, SENTRY_DSN, NODE_ENV, MONGO_URL, SITE_URL } from './config';
|
||||
import { apiLimiter } from './helpers/rateLimiter';
|
||||
import { createTerminus } from '@godaddy/terminus';
|
||||
|
||||
const app = express();
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
tracesSampleRate: 1.0,
|
||||
debug: NODE_ENV === 'production' ? false : true,
|
||||
environment: NODE_ENV
|
||||
dsn: SENTRY_DSN,
|
||||
tracesSampleRate: 1.0,
|
||||
debug: NODE_ENV === 'production' ? false : true,
|
||||
environment: NODE_ENV
|
||||
});
|
||||
|
||||
import {
|
||||
signup as signupRouter,
|
||||
auth as authRouter,
|
||||
bot as botRouter,
|
||||
organization as organizationRouter,
|
||||
workspace as workspaceRouter,
|
||||
membershipOrg as membershipOrgRouter,
|
||||
@ -38,31 +43,35 @@ import {
|
||||
} from './routes';
|
||||
|
||||
const connectWithRetry = () => {
|
||||
mongoose.connect(MONGO_URL)
|
||||
.then(() => console.log('Successfully connected to DB'))
|
||||
.catch((e) => {
|
||||
console.log('Failed to connect to DB ', e);
|
||||
setTimeout(() => {
|
||||
console.log(e);
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
mongoose
|
||||
.connect(MONGO_URL)
|
||||
.then(() => console.log('Successfully connected to DB'))
|
||||
.catch((e) => {
|
||||
console.log('Failed to connect to DB ', e);
|
||||
setTimeout(() => {
|
||||
console.log(e);
|
||||
}, 5000);
|
||||
});
|
||||
return mongoose.connection;
|
||||
};
|
||||
|
||||
connectWithRetry();
|
||||
const dbConnection = connectWithRetry();
|
||||
|
||||
app.enable('trust proxy');
|
||||
app.use(cookieParser());
|
||||
app.use(cors({
|
||||
credentials: true,
|
||||
origin: WEBSITE_URL
|
||||
}));
|
||||
app.use(
|
||||
cors({
|
||||
credentials: true,
|
||||
origin: SITE_URL
|
||||
})
|
||||
);
|
||||
|
||||
if (NODE_ENV === 'production') {
|
||||
// enable app-wide rate-limiting + helmet security
|
||||
// in production
|
||||
app.disable('x-powered-by');
|
||||
app.use(apiLimiter);
|
||||
app.use(helmet());
|
||||
// enable app-wide rate-limiting + helmet security
|
||||
// in production
|
||||
app.disable('x-powered-by');
|
||||
app.use(apiLimiter);
|
||||
app.use(helmet());
|
||||
}
|
||||
|
||||
app.use(express.json());
|
||||
@ -70,6 +79,7 @@ app.use(express.json());
|
||||
// routers
|
||||
app.use('/api/v1/signup', signupRouter);
|
||||
app.use('/api/v1/auth', authRouter);
|
||||
app.use('/api/v1/bot', botRouter);
|
||||
app.use('/api/v1/user', userRouter);
|
||||
app.use('/api/v1/user-action', userActionRouter);
|
||||
app.use('/api/v1/organization', organizationRouter);
|
||||
@ -85,6 +95,35 @@ app.use('/api/v1/stripe', stripeRouter);
|
||||
app.use('/api/v1/integration', integrationRouter);
|
||||
app.use('/api/v1/integration-auth', integrationAuthRouter);
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log('Listening on PORT ' + PORT);
|
||||
const server = http.createServer(app);
|
||||
|
||||
const onSignal = () => {
|
||||
console.log('Server is starting clean-up');
|
||||
return Promise.all([
|
||||
() => {
|
||||
dbConnection.close(() => {
|
||||
console.info('Database connection closed');
|
||||
});
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
const healthCheck = () => {
|
||||
// `state.isShuttingDown` (boolean) shows whether the server is shutting down or not
|
||||
return Promise
|
||||
.resolve
|
||||
// optionally include a resolve value to be included as
|
||||
// info in the health check response
|
||||
();
|
||||
};
|
||||
|
||||
createTerminus(server, {
|
||||
healthChecks: {
|
||||
'/healthcheck': healthCheck,
|
||||
onSignal
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log('Listening on PORT ' + PORT);
|
||||
});
|
||||
|
169
backend/src/integrations/apps.ts
Normal file
169
backend/src/integrations/apps.ts
Normal file
@ -0,0 +1,169 @@
|
||||
import axios from 'axios';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
IIntegrationAuth
|
||||
} from '../models';
|
||||
import {
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL
|
||||
} from '../variables';
|
||||
|
||||
/**
|
||||
* Return list of names of apps for integration named [integration]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integration - name of integration
|
||||
* @param {String} obj.accessToken - access token for integration
|
||||
* @returns {Object[]} apps - names of integration apps
|
||||
* @returns {String} apps.name - name of integration app
|
||||
*/
|
||||
const getApps = async ({
|
||||
integrationAuth,
|
||||
accessToken
|
||||
}: {
|
||||
integrationAuth: IIntegrationAuth;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
|
||||
interface App {
|
||||
name: string;
|
||||
siteId?: string;
|
||||
}
|
||||
|
||||
let apps: App[]; // TODO: add type and define payloads for apps
|
||||
try {
|
||||
switch (integrationAuth.integration) {
|
||||
case INTEGRATION_HEROKU:
|
||||
apps = await getAppsHeroku({
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_VERCEL:
|
||||
apps = await getAppsVercel({
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_NETLIFY:
|
||||
apps = await getAppsNetlify({
|
||||
integrationAuth,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get integration apps');
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of names of apps for Heroku integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.accessToken - access token for Heroku API
|
||||
* @returns {Object[]} apps - names of Heroku apps
|
||||
* @returns {String} apps.name - name of Heroku app
|
||||
*/
|
||||
const getAppsHeroku = async ({
|
||||
accessToken
|
||||
}: {
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let apps;
|
||||
try {
|
||||
const res = (await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
|
||||
headers: {
|
||||
Accept: 'application/vnd.heroku+json; version=3',
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
})).data;
|
||||
|
||||
apps = res.map((a: any) => ({
|
||||
name: a.name
|
||||
}));
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get Heroku integration apps');
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of names of apps for Vercel integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.accessToken - access token for Vercel API
|
||||
* @returns {Object[]} apps - names of Vercel apps
|
||||
* @returns {String} apps.name - name of Vercel app
|
||||
*/
|
||||
const getAppsVercel = async ({
|
||||
accessToken
|
||||
}: {
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let apps;
|
||||
try {
|
||||
const res = (await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
})).data;
|
||||
|
||||
apps = res.projects.map((a: any) => ({
|
||||
name: a.name
|
||||
}));
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get Vercel integration apps');
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of names of sites for Netlify integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.accessToken - access token for Netlify API
|
||||
* @returns {Object[]} apps - names of Netlify sites
|
||||
* @returns {String} apps.name - name of Netlify site
|
||||
*/
|
||||
const getAppsNetlify = async ({
|
||||
integrationAuth,
|
||||
accessToken
|
||||
}: {
|
||||
integrationAuth: IIntegrationAuth;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let apps;
|
||||
try {
|
||||
const res = (await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
})).data;
|
||||
|
||||
apps = res.map((a: any) => ({
|
||||
name: a.name,
|
||||
siteId: a.site_id
|
||||
}));
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get Netlify integration apps');
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
export {
|
||||
getApps
|
||||
}
|
241
backend/src/integrations/exchange.ts
Normal file
241
backend/src/integrations/exchange.ts
Normal file
@ -0,0 +1,241 @@
|
||||
import axios from 'axios';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
ACTION_PUSH_TO_HEROKU
|
||||
} from '../variables';
|
||||
import {
|
||||
SITE_URL,
|
||||
CLIENT_SECRET_HEROKU,
|
||||
CLIENT_ID_VERCEL,
|
||||
CLIENT_ID_NETLIFY,
|
||||
CLIENT_SECRET_VERCEL,
|
||||
CLIENT_SECRET_NETLIFY
|
||||
} from '../config';
|
||||
|
||||
interface ExchangeCodeHerokuResponse {
|
||||
token_type: string;
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
user_id: string;
|
||||
session_nonce?: string;
|
||||
}
|
||||
|
||||
interface ExchangeCodeVercelResponse {
|
||||
token_type: string;
|
||||
access_token: string;
|
||||
installation_id: string;
|
||||
user_id: string;
|
||||
team_id?: string;
|
||||
}
|
||||
|
||||
interface ExchangeCodeNetlifyResponse {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
refresh_token: string;
|
||||
scope: string;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2
|
||||
* code-token exchange for integration named [integration]
|
||||
* @param {Object} obj1
|
||||
* @param {String} obj1.integration - name of integration
|
||||
* @param {String} obj1.code - code for code-token exchange
|
||||
* @returns {Object} obj
|
||||
* @returns {String} obj.accessToken - access token for integration
|
||||
* @returns {String} obj.refreshToken - refresh token for integration
|
||||
* @returns {Date} obj.accessExpiresAt - date of expiration for access token
|
||||
* @returns {String} obj.action - integration action for bot sequence
|
||||
*/
|
||||
const exchangeCode = async ({
|
||||
integration,
|
||||
code
|
||||
}: {
|
||||
integration: string;
|
||||
code: string;
|
||||
}) => {
|
||||
let obj = {} as any;
|
||||
|
||||
try {
|
||||
switch (integration) {
|
||||
case INTEGRATION_HEROKU:
|
||||
obj = await exchangeCodeHeroku({
|
||||
code
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_VERCEL:
|
||||
obj = await exchangeCodeVercel({
|
||||
code
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_NETLIFY:
|
||||
obj = await exchangeCodeNetlify({
|
||||
code
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange');
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
|
||||
* OAuth2 code-token exchange
|
||||
* @param {Object} obj1
|
||||
* @param {Object} obj1.code - code for code-token exchange
|
||||
* @returns {Object} obj2
|
||||
* @returns {String} obj2.accessToken - access token for Heroku API
|
||||
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||
*/
|
||||
const exchangeCodeHeroku = async ({
|
||||
code
|
||||
}: {
|
||||
code: string;
|
||||
}) => {
|
||||
let res: ExchangeCodeHerokuResponse;
|
||||
let accessExpiresAt = new Date();
|
||||
try {
|
||||
res = (await axios.post(
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
client_secret: CLIENT_SECRET_HEROKU
|
||||
} as any)
|
||||
)).data;
|
||||
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + res.expires_in
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange with Heroku');
|
||||
}
|
||||
|
||||
return ({
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
accessExpiresAt
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
|
||||
* code-token exchange
|
||||
* @param {Object} obj1
|
||||
* @param {Object} obj1.code - code for code-token exchange
|
||||
* @returns {Object} obj2
|
||||
* @returns {String} obj2.accessToken - access token for Heroku API
|
||||
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||
*/
|
||||
const exchangeCodeVercel = async ({
|
||||
code
|
||||
}: {
|
||||
code: string;
|
||||
}) => {
|
||||
let res: ExchangeCodeVercelResponse;
|
||||
try {
|
||||
res = (await axios.post(
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
code: code,
|
||||
client_id: CLIENT_ID_VERCEL,
|
||||
client_secret: CLIENT_SECRET_VERCEL,
|
||||
redirect_uri: `${SITE_URL}/vercel`
|
||||
} as any)
|
||||
)).data;
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange with Vercel');
|
||||
}
|
||||
|
||||
return ({
|
||||
accessToken: res.access_token,
|
||||
refreshToken: null,
|
||||
accessExpiresAt: null,
|
||||
teamId: res.team_id
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
|
||||
* code-token exchange
|
||||
* @param {Object} obj1
|
||||
* @param {Object} obj1.code - code for code-token exchange
|
||||
* @returns {Object} obj2
|
||||
* @returns {String} obj2.accessToken - access token for Heroku API
|
||||
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||
*/
|
||||
const exchangeCodeNetlify = async ({
|
||||
code
|
||||
}: {
|
||||
code: string;
|
||||
}) => {
|
||||
let res: ExchangeCodeNetlifyResponse;
|
||||
let accountId;
|
||||
try {
|
||||
res = (await axios.post(
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
client_id: CLIENT_ID_NETLIFY,
|
||||
client_secret: CLIENT_SECRET_NETLIFY,
|
||||
redirect_uri: `${SITE_URL}/netlify`
|
||||
} as any)
|
||||
)).data;
|
||||
|
||||
const res2 = await axios.get(
|
||||
'https://api.netlify.com/api/v1/sites',
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${res.access_token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const res3 = (await axios.get(
|
||||
'https://api.netlify.com/api/v1/accounts',
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${res.access_token}`
|
||||
}
|
||||
}
|
||||
)).data;
|
||||
|
||||
accountId = res3[0].id;
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange with Netlify');
|
||||
}
|
||||
|
||||
return ({
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
accountId
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
exchangeCode
|
||||
}
|
13
backend/src/integrations/index.ts
Normal file
13
backend/src/integrations/index.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { exchangeCode } from './exchange';
|
||||
import { exchangeRefresh } from './refresh';
|
||||
import { getApps } from './apps';
|
||||
import { syncSecrets } from './sync';
|
||||
import { revokeAccess } from './revoke';
|
||||
|
||||
export {
|
||||
exchangeCode,
|
||||
exchangeRefresh,
|
||||
getApps,
|
||||
syncSecrets,
|
||||
revokeAccess
|
||||
}
|
78
backend/src/integrations/refresh.ts
Normal file
78
backend/src/integrations/refresh.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import axios from 'axios';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { INTEGRATION_HEROKU } from '../variables';
|
||||
import {
|
||||
CLIENT_SECRET_HEROKU
|
||||
} from '../config';
|
||||
import {
|
||||
INTEGRATION_HEROKU_TOKEN_URL
|
||||
} from '../variables';
|
||||
|
||||
/**
|
||||
* Return new access token by exchanging refresh token [refreshToken] for integration
|
||||
* named [integration]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integration - name of integration
|
||||
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
|
||||
*/
|
||||
const exchangeRefresh = async ({
|
||||
integration,
|
||||
refreshToken
|
||||
}: {
|
||||
integration: string;
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
let accessToken;
|
||||
try {
|
||||
switch (integration) {
|
||||
case INTEGRATION_HEROKU:
|
||||
accessToken = await exchangeRefreshHeroku({
|
||||
refreshToken
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get new OAuth2 access token');
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||
* Heroku integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
|
||||
* @returns
|
||||
*/
|
||||
const exchangeRefreshHeroku = async ({
|
||||
refreshToken
|
||||
}: {
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
let accessToken;
|
||||
try {
|
||||
const res = await axios.post(
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
client_secret: CLIENT_SECRET_HEROKU
|
||||
} as any)
|
||||
);
|
||||
|
||||
accessToken = res.data.access_token;
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get new OAuth2 access token for Heroku');
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
export {
|
||||
exchangeRefresh
|
||||
}
|
50
backend/src/integrations/revoke.ts
Normal file
50
backend/src/integrations/revoke.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import axios from 'axios';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
IIntegrationAuth,
|
||||
IntegrationAuth,
|
||||
Integration
|
||||
} from '../models';
|
||||
import {
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY
|
||||
} from '../variables';
|
||||
|
||||
const revokeAccess = async ({
|
||||
integrationAuth,
|
||||
accessToken
|
||||
}: {
|
||||
integrationAuth: IIntegrationAuth,
|
||||
accessToken: String
|
||||
}) => {
|
||||
try {
|
||||
// add any integration-specific revocation logic
|
||||
switch (integrationAuth.integration) {
|
||||
case INTEGRATION_HEROKU:
|
||||
break;
|
||||
case INTEGRATION_VERCEL:
|
||||
break;
|
||||
case INTEGRATION_NETLIFY:
|
||||
break;
|
||||
}
|
||||
|
||||
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
||||
_id: integrationAuth._id
|
||||
});
|
||||
|
||||
if (deletedIntegrationAuth) {
|
||||
await Integration.deleteMany({
|
||||
integrationAuth: deletedIntegrationAuth._id
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to delete integration authorization');
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
revokeAccess
|
||||
}
|
598
backend/src/integrations/sync.ts
Normal file
598
backend/src/integrations/sync.ts
Normal file
@ -0,0 +1,598 @@
|
||||
import axios from 'axios';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
IIntegration, IIntegrationAuth
|
||||
} from '../models';
|
||||
import {
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL
|
||||
} from '../variables';
|
||||
|
||||
// TODO: need a helper function in the future to handle integration
|
||||
// envar priorities (i.e. prioritize secrets within integration or those on Infisical)
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to [app] in integration named [integration]
|
||||
* @param {Object} obj
|
||||
* @param {IIntegration} obj.integration - integration details
|
||||
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
|
||||
* @param {Object} obj.app - app in integration
|
||||
* @param {Object} obj.target - (optional) target (environment) in integration
|
||||
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||
* @param {String} obj.accessToken - access token for integration
|
||||
*/
|
||||
const syncSecrets = async ({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessToken,
|
||||
}: {
|
||||
integration: IIntegration;
|
||||
integrationAuth: IIntegrationAuth;
|
||||
secrets: any;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
try {
|
||||
switch (integration.integration) {
|
||||
case INTEGRATION_HEROKU:
|
||||
await syncSecretsHeroku({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_VERCEL:
|
||||
await syncSecretsVercel({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_NETLIFY:
|
||||
await syncSecretsNetlify({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO: set integration to inactive if it was not synced correctly (send alert?)
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to integration');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Heroku [app]
|
||||
* @param {Object} obj
|
||||
* @param {IIntegration} obj.integration - integration details
|
||||
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||
*/
|
||||
const syncSecretsHeroku = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
}: {
|
||||
integration: IIntegration,
|
||||
secrets: any;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
try {
|
||||
const herokuSecrets = (await axios.get(
|
||||
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/vnd.heroku+json; version=3',
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
)).data;
|
||||
|
||||
Object.keys(herokuSecrets).forEach(key => {
|
||||
if (!(key in secrets)) {
|
||||
secrets[key] = null;
|
||||
}
|
||||
});
|
||||
|
||||
await axios.patch(
|
||||
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
|
||||
secrets,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/vnd.heroku+json; version=3',
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to Heroku');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Heroku [app]
|
||||
* @param {Object} obj
|
||||
* @param {IIntegration} obj.integration - integration details
|
||||
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||
*/
|
||||
const syncSecretsVercel = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
}: {
|
||||
integration: IIntegration,
|
||||
secrets: any;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
|
||||
interface VercelSecret {
|
||||
id?: string;
|
||||
type: string;
|
||||
key: string;
|
||||
value: string;
|
||||
target: string[];
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all (decrypted) secrets back from Vercel in
|
||||
// decrypted format
|
||||
const params = new URLSearchParams({
|
||||
decrypt: "true"
|
||||
});
|
||||
|
||||
const res = (await Promise.all((await axios.get(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
))
|
||||
.data
|
||||
.envs
|
||||
.filter((secret: VercelSecret) => secret.target.includes(integration.target))
|
||||
.map(async (secret: VercelSecret) => (await axios.get(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
}
|
||||
)).data)
|
||||
)).reduce((obj: any, secret: any) => ({
|
||||
...obj,
|
||||
[secret.key]: secret
|
||||
}), {});
|
||||
|
||||
const updateSecrets: VercelSecret[] = [];
|
||||
const deleteSecrets: VercelSecret[] = [];
|
||||
const newSecrets: VercelSecret[] = [];
|
||||
|
||||
// Identify secrets to create
|
||||
Object.keys(secrets).map((key) => {
|
||||
if (!(key in res)) {
|
||||
// case: secret has been created
|
||||
newSecrets.push({
|
||||
key: key,
|
||||
value: secrets[key],
|
||||
type: 'encrypted',
|
||||
target: [integration.target]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Identify secrets to update and delete
|
||||
Object.keys(res).map((key) => {
|
||||
if (key in secrets) {
|
||||
if (res[key].value !== secrets[key]) {
|
||||
// case: secret value has changed
|
||||
updateSecrets.push({
|
||||
id: res[key].id,
|
||||
key: key,
|
||||
value: secrets[key],
|
||||
type: 'encrypted',
|
||||
target: [integration.target]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// case: secret has been deleted
|
||||
deleteSecrets.push({
|
||||
id: res[key].id,
|
||||
key: key,
|
||||
value: res[key].value,
|
||||
type: 'encrypted',
|
||||
target: [integration.target],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Sync/push new secrets
|
||||
if (newSecrets.length > 0) {
|
||||
await axios.post(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`,
|
||||
newSecrets,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Sync/push updated secrets
|
||||
if (updateSecrets.length > 0) {
|
||||
updateSecrets.forEach(async (secret: VercelSecret) => {
|
||||
const {
|
||||
id,
|
||||
...updatedSecret
|
||||
} = secret;
|
||||
await axios.patch(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||
updatedSecret,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Delete secrets
|
||||
if (deleteSecrets.length > 0) {
|
||||
deleteSecrets.forEach(async (secret: VercelSecret) => {
|
||||
await axios.delete(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to Vercel');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Netlify site [app]
|
||||
* @param {Object} obj
|
||||
* @param {IIntegration} obj.integration - integration details
|
||||
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
|
||||
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||
*/
|
||||
const syncSecretsNetlify = async ({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessToken
|
||||
}: {
|
||||
integration: IIntegration;
|
||||
integrationAuth: IIntegrationAuth;
|
||||
secrets: any;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
// TODO: Netlify revision in progress
|
||||
// try {
|
||||
// const getParams = new URLSearchParams({
|
||||
// context_name: integration.context,
|
||||
// site_id: integration.siteId
|
||||
// });
|
||||
|
||||
// const res = (await axios.get(
|
||||
// `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||
// {
|
||||
// params: getParams,
|
||||
// headers: {
|
||||
// Authorization: `Bearer ${accessToken}`
|
||||
// }
|
||||
// }
|
||||
// ))
|
||||
// .data
|
||||
// .reduce((obj: any, secret: any) => ({
|
||||
// ...obj,
|
||||
// [secret.key]: secret
|
||||
// }), {});
|
||||
|
||||
// res.forEach((r: any) => console.log(r));
|
||||
// console.log('getParams', getParams);
|
||||
|
||||
// interface UpdateNetlifySecret {
|
||||
// key: string;
|
||||
// context: string;
|
||||
// value: string;
|
||||
// }
|
||||
|
||||
// interface DeleteNetlifySecret {
|
||||
// key: string;
|
||||
// }
|
||||
|
||||
// interface NewNetlifySecretValue {
|
||||
// value: string;
|
||||
// context: string;
|
||||
// }
|
||||
|
||||
// interface NewNetlifySecret {
|
||||
// key: string;
|
||||
// values: NewNetlifySecretValue[];
|
||||
// }
|
||||
|
||||
// let updateSecrets: UpdateNetlifySecret[] = [];
|
||||
// let deleteSecrets: DeleteNetlifySecret[] = [];
|
||||
// let newSecrets: NewNetlifySecret[] = [];
|
||||
|
||||
// interface NetlifyValue {
|
||||
// id: string;
|
||||
// value: string;
|
||||
// context: string;
|
||||
// role: string;
|
||||
// }
|
||||
|
||||
// // Identify secrets to create - GOOD
|
||||
// Object.keys(secrets).map((key) => {
|
||||
// if (!(key in res)) {
|
||||
// // case: secret does not exist in Netlify -> create secret
|
||||
// newSecrets.push({
|
||||
// key: key,
|
||||
// values: [{
|
||||
// value: secrets[key],
|
||||
// context: integration.context
|
||||
// }]
|
||||
// });
|
||||
// } else {
|
||||
// // case: secret exists in Netlify
|
||||
|
||||
// const netlifyContextsSet = new Set (res[key].values.map((netlifyValue: NetlifyValue) => netlifyValue.context));
|
||||
|
||||
// // TODO: check context/env.
|
||||
// res[key].values.forEach((netlifyValue: NetlifyValue) => {
|
||||
// if (netlifyValue.context === integration.context) {
|
||||
// // case: Netlify value context matches integration context
|
||||
// // TODO: check if value has changed
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
// // Identify secrets to update and delete
|
||||
// Object.keys(res).map((key) => {
|
||||
// if (key in secrets) {
|
||||
// // if (res[key] !== secrets[key]) {
|
||||
// // // case: secret value has changed
|
||||
// // updateSecrets.push({
|
||||
// // key: key,
|
||||
// // context: integration.context,
|
||||
// // value: secrets[key]
|
||||
// // });
|
||||
// // }
|
||||
|
||||
// // TODO: modify check and record of updated secrets
|
||||
|
||||
// // case 1: new context added.
|
||||
// } else {
|
||||
// // case: secret has been deleted
|
||||
// deleteSecrets.push({
|
||||
// key
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
// const syncParams = new URLSearchParams({
|
||||
// site_id: integration.siteId
|
||||
// });
|
||||
|
||||
// console.log('Netlify newSecrets', newSecrets);
|
||||
// newSecrets.forEach(secret => console.log(secret.values));
|
||||
// // Sync/push new secrets
|
||||
// if (newSecrets.length > 0) {
|
||||
// await axios.post(
|
||||
// `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||
// newSecrets,
|
||||
// {
|
||||
// params: syncParams,
|
||||
// headers: {
|
||||
// Authorization: `Bearer ${accessToken}`
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
|
||||
// console.log('Netlify updateSecrets', updateSecrets);
|
||||
// // Sync/push updated secrets
|
||||
// if (updateSecrets.length > 0) {
|
||||
|
||||
// updateSecrets.forEach(async (secret: UpdateNetlifySecret) => {
|
||||
// await axios.patch(
|
||||
// `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
|
||||
// {
|
||||
// context: secret.context,
|
||||
// value: secret.value
|
||||
// },
|
||||
// {
|
||||
// params: syncParams,
|
||||
// headers: {
|
||||
// Authorization: `Bearer ${accessToken}`
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
|
||||
// console.log('Netlify deleteSecrets', deleteSecrets);
|
||||
// // Delete secrets
|
||||
// if (deleteSecrets.length > 0) {
|
||||
// deleteSecrets.forEach(async (secret: DeleteNetlifySecret) => {
|
||||
// await axios.delete(
|
||||
// `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
|
||||
// {
|
||||
// params: syncParams,
|
||||
// headers: {
|
||||
// Authorization: `Bearer ${accessToken}`
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
// } catch (err) {
|
||||
// Sentry.setUser(null);
|
||||
// Sentry.captureException(err);
|
||||
// throw new Error('Failed to sync secrets to Heroku');
|
||||
// }
|
||||
|
||||
try {
|
||||
const getParams = new URLSearchParams({
|
||||
context_name: integration.context,
|
||||
site_id: integration.siteId
|
||||
});
|
||||
|
||||
const res = (await axios.get(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||
{
|
||||
params: getParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
))
|
||||
.data
|
||||
.reduce((obj: any, secret: any) => ({
|
||||
...obj,
|
||||
[secret.key]: secret.values[0].value
|
||||
}), {});
|
||||
|
||||
interface UpdateNetlifySecret {
|
||||
key: string;
|
||||
context: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface DeleteNetlifySecret {
|
||||
key: string;
|
||||
}
|
||||
|
||||
interface NewNetlifySecretValue {
|
||||
value: string;
|
||||
context: string;
|
||||
}
|
||||
|
||||
interface NewNetlifySecret {
|
||||
key: string;
|
||||
values: NewNetlifySecretValue[];
|
||||
}
|
||||
|
||||
const updateSecrets: UpdateNetlifySecret[] = [];
|
||||
const deleteSecrets: DeleteNetlifySecret[] = [];
|
||||
const newSecrets: NewNetlifySecret[] = [];
|
||||
|
||||
// Identify secrets to create
|
||||
Object.keys(secrets).map((key) => {
|
||||
if (!(key in res)) {
|
||||
// case: secret has been created
|
||||
newSecrets.push({
|
||||
key: key,
|
||||
values: [{
|
||||
value: secrets[key], // include id?
|
||||
context: integration.context
|
||||
}]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Identify secrets to update and delete
|
||||
Object.keys(res).map((key) => {
|
||||
if (key in secrets) {
|
||||
if (res[key] !== secrets[key]) {
|
||||
// case: secret value has changed
|
||||
updateSecrets.push({
|
||||
key: key,
|
||||
context: integration.context,
|
||||
value: secrets[key]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// case: secret has been deleted
|
||||
deleteSecrets.push({
|
||||
key
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const syncParams = new URLSearchParams({
|
||||
site_id: integration.siteId
|
||||
});
|
||||
|
||||
// Sync/push new secrets
|
||||
if (newSecrets.length > 0) {
|
||||
await axios.post(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||
newSecrets,
|
||||
{
|
||||
params: syncParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Sync/push updated secrets
|
||||
if (updateSecrets.length > 0) {
|
||||
|
||||
updateSecrets.forEach(async (secret: UpdateNetlifySecret) => {
|
||||
await axios.patch(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
|
||||
{
|
||||
context: secret.context,
|
||||
value: secret.value
|
||||
},
|
||||
{
|
||||
params: syncParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Delete secrets
|
||||
if (deleteSecrets.length > 0) {
|
||||
deleteSecrets.forEach(async (secret: DeleteNetlifySecret) => {
|
||||
await axios.delete(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
|
||||
{
|
||||
params: syncParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to Heroku');
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
syncSecrets
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
{
|
||||
"heroku": {
|
||||
"name": "Heroku",
|
||||
"type": "oauth2",
|
||||
"clientId": "bc132901-935a-4590-b010-f1857efc380d",
|
||||
"docsLink": ""
|
||||
},
|
||||
"netlify": {
|
||||
"name": "Netlify",
|
||||
"type": "oauth2",
|
||||
"clientId": "",
|
||||
"docsLink": ""
|
||||
},
|
||||
"digitalocean": {
|
||||
"name": "Digital Ocean",
|
||||
"type": "oauth2",
|
||||
"clientId": "",
|
||||
"docsLink": ""
|
||||
},
|
||||
"gcp": {
|
||||
"name": "Google Cloud Platform",
|
||||
"type": "oauth2",
|
||||
"clientId": "",
|
||||
"docsLink": ""
|
||||
},
|
||||
"aws": {
|
||||
"name": "Amazon Web Services",
|
||||
"type": "oauth2",
|
||||
"clientId": "",
|
||||
"docsLink": ""
|
||||
},
|
||||
"azure": {
|
||||
"name": "Microsoft Azure",
|
||||
"type": "oauth2",
|
||||
"clientId": "",
|
||||
"docsLink": ""
|
||||
},
|
||||
"travisci": {
|
||||
"name": "Travis CI",
|
||||
"type": "oauth2",
|
||||
"clientId": "",
|
||||
"docsLink": ""
|
||||
},
|
||||
"circleci": {
|
||||
"name": "Circle CI",
|
||||
"type": "oauth2",
|
||||
"clientId": "",
|
||||
"docsLink": ""
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import requireAuth from './requireAuth';
|
||||
import requireBotAuth from './requireBotAuth';
|
||||
import requireSignupAuth from './requireSignupAuth';
|
||||
import requireWorkspaceAuth from './requireWorkspaceAuth';
|
||||
import requireOrganizationAuth from './requireOrganizationAuth';
|
||||
@ -9,6 +10,7 @@ import validateRequest from './validateRequest';
|
||||
|
||||
export {
|
||||
requireAuth,
|
||||
requireBotAuth,
|
||||
requireSignupAuth,
|
||||
requireWorkspaceAuth,
|
||||
requireOrganizationAuth,
|
||||
|
45
backend/src/middleware/requireBotAuth.ts
Normal file
45
backend/src/middleware/requireBotAuth.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Bot } from '../models';
|
||||
import { validateMembership } from '../helpers/membership';
|
||||
|
||||
type req = 'params' | 'body' | 'query';
|
||||
|
||||
const requireBotAuth = ({
|
||||
acceptedRoles,
|
||||
acceptedStatuses,
|
||||
location = 'params'
|
||||
}: {
|
||||
acceptedRoles: string[];
|
||||
acceptedStatuses: string[];
|
||||
location?: req;
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const bot = await Bot.findOne({ _id: req[location].botId });
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Failed to find bot');
|
||||
}
|
||||
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: bot.workspace.toString(),
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
});
|
||||
|
||||
req.bot = bot;
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(401).send({
|
||||
error: 'Failed bot authorization'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default requireBotAuth;
|
@ -1,7 +1,8 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Integration, IntegrationAuth, Membership } from '../models';
|
||||
import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
||||
import { Bot, Integration, IntegrationAuth, Membership } from '../models';
|
||||
import { IntegrationService } from '../services';
|
||||
import { validateMembership } from '../helpers/membership';
|
||||
|
||||
/**
|
||||
* Validate if user on request is a member of workspace with proper roles associated
|
||||
@ -31,24 +32,14 @@ const requireIntegrationAuth = ({
|
||||
if (!integration) {
|
||||
throw new Error('Failed to find integration');
|
||||
}
|
||||
|
||||
const membership = await Membership.findOne({
|
||||
user: req.user._id,
|
||||
workspace: integration.workspace
|
||||
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: integration.workspace.toString(),
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new Error('Failed to find integration workspace membership');
|
||||
}
|
||||
|
||||
if (!acceptedRoles.includes(membership.role)) {
|
||||
throw new Error('Failed to validate workspace membership role');
|
||||
}
|
||||
|
||||
if (!acceptedStatuses.includes(membership.status)) {
|
||||
throw new Error('Failed to validate workspace membership status');
|
||||
}
|
||||
|
||||
const integrationAuth = await IntegrationAuth.findOne({
|
||||
_id: integration.integrationAuth
|
||||
}).select(
|
||||
@ -60,7 +51,9 @@ const requireIntegrationAuth = ({
|
||||
}
|
||||
|
||||
req.integration = integration;
|
||||
req.accessToken = await getOAuthAccessToken({ integrationAuth });
|
||||
req.accessToken = await IntegrationService.getIntegrationAuthAccess({
|
||||
integrationAuthId: integrationAuth._id.toString()
|
||||
});
|
||||
|
||||
return next();
|
||||
} catch (err) {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { IntegrationAuth, Membership } from '../models';
|
||||
import { decryptSymmetric } from '../utils/crypto';
|
||||
import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
||||
import { IntegrationAuth } from '../models';
|
||||
import { IntegrationService } from '../services';
|
||||
import { validateMembership } from '../helpers/membership';
|
||||
|
||||
/**
|
||||
* Validate if user on request is a member of workspace with proper roles associated
|
||||
@ -10,18 +10,18 @@ import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
||||
* @param {Object} obj
|
||||
* @param {String[]} obj.acceptedRoles - accepted workspace roles
|
||||
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses
|
||||
* @param {Boolean} obj.attachRefresh - whether or not to decrypt and attach integration authorization refresh token onto request
|
||||
* @param {Boolean} obj.attachAccessToken - whether or not to decrypt and attach integration authorization access token onto request
|
||||
*/
|
||||
const requireIntegrationAuthorizationAuth = ({
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
acceptedStatuses,
|
||||
attachAccessToken = true
|
||||
}: {
|
||||
acceptedRoles: string[];
|
||||
acceptedStatuses: string[];
|
||||
attachAccessToken?: boolean;
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
// (authorization) integration authorization middleware
|
||||
|
||||
try {
|
||||
const { integrationAuthId } = req.params;
|
||||
|
||||
@ -34,30 +34,21 @@ const requireIntegrationAuthorizationAuth = ({
|
||||
if (!integrationAuth) {
|
||||
throw new Error('Failed to find integration authorization');
|
||||
}
|
||||
|
||||
const membership = await Membership.findOne({
|
||||
user: req.user._id,
|
||||
workspace: integrationAuth.workspace
|
||||
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new Error(
|
||||
'Failed to find integration authorization workspace membership'
|
||||
);
|
||||
}
|
||||
|
||||
if (!acceptedRoles.includes(membership.role)) {
|
||||
throw new Error('Failed to validate workspace membership role');
|
||||
}
|
||||
|
||||
if (!acceptedStatuses.includes(membership.status)) {
|
||||
throw new Error('Failed to validate workspace membership status');
|
||||
}
|
||||
|
||||
req.integrationAuth = integrationAuth;
|
||||
|
||||
// TODO: make compatible with other integration types since they won't necessarily have access tokens
|
||||
req.accessToken = await getOAuthAccessToken({ integrationAuth });
|
||||
if (attachAccessToken) {
|
||||
req.accessToken = await IntegrationService.getIntegrationAuthAccess({
|
||||
integrationAuthId: integrationAuth._id.toString()
|
||||
});
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Membership, IWorkspace } from '../models';
|
||||
import { validateMembership } from '../helpers/membership';
|
||||
|
||||
type req = 'params' | 'body' | 'query';
|
||||
|
||||
@ -25,24 +25,12 @@ const requireWorkspaceAuth = ({
|
||||
// workspace authorization middleware
|
||||
|
||||
try {
|
||||
// validate workspace membership
|
||||
|
||||
const membership = await Membership.findOne({
|
||||
user: req.user._id,
|
||||
workspace: req[location].workspaceId
|
||||
}).populate<{ workspace: IWorkspace }>('workspace');
|
||||
|
||||
if (!membership) {
|
||||
throw new Error('Failed to find workspace membership');
|
||||
}
|
||||
|
||||
if (!acceptedRoles.includes(membership.role)) {
|
||||
throw new Error('Failed to validate workspace membership role');
|
||||
}
|
||||
|
||||
if (!acceptedStatuses.includes(membership.status)) {
|
||||
throw new Error('Failed to validate workspace membership status');
|
||||
}
|
||||
const membership = await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: req[location].workspaceId,
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
});
|
||||
|
||||
req.membership = membership;
|
||||
|
||||
|
57
backend/src/models/bot.ts
Normal file
57
backend/src/models/bot.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
|
||||
export interface IBot {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
workspace: Types.ObjectId;
|
||||
isActive: boolean;
|
||||
publicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
const botSchema = new Schema<IBot>(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false
|
||||
},
|
||||
publicKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
encryptedPrivateKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
select: false
|
||||
},
|
||||
iv: {
|
||||
type: String,
|
||||
required: true,
|
||||
select: false
|
||||
},
|
||||
tag: {
|
||||
type: String,
|
||||
required: true,
|
||||
select: false
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const Bot = model<IBot>('Bot', botSchema);
|
||||
|
||||
export default Bot;
|
45
backend/src/models/botKey.ts
Normal file
45
backend/src/models/botKey.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
|
||||
export interface IBotKey {
|
||||
_id: Types.ObjectId;
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
sender: Types.ObjectId;
|
||||
bot: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
}
|
||||
|
||||
const botKeySchema = new Schema<IBotKey>(
|
||||
{
|
||||
encryptedKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
nonce: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
sender: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
bot: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Bot',
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const BotKey = model<IBotKey>('BotKey', botKeySchema);
|
||||
|
||||
export default BotKey;
|
@ -1,4 +1,6 @@
|
||||
import BackupPrivateKey, { IBackupPrivateKey } from './backupPrivateKey';
|
||||
import Bot, { IBot } from './bot';
|
||||
import BotKey, { IBotKey } from './botKey';
|
||||
import IncidentContactOrg, { IIncidentContactOrg } from './incidentContactOrg';
|
||||
import Integration, { IIntegration } from './integration';
|
||||
import IntegrationAuth, { IIntegrationAuth } from './integrationAuth';
|
||||
@ -16,6 +18,10 @@ import Workspace, { IWorkspace } from './workspace';
|
||||
export {
|
||||
BackupPrivateKey,
|
||||
IBackupPrivateKey,
|
||||
Bot,
|
||||
IBot,
|
||||
BotKey,
|
||||
IBotKey,
|
||||
IncidentContactOrg,
|
||||
IIncidentContactOrg,
|
||||
Integration,
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
ENV_STAGING,
|
||||
ENV_PROD,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY
|
||||
} from '../variables';
|
||||
|
||||
@ -14,7 +15,10 @@ export interface IIntegration {
|
||||
environment: 'dev' | 'test' | 'staging' | 'prod';
|
||||
isActive: boolean;
|
||||
app: string;
|
||||
integration: 'heroku' | 'netlify';
|
||||
target: string;
|
||||
context: string;
|
||||
siteId: string;
|
||||
integration: 'heroku' | 'vercel' | 'netlify';
|
||||
integrationAuth: Types.ObjectId;
|
||||
}
|
||||
|
||||
@ -34,15 +38,29 @@ const integrationSchema = new Schema<IIntegration>(
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
app: {
|
||||
// name of app in provider
|
||||
app: { // name of app in provider
|
||||
type: String,
|
||||
default: null,
|
||||
required: true
|
||||
default: null
|
||||
},
|
||||
target: { // vercel-specific target (environment)
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
context: { // netlify-specific context (deploy)
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
siteId: { // netlify-specific site (app) id
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
integration: {
|
||||
type: String,
|
||||
enum: [INTEGRATION_HEROKU, INTEGRATION_NETLIFY],
|
||||
enum: [
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY
|
||||
],
|
||||
required: true
|
||||
},
|
||||
integrationAuth: {
|
||||
|
@ -1,10 +1,16 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
import { INTEGRATION_HEROKU, INTEGRATION_NETLIFY } from '../variables';
|
||||
import {
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY
|
||||
} from '../variables';
|
||||
|
||||
export interface IIntegrationAuth {
|
||||
_id: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
integration: 'heroku' | 'netlify';
|
||||
integration: 'heroku' | 'vercel' | 'netlify';
|
||||
teamId: string;
|
||||
accountId: string;
|
||||
refreshCiphertext?: string;
|
||||
refreshIV?: string;
|
||||
refreshTag?: string;
|
||||
@ -22,9 +28,19 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
||||
},
|
||||
integration: {
|
||||
type: String,
|
||||
enum: [INTEGRATION_HEROKU, INTEGRATION_NETLIFY],
|
||||
enum: [
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY
|
||||
],
|
||||
required: true
|
||||
},
|
||||
teamId: { // vercel-specific integration param
|
||||
type: String
|
||||
},
|
||||
accountId: { // netlify-specific integration param
|
||||
type: String
|
||||
},
|
||||
refreshCiphertext: {
|
||||
type: String,
|
||||
select: false
|
||||
|
@ -2,25 +2,30 @@ import { Schema, model } from 'mongoose';
|
||||
import { EMAIL_TOKEN_LIFETIME } from '../config';
|
||||
|
||||
export interface IToken {
|
||||
email: String;
|
||||
token: String;
|
||||
createdAt: Date;
|
||||
email: string;
|
||||
token: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
const tokenSchema = new Schema<IToken>({
|
||||
email: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
token: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
expires: EMAIL_TOKEN_LIFETIME,
|
||||
default: Date.now
|
||||
}
|
||||
email: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
token: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
});
|
||||
|
||||
tokenSchema.index({
|
||||
createdAt: 1
|
||||
}, {
|
||||
expireAfterSeconds: parseInt(EMAIL_TOKEN_LIFETIME)
|
||||
});
|
||||
|
||||
const Token = model<IToken>('Token', tokenSchema);
|
||||
|
@ -5,28 +5,24 @@ import { requireAuth, validateRequest } from '../middleware';
|
||||
import { authController } from '../controllers';
|
||||
import { loginLimiter } from '../helpers/rateLimiter';
|
||||
|
||||
router.post('/token', validateRequest, authController.getNewToken);
|
||||
|
||||
router.post(
|
||||
'/token',
|
||||
validateRequest,
|
||||
authController.getNewToken
|
||||
'/login1',
|
||||
loginLimiter,
|
||||
body('email').exists().trim().notEmpty(),
|
||||
body('clientPublicKey').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
authController.login1
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/login1',
|
||||
// loginLimiter,
|
||||
body('email').exists().trim().notEmpty(),
|
||||
body('clientPublicKey').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
authController.login1
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/login2',
|
||||
// loginLimiter,
|
||||
body('email').exists().trim().notEmpty(),
|
||||
body('clientProof').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
authController.login2
|
||||
'/login2',
|
||||
loginLimiter,
|
||||
body('email').exists().trim().notEmpty(),
|
||||
body('clientProof').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
authController.login2
|
||||
);
|
||||
|
||||
router.post('/logout', requireAuth, authController.logout);
|
||||
|
38
backend/src/routes/bot.ts
Normal file
38
backend/src/routes/bot.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body, param } from 'express-validator';
|
||||
import {
|
||||
requireAuth,
|
||||
requireBotAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
import { botController } from '../controllers';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||
|
||||
router.get(
|
||||
'/:workspaceId',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
}),
|
||||
param('workspaceId').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
botController.getBotByWorkspaceId
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:botId/active',
|
||||
requireAuth,
|
||||
requireBotAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
}),
|
||||
body('isActive').isBoolean(),
|
||||
body('botKey'),
|
||||
validateRequest,
|
||||
botController.setBotActiveState
|
||||
);
|
||||
|
||||
export default router;
|
@ -1,4 +1,5 @@
|
||||
import signup from './signup';
|
||||
import bot from './bot';
|
||||
import auth from './auth';
|
||||
import user from './user';
|
||||
import userAction from './userAction';
|
||||
@ -18,6 +19,7 @@ import integrationAuth from './integrationAuth';
|
||||
export {
|
||||
signup,
|
||||
auth,
|
||||
bot,
|
||||
user,
|
||||
userAction,
|
||||
organization,
|
||||
|
@ -9,22 +9,6 @@ import { ADMIN, MEMBER, GRANTED } from '../variables';
|
||||
import { body, param } from 'express-validator';
|
||||
import { integrationController } from '../controllers';
|
||||
|
||||
router.get('/integrations', requireAuth, integrationController.getIntegrations);
|
||||
|
||||
router.post(
|
||||
'/:integrationId/sync',
|
||||
requireAuth,
|
||||
requireIntegrationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
}),
|
||||
param('integrationId').exists().trim(),
|
||||
body('key').exists(),
|
||||
body('secrets').exists(),
|
||||
validateRequest,
|
||||
integrationController.syncIntegration
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:integrationId',
|
||||
requireAuth,
|
||||
@ -32,10 +16,15 @@ router.patch(
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
}),
|
||||
param('integrationId'),
|
||||
body('update'),
|
||||
param('integrationId').exists().trim(),
|
||||
body('app').exists().trim(),
|
||||
body('environment').exists().trim(),
|
||||
body('isActive').exists().isBoolean(),
|
||||
body('target').exists(),
|
||||
body('context').exists(),
|
||||
body('siteId').exists(),
|
||||
validateRequest,
|
||||
integrationController.modifyIntegration
|
||||
integrationController.updateIntegration
|
||||
);
|
||||
|
||||
router.delete(
|
||||
@ -45,7 +34,7 @@ router.delete(
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
}),
|
||||
param('integrationId'),
|
||||
param('integrationId').exists().trim(),
|
||||
validateRequest,
|
||||
integrationController.deleteIntegration
|
||||
);
|
||||
|
@ -10,6 +10,12 @@ import {
|
||||
import { ADMIN, MEMBER, GRANTED } from '../variables';
|
||||
import { integrationAuthController } from '../controllers';
|
||||
|
||||
router.get(
|
||||
'/integration-options',
|
||||
requireAuth,
|
||||
integrationAuthController.getIntegrationOptions
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/oauth-token',
|
||||
requireAuth,
|
||||
@ -22,7 +28,7 @@ router.post(
|
||||
body('code').exists().trim().notEmpty(),
|
||||
body('integration').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
integrationAuthController.integrationAuthOauthExchange
|
||||
integrationAuthController.oAuthExchange
|
||||
);
|
||||
|
||||
router.get(
|
||||
@ -42,7 +48,8 @@ router.delete(
|
||||
requireAuth,
|
||||
requireIntegrationAuthorizationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
acceptedStatuses: [GRANTED],
|
||||
attachAccessToken: false
|
||||
}),
|
||||
param('integrationAuthId'),
|
||||
validateRequest,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body } from 'express-validator';
|
||||
import { requireAuth, validateRequest } from '../middleware';
|
||||
import { requireAuth, requireSignupAuth, validateRequest } from '../middleware';
|
||||
import { passwordController } from '../controllers';
|
||||
import { passwordLimiter } from '../helpers/rateLimiter';
|
||||
|
||||
@ -27,6 +27,30 @@ router.post(
|
||||
passwordController.changePassword
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/email/password-reset',
|
||||
passwordLimiter,
|
||||
body('email').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
passwordController.emailPasswordReset
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/email/password-reset-verify',
|
||||
passwordLimiter,
|
||||
body('email').exists().trim().notEmpty().isEmail(),
|
||||
body('code').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
passwordController.emailPasswordResetVerify
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/backup-private-key',
|
||||
passwordLimiter,
|
||||
requireSignupAuth,
|
||||
passwordController.getBackupPrivateKey
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/backup-private-key',
|
||||
passwordLimiter,
|
||||
@ -41,4 +65,16 @@ router.post(
|
||||
passwordController.createBackupPrivateKey
|
||||
);
|
||||
|
||||
export default router;
|
||||
router.post(
|
||||
'/password-reset',
|
||||
requireSignupAuth,
|
||||
body('encryptedPrivateKey').exists().trim().notEmpty(), // private key encrypted under new pwd
|
||||
body('iv').exists().trim().notEmpty(), // new iv for private key
|
||||
body('tag').exists().trim().notEmpty(), // new tag for private key
|
||||
body('salt').exists().trim().notEmpty(), // part of new pwd
|
||||
body('verifier').exists().trim().notEmpty(), // part of new pwd
|
||||
validateRequest,
|
||||
passwordController.resetPassword
|
||||
);
|
||||
|
||||
export default router;
|
@ -7,7 +7,7 @@ import { signupLimiter } from '../helpers/rateLimiter';
|
||||
|
||||
router.post(
|
||||
'/email/signup',
|
||||
// signupLimiter,
|
||||
signupLimiter,
|
||||
body('email').exists().trim().notEmpty().isEmail(),
|
||||
validateRequest,
|
||||
signupController.beginEmailSignup
|
||||
@ -15,7 +15,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/email/verify',
|
||||
// signupLimiter,
|
||||
signupLimiter,
|
||||
body('email').exists().trim().notEmpty().isEmail(),
|
||||
body('code').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
@ -24,7 +24,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/complete-account/signup',
|
||||
// signupLimiter,
|
||||
signupLimiter,
|
||||
requireSignupAuth,
|
||||
body('email').exists().trim().notEmpty().isEmail(),
|
||||
body('firstName').exists().trim().notEmpty(),
|
||||
@ -42,7 +42,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/complete-account/invite',
|
||||
// signupLimiter,
|
||||
signupLimiter,
|
||||
requireSignupAuth,
|
||||
body('email').exists().trim().notEmpty().isEmail(),
|
||||
body('firstName').exists().trim().notEmpty(),
|
||||
|
82
backend/src/services/BotService.ts
Normal file
82
backend/src/services/BotService.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import {
|
||||
getSecretsHelper,
|
||||
encryptSymmetricHelper,
|
||||
decryptSymmetricHelper
|
||||
} from '../helpers/bot';
|
||||
|
||||
/**
|
||||
* Class to handle bot actions
|
||||
*/
|
||||
class BotService {
|
||||
|
||||
/**
|
||||
* Return decrypted secrets for workspace with id [workspaceId] and
|
||||
* environment [environmen] shared to bot.
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace of secrets
|
||||
* @param {String} obj.environment - environment for secrets
|
||||
* @returns {Object} secretObj - object where keys are secret keys and values are secret values
|
||||
*/
|
||||
static async getSecrets({
|
||||
workspaceId,
|
||||
environment
|
||||
}: {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
}) {
|
||||
return await getSecretsHelper({
|
||||
workspaceId,
|
||||
environment
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return symmetrically encrypted [plaintext] using the
|
||||
* bot's copy of the workspace key for workspace with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.plaintext - plaintext to encrypt
|
||||
*/
|
||||
static async encryptSymmetric({
|
||||
workspaceId,
|
||||
plaintext
|
||||
}: {
|
||||
workspaceId: string;
|
||||
plaintext: string;
|
||||
}) {
|
||||
return await encryptSymmetricHelper({
|
||||
workspaceId,
|
||||
plaintext
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return symmetrically decrypted [ciphertext] using the
|
||||
* bot's copy of the workspace key for workspace with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||
* @param {String} obj.iv - iv
|
||||
* @param {String} obj.tag - tag
|
||||
*/
|
||||
static async decryptSymmetric({
|
||||
workspaceId,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag
|
||||
}: {
|
||||
workspaceId: string;
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}) {
|
||||
return await decryptSymmetricHelper({
|
||||
workspaceId,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default BotService;
|
30
backend/src/services/EventService.ts
Normal file
30
backend/src/services/EventService.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Bot, IBot } from '../models';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { handleEventHelper } from '../helpers/event';
|
||||
|
||||
interface Event {
|
||||
name: string;
|
||||
workspaceId: string;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle events.
|
||||
*/
|
||||
class EventService {
|
||||
/**
|
||||
* Handle event [event]
|
||||
* @param {Object} obj
|
||||
* @param {Event} obj.event - an event
|
||||
* @param {String} obj.event.name - name of event
|
||||
* @param {String} obj.event.workspaceId - id of workspace that event is part of
|
||||
* @param {Object} obj.event.payload - payload of event (depends on event)
|
||||
*/
|
||||
static async handleEvent({ event }: { event: Event }): Promise<void> {
|
||||
await handleEventHelper({
|
||||
event
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default EventService;
|
145
backend/src/services/IntegrationService.ts
Normal file
145
backend/src/services/IntegrationService.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Integration
|
||||
} from '../models';
|
||||
import {
|
||||
handleOAuthExchangeHelper,
|
||||
syncIntegrationsHelper,
|
||||
getIntegrationAuthRefreshHelper,
|
||||
getIntegrationAuthAccessHelper,
|
||||
setIntegrationAuthRefreshHelper,
|
||||
setIntegrationAuthAccessHelper,
|
||||
} from '../helpers/integration';
|
||||
import { exchangeCode } from '../integrations';
|
||||
import {
|
||||
ENV_DEV,
|
||||
EVENT_PUSH_SECRETS
|
||||
} from '../variables';
|
||||
|
||||
// should sync stuff be here too? Probably.
|
||||
// TODO: move bot functions to IntegrationService.
|
||||
|
||||
/**
|
||||
* Class to handle integrations
|
||||
*/
|
||||
class IntegrationService {
|
||||
|
||||
/**
|
||||
* Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
|
||||
* named [integration]
|
||||
* - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
|
||||
* - Add placeholder inactive integration
|
||||
* - Create bot sequence for integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.integration - name of integration
|
||||
* @param {String} obj.code - code
|
||||
*/
|
||||
static async handleOAuthExchange({
|
||||
workspaceId,
|
||||
integration,
|
||||
code
|
||||
}: {
|
||||
workspaceId: string;
|
||||
integration: string;
|
||||
code: string;
|
||||
}) {
|
||||
await handleOAuthExchangeHelper({
|
||||
workspaceId,
|
||||
integration,
|
||||
code
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync/push environment variables in workspace with id [workspaceId] to
|
||||
* all associated integrations
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.workspaceId - id of workspace
|
||||
*/
|
||||
static async syncIntegrations({
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) {
|
||||
return await syncIntegrationsHelper({
|
||||
workspaceId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return decrypted refresh token for integration auth
|
||||
* with id [integrationAuthId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @param {String} refreshToken - decrypted refresh token
|
||||
*/
|
||||
static async getIntegrationAuthRefresh({ integrationAuthId }: { integrationAuthId: string}) {
|
||||
return await getIntegrationAuthRefreshHelper({
|
||||
integrationAuthId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return decrypted access token for integration auth
|
||||
* with id [integrationAuthId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @param {String} accessToken - decrypted access token
|
||||
*/
|
||||
static async getIntegrationAuthAccess({ integrationAuthId }: { integrationAuthId: string}) {
|
||||
return await getIntegrationAuthAccessHelper({
|
||||
integrationAuthId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt refresh token [refreshToken] using the bot's copy
|
||||
* of the workspace key for workspace belonging to integration auth
|
||||
* with id [integrationAuthId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @param {String} obj.refreshToken - refresh token
|
||||
* @returns {IntegrationAuth} integrationAuth - updated integration auth
|
||||
*/
|
||||
static async setIntegrationAuthRefresh({
|
||||
integrationAuthId,
|
||||
refreshToken
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
refreshToken: string;
|
||||
}) {
|
||||
return await setIntegrationAuthRefreshHelper({
|
||||
integrationAuthId,
|
||||
refreshToken
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt access token [accessToken] using the bot's copy
|
||||
* of the workspace key for workspace belonging to integration auth
|
||||
* with id [integrationAuthId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @param {String} obj.accessToken - access token
|
||||
* @param {String} obj.accessExpiresAt - expiration date of access token
|
||||
* @returns {IntegrationAuth} - updated integration auth
|
||||
*/
|
||||
static async setIntegrationAuthAccess({
|
||||
integrationAuthId,
|
||||
accessToken,
|
||||
accessExpiresAt
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
accessToken: string;
|
||||
accessExpiresAt: Date;
|
||||
}) {
|
||||
return await setIntegrationAuthAccessHelper({
|
||||
integrationAuthId,
|
||||
accessToken,
|
||||
accessExpiresAt
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default IntegrationService;
|
19
backend/src/services/PostHogClient.ts
Normal file
19
backend/src/services/PostHogClient.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { PostHog } from 'posthog-node';
|
||||
import {
|
||||
NODE_ENV,
|
||||
POSTHOG_HOST,
|
||||
POSTHOG_PROJECT_API_KEY,
|
||||
TELEMETRY_ENABLED
|
||||
} from '../config';
|
||||
|
||||
console.log('TELEMETRY_ENABLED: ', TELEMETRY_ENABLED);
|
||||
|
||||
let postHogClient: any;
|
||||
if (NODE_ENV === 'production' && TELEMETRY_ENABLED) {
|
||||
// case: enable opt-out telemetry in production
|
||||
postHogClient = new PostHog(POSTHOG_PROJECT_API_KEY, {
|
||||
host: POSTHOG_HOST
|
||||
});
|
||||
}
|
||||
|
||||
export default postHogClient;
|
11
backend/src/services/index.ts
Normal file
11
backend/src/services/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import postHogClient from './PostHogClient';
|
||||
import BotService from './BotService';
|
||||
import EventService from './EventService';
|
||||
import IntegrationService from './IntegrationService';
|
||||
|
||||
export {
|
||||
postHogClient,
|
||||
BotService,
|
||||
EventService,
|
||||
IntegrationService
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Email Verification</title>
|
||||
<title>Organization Invitation</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
|
15
backend/src/templates/passwordReset.handlebars
Normal file
15
backend/src/templates/passwordReset.handlebars
Normal file
@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Account Recovery</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
<h2>Reset your password</h2>
|
||||
<p>Someone requested a password reset.</p>
|
||||
<a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a>
|
||||
<p>If you didn't initiate this request, please contact us immediately at team@infisical.com</p>
|
||||
</body>
|
||||
</html>
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Email Verification</title>
|
||||
<title>Project Invitation</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
|
1
backend/src/types/express/index.d.ts
vendored
1
backend/src/types/express/index.d.ts
vendored
@ -11,6 +11,7 @@ declare global {
|
||||
membershipOrg: any;
|
||||
integration: any;
|
||||
integrationAuth: any;
|
||||
bot: any;
|
||||
serviceToken: any;
|
||||
accessToken: any;
|
||||
query?: any;
|
||||
|
@ -2,6 +2,21 @@ import nacl from 'tweetnacl';
|
||||
import util from 'tweetnacl-util';
|
||||
import AesGCM from './aes-gcm';
|
||||
|
||||
/**
|
||||
* Return new base64, NaCl, public-private key pair.
|
||||
* @returns {Object} obj
|
||||
* @returns {String} obj.publicKey - base64, NaCl, public key
|
||||
* @returns {String} obj.privateKey - base64, NaCl, private key
|
||||
*/
|
||||
const generateKeyPair = () => {
|
||||
const pair = nacl.box.keyPair();
|
||||
|
||||
return ({
|
||||
publicKey: util.encodeBase64(pair.publicKey),
|
||||
privateKey: util.encodeBase64(pair.secretKey)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return assymmetrically encrypted [plaintext] using [publicKey] where
|
||||
* [publicKey] likely belongs to the recipient.
|
||||
@ -81,7 +96,7 @@ const decryptAsymmetric = ({
|
||||
* Return symmetrically encrypted [plaintext] using [key].
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.plaintext - plaintext to encrypt
|
||||
* @param {String} obj.key - 16-byte hex key
|
||||
* @param {String} obj.key - hex key
|
||||
*/
|
||||
const encryptSymmetric = ({
|
||||
plaintext,
|
||||
@ -114,7 +129,7 @@ const encryptSymmetric = ({
|
||||
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||
* @param {String} obj.iv - iv
|
||||
* @param {String} obj.tag - tag
|
||||
* @param {String} obj.key - 32-byte hex key
|
||||
* @param {String} obj.key - hex key
|
||||
*
|
||||
*/
|
||||
const decryptSymmetric = ({
|
||||
@ -139,6 +154,7 @@ const decryptSymmetric = ({
|
||||
};
|
||||
|
||||
export {
|
||||
generateKeyPair,
|
||||
encryptAsymmetric,
|
||||
decryptAsymmetric,
|
||||
encryptSymmetric,
|
||||
|
@ -1,60 +0,0 @@
|
||||
// membership roles
|
||||
const OWNER = 'owner';
|
||||
const ADMIN = 'admin';
|
||||
const MEMBER = 'member';
|
||||
|
||||
// membership statuses
|
||||
const INVITED = 'invited';
|
||||
|
||||
// -- organization
|
||||
const ACCEPTED = 'accepted';
|
||||
|
||||
// -- workspace
|
||||
const COMPLETED = 'completed';
|
||||
const GRANTED = 'granted';
|
||||
|
||||
// subscriptions
|
||||
const PLAN_STARTER = 'starter';
|
||||
const PLAN_PRO = 'pro';
|
||||
|
||||
// secrets
|
||||
const SECRET_SHARED = 'shared';
|
||||
const SECRET_PERSONAL = 'personal';
|
||||
|
||||
// environments
|
||||
const ENV_DEV = 'dev';
|
||||
const ENV_TESTING = 'test';
|
||||
const ENV_STAGING = 'staging';
|
||||
const ENV_PROD = 'prod';
|
||||
const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]);
|
||||
|
||||
// integrations
|
||||
const INTEGRATION_HEROKU = 'heroku';
|
||||
const INTEGRATION_NETLIFY = 'netlify';
|
||||
const INTEGRATION_SET = new Set([INTEGRATION_HEROKU, INTEGRATION_NETLIFY]);
|
||||
|
||||
// integration types
|
||||
const INTEGRATION_OAUTH2 = 'oauth2';
|
||||
|
||||
export {
|
||||
OWNER,
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
INVITED,
|
||||
ACCEPTED,
|
||||
COMPLETED,
|
||||
GRANTED,
|
||||
PLAN_STARTER,
|
||||
PLAN_PRO,
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL,
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD,
|
||||
ENV_SET,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_SET,
|
||||
INTEGRATION_OAUTH2
|
||||
};
|
5
backend/src/variables/action.ts
Normal file
5
backend/src/variables/action.ts
Normal file
@ -0,0 +1,5 @@
|
||||
const ACTION_PUSH_TO_HEROKU = 'pushToHeroku';
|
||||
|
||||
export {
|
||||
ACTION_PUSH_TO_HEROKU
|
||||
}
|
14
backend/src/variables/environment.ts
Normal file
14
backend/src/variables/environment.ts
Normal file
@ -0,0 +1,14 @@
|
||||
// environments
|
||||
const ENV_DEV = 'dev';
|
||||
const ENV_TESTING = 'test';
|
||||
const ENV_STAGING = 'staging';
|
||||
const ENV_PROD = 'prod';
|
||||
const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]);
|
||||
|
||||
export {
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD,
|
||||
ENV_SET
|
||||
}
|
7
backend/src/variables/event.ts
Normal file
7
backend/src/variables/event.ts
Normal file
@ -0,0 +1,7 @@
|
||||
const EVENT_PUSH_SECRETS = 'pushSecrets';
|
||||
const EVENT_PULL_SECRETS = 'pullSecrets';
|
||||
|
||||
export {
|
||||
EVENT_PUSH_SECRETS,
|
||||
EVENT_PULL_SECRETS
|
||||
}
|
79
backend/src/variables/index.ts
Normal file
79
backend/src/variables/index.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import {
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD,
|
||||
ENV_SET
|
||||
} from './environment';
|
||||
import {
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_SET,
|
||||
INTEGRATION_OAUTH2,
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_OPTIONS
|
||||
} from './integration';
|
||||
import {
|
||||
OWNER,
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
INVITED,
|
||||
ACCEPTED,
|
||||
COMPLETED,
|
||||
GRANTED
|
||||
} from './organization';
|
||||
import {
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL
|
||||
} from './secret';
|
||||
import {
|
||||
PLAN_STARTER,
|
||||
PLAN_PRO
|
||||
} from './stripe';
|
||||
import {
|
||||
EVENT_PUSH_SECRETS,
|
||||
EVENT_PULL_SECRETS
|
||||
} from './event';
|
||||
import {
|
||||
ACTION_PUSH_TO_HEROKU
|
||||
} from './action';
|
||||
|
||||
export {
|
||||
OWNER,
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
INVITED,
|
||||
ACCEPTED,
|
||||
COMPLETED,
|
||||
GRANTED,
|
||||
PLAN_STARTER,
|
||||
PLAN_PRO,
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL,
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD,
|
||||
ENV_SET,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_SET,
|
||||
INTEGRATION_OAUTH2,
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
EVENT_PUSH_SECRETS,
|
||||
EVENT_PULL_SECRETS,
|
||||
ACTION_PUSH_TO_HEROKU,
|
||||
INTEGRATION_OPTIONS
|
||||
};
|
119
backend/src/variables/integration.ts
Normal file
119
backend/src/variables/integration.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import {
|
||||
CLIENT_ID_HEROKU,
|
||||
CLIENT_ID_NETLIFY,
|
||||
CLIENT_SLUG_VERCEL
|
||||
} from '../config';
|
||||
|
||||
// integrations
|
||||
const INTEGRATION_HEROKU = 'heroku';
|
||||
const INTEGRATION_VERCEL = 'vercel';
|
||||
const INTEGRATION_NETLIFY = 'netlify';
|
||||
const INTEGRATION_SET = new Set([
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY
|
||||
]);
|
||||
|
||||
// integration types
|
||||
const INTEGRATION_OAUTH2 = 'oauth2';
|
||||
|
||||
// integration oauth endpoints
|
||||
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
|
||||
const INTEGRATION_VERCEL_TOKEN_URL = 'https://api.vercel.com/v2/oauth/access_token';
|
||||
const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token';
|
||||
|
||||
// integration apps endpoints
|
||||
const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com';
|
||||
const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com';
|
||||
const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com';
|
||||
|
||||
const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: 'Heroku',
|
||||
slug: 'heroku',
|
||||
image: 'Heroku',
|
||||
isAvailable: true,
|
||||
type: 'oauth2',
|
||||
clientId: CLIENT_ID_HEROKU,
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Vercel',
|
||||
slug: 'vercel',
|
||||
image: 'Vercel',
|
||||
isAvailable: true,
|
||||
type: 'vercel',
|
||||
clientId: '',
|
||||
clientSlug: CLIENT_SLUG_VERCEL,
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Netlify',
|
||||
slug: 'netlify',
|
||||
image: 'Netlify',
|
||||
isAvailable: false,
|
||||
type: 'oauth2',
|
||||
clientId: CLIENT_ID_NETLIFY,
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Google Cloud Platform',
|
||||
slug: 'gcp',
|
||||
image: 'Google Cloud Platform',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Amazon Web Services',
|
||||
slug: 'aws',
|
||||
image: 'Amazon Web Services',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Microsoft Azure',
|
||||
slug: 'azure',
|
||||
image: 'Microsoft Azure',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Travis CI',
|
||||
slug: 'travisci',
|
||||
image: 'Travis CI',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Circle CI',
|
||||
slug: 'circleci',
|
||||
image: 'Circle CI',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
}
|
||||
]
|
||||
|
||||
export {
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_SET,
|
||||
INTEGRATION_OAUTH2,
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_OPTIONS
|
||||
}
|
24
backend/src/variables/organization.ts
Normal file
24
backend/src/variables/organization.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// membership roles
|
||||
const OWNER = 'owner';
|
||||
const ADMIN = 'admin';
|
||||
const MEMBER = 'member';
|
||||
|
||||
// membership statuses
|
||||
const INVITED = 'invited';
|
||||
|
||||
// -- organization
|
||||
const ACCEPTED = 'accepted';
|
||||
|
||||
// -- workspace
|
||||
const COMPLETED = 'completed';
|
||||
const GRANTED = 'granted';
|
||||
|
||||
export {
|
||||
OWNER,
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
INVITED,
|
||||
ACCEPTED,
|
||||
COMPLETED,
|
||||
GRANTED
|
||||
}
|
8
backend/src/variables/secret.ts
Normal file
8
backend/src/variables/secret.ts
Normal file
@ -0,0 +1,8 @@
|
||||
// secrets
|
||||
const SECRET_SHARED = 'shared';
|
||||
const SECRET_PERSONAL = 'personal';
|
||||
|
||||
export {
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL
|
||||
}
|
7
backend/src/variables/stripe.ts
Normal file
7
backend/src/variables/stripe.ts
Normal file
@ -0,0 +1,7 @@
|
||||
const PLAN_STARTER = 'starter';
|
||||
const PLAN_PRO = 'pro';
|
||||
|
||||
export {
|
||||
PLAN_STARTER,
|
||||
PLAN_PRO
|
||||
}
|
102
cli/README.md
102
cli/README.md
@ -1,102 +0,0 @@
|
||||
## Install
|
||||
#### Windows
|
||||
Use [Scoop](https://scoop.sh/) package manager
|
||||
|
||||
```
|
||||
$ scoop bucket add org https://github.com/Infisical/scoop-infisical.git
|
||||
$ scoop install infisical
|
||||
$ infisical --version
|
||||
```
|
||||
|
||||
To update:
|
||||
|
||||
```
|
||||
$ scoop update infisical
|
||||
```
|
||||
|
||||
#### Mac OS
|
||||
Use [brew](https://brew.sh/) package manager
|
||||
|
||||
```
|
||||
$ brew install infisical/get-cli/infisical
|
||||
$ infisical --version
|
||||
```
|
||||
|
||||
To update:
|
||||
|
||||
```
|
||||
$ brew upgrade infisical
|
||||
```
|
||||
|
||||
#### Linux
|
||||
##### Debian/Ubuntu (package manager: apt)
|
||||
|
||||
```
|
||||
Add Infisical apt repo
|
||||
$ echo "deb [trusted=yes] https://apt.fury.io/infisical/ /" | tee -a /etc/apt/sources.list.d/infisical.list
|
||||
|
||||
Add prerequisites
|
||||
$ apt update && apt -y install ca-certificates sudo
|
||||
|
||||
Install infisical cli
|
||||
$ sudo apt update && apt install infisical
|
||||
|
||||
To make sure the CLI has been installed, you may run this command.
|
||||
$ infisical --version
|
||||
```
|
||||
|
||||
We do not yet have repositores setup for APK, YUM and APT package managers. However, we have several binaries which can be downloaded manually for your Linux. Please vist the [release age](https://github.com/Infisical/infisical/releases)
|
||||
|
||||
#### Install via bash and curl
|
||||
This script will attempt to download the correct version of Infisical CLI and add it to your path. No package manager needed.
|
||||
|
||||
```
|
||||
curl https://raw.githubusercontent.com/Infisical/infisical/main/scripts/install.sh | sh
|
||||
```
|
||||
|
||||
## Local Usage
|
||||
Once you have the CLI installed, using it is easy.
|
||||
|
||||
#### Steps 1
|
||||
Create a project at https://infisical.com/ if you haven't already add your secrets to it.
|
||||
|
||||
#### Step 2
|
||||
Login to the CLI by running the following command in your terminal
|
||||
|
||||
```
|
||||
infisical login
|
||||
```
|
||||
|
||||
#### Step 3
|
||||
After logging in, `CD` to the root of the project where you would like to inject your secrets into. Once you are in the root, run the following command in the terminal to link your Infisical project to your local project.
|
||||
|
||||
```
|
||||
infisical init
|
||||
```
|
||||
|
||||
#### Step 3
|
||||
To inject the secrets from the project you have selected into your application process, run the following command.
|
||||
|
||||
```
|
||||
infisical run -- <your application start command>
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
infisical run -- npm run dev
|
||||
```
|
||||
|
||||
## General production Usage
|
||||
Once you have the binary installed in your production environment, injecting secrets is easy.
|
||||
|
||||
#### Steps 1
|
||||
Get a Infisical Token for your project by visiting BLANK. Also note down the project ID for which you created the token for.
|
||||
|
||||
#### Steps 2
|
||||
Ensure your application has the environment variable `INFISICAL_TOKEN` asigned to the token you received in step one. Then run
|
||||
|
||||
```
|
||||
infisical run --projectId=<projectID> -- <your application start command>
|
||||
```
|
||||
|
@ -4,14 +4,14 @@ go 1.19
|
||||
|
||||
require (
|
||||
github.com/spf13/cobra v1.6.1
|
||||
golang.org/x/crypto v0.2.0
|
||||
golang.org/x/crypto v0.3.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alessio/shellescape v1.4.1 // indirect
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
|
||||
github.com/danieljoos/wincred v1.1.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.0.6 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/danieljoos/wincred v1.1.2 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
golang.org/x/net v0.2.0 // indirect
|
||||
golang.org/x/sys v0.2.0 // indirect
|
||||
)
|
||||
|
21
cli/go.sum
21
cli/go.sum
@ -1,21 +1,26 @@
|
||||
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
|
||||
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g=
|
||||
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
|
||||
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
|
||||
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
|
||||
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
||||
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
|
||||
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
@ -36,14 +41,16 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/zalando/go-keyring v0.2.1 h1:MBRN/Z8H4U5wEKXiD67YbDAr5cj/DOStmSga70/2qKc=
|
||||
github.com/zalando/go-keyring v0.2.1/go.mod h1:g63M2PPn0w5vjmEbwAX3ib5I+41zdm4esSETOn9Y6Dw=
|
||||
golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
|
||||
golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
151
cli/packages/cmd/export.go
Normal file
151
cli/packages/cmd/export.go
Normal file
@ -0,0 +1,151 @@
|
||||
/*
|
||||
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
FormatDotenv string = "dotenv"
|
||||
FormatJson string = "json"
|
||||
FormatCSV string = "csv"
|
||||
FormatYaml string = "yaml"
|
||||
)
|
||||
|
||||
// exportCmd represents the export command
|
||||
var exportCmd = &cobra.Command{
|
||||
Use: "export",
|
||||
Short: "Used to export environment variables to a file",
|
||||
DisableFlagsInUseLine: true,
|
||||
Example: "infisical export --env=prod --format=json > secrets.json",
|
||||
Args: cobra.NoArgs,
|
||||
PreRun: toggleDebug,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
envName, err := cmd.Flags().GetString("env")
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse the environment flag")
|
||||
log.Debugln(err)
|
||||
return
|
||||
}
|
||||
|
||||
shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse the substitute flag")
|
||||
log.Debugln(err)
|
||||
return
|
||||
}
|
||||
|
||||
projectId, err := cmd.Flags().GetString("projectId")
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse the project id flag")
|
||||
log.Debugln(err)
|
||||
return
|
||||
}
|
||||
|
||||
format, err := cmd.Flags().GetString("format")
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse the format flag")
|
||||
log.Debugln(err)
|
||||
return
|
||||
}
|
||||
|
||||
envsFromApi, err := util.GetAllEnvironmentVariables(projectId, envName)
|
||||
if err != nil {
|
||||
log.Errorln("Something went wrong when pulling secrets using your Infisical token. Double check the token, project id or environment name (dev, prod, ect.)")
|
||||
log.Debugln(err)
|
||||
return
|
||||
}
|
||||
|
||||
var output string
|
||||
if shouldExpandSecrets {
|
||||
substitutions := util.SubstituteSecrets(envsFromApi)
|
||||
output, err = formatEnvs(substitutions, format)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
output, err = formatEnvs(envsFromApi, format)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
fmt.Print(output)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(exportCmd)
|
||||
exportCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
|
||||
exportCmd.Flags().String("projectId", "", "The project ID from which your secrets should be pulled from")
|
||||
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)")
|
||||
}
|
||||
|
||||
// Format according to the format flag
|
||||
func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string, error) {
|
||||
switch strings.ToLower(format) {
|
||||
case FormatDotenv:
|
||||
return formatAsDotEnv(envs), nil
|
||||
case FormatJson:
|
||||
return formatAsJson(envs), nil
|
||||
case FormatCSV:
|
||||
return formatAsCSV(envs), nil
|
||||
case FormatYaml:
|
||||
return formatAsYaml(envs), nil
|
||||
default:
|
||||
return "", fmt.Errorf("invalid format type: %s. Available format types are [%s]", format, []string{FormatDotenv, FormatJson, FormatCSV, FormatYaml})
|
||||
}
|
||||
}
|
||||
|
||||
// Format environment variables as a CSV file
|
||||
func formatAsCSV(envs []models.SingleEnvironmentVariable) string {
|
||||
csvString := &strings.Builder{}
|
||||
writer := csv.NewWriter(csvString)
|
||||
writer.Write([]string{"Key", "Value"})
|
||||
for _, env := range envs {
|
||||
writer.Write([]string{env.Key, env.Value})
|
||||
}
|
||||
writer.Flush()
|
||||
return csvString.String()
|
||||
}
|
||||
|
||||
// Format environment variables as a dotenv file
|
||||
func formatAsDotEnv(envs []models.SingleEnvironmentVariable) string {
|
||||
var dotenv string
|
||||
for _, env := range envs {
|
||||
dotenv += fmt.Sprintf("%s='%s'\n", env.Key, env.Value)
|
||||
}
|
||||
return dotenv
|
||||
}
|
||||
|
||||
func formatAsYaml(envs []models.SingleEnvironmentVariable) string {
|
||||
var dotenv string
|
||||
for _, env := range envs {
|
||||
dotenv += fmt.Sprintf("%s: %s\n", env.Key, env.Value)
|
||||
}
|
||||
return dotenv
|
||||
}
|
||||
|
||||
// Format environment variables as a JSON file
|
||||
func formatAsJson(envs []models.SingleEnvironmentVariable) string {
|
||||
// Dump as a json array
|
||||
json, err := json.Marshal(envs)
|
||||
if err != nil {
|
||||
log.Errorln("Unable to marshal environment variables to JSON")
|
||||
log.Debugln(err)
|
||||
return ""
|
||||
}
|
||||
return string(json)
|
||||
}
|
@ -36,7 +36,7 @@ var initCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
if util.WorkspaceConfigFileExists() {
|
||||
if util.WorkspaceConfigFileExistsInCurrentPath() {
|
||||
shouldOverride, err := shouldOverrideWorkspacePrompt()
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse your answer")
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user