Compare commits

..

448 Commits

Author SHA1 Message Date
8c10ae78f5 update cli version 2022-12-25 14:59:59 -05:00
dd7b2dc1ed Remove creating instance on PersistentPreRun 2022-12-25 14:58:51 -05:00
3b6b9d41d1 Remove creating instance on PersistentPreRun 2022-12-25 14:47:46 -05:00
859f4226ae update vault command Description 2022-12-25 14:16:45 -05:00
9d0c293600 Update vault docs and add cli FAQ 2022-12-25 14:08:34 -05:00
b454a0400a Merge pull request #170 from Infisical/cross-compile-cgo
split builds for cross compile
2022-12-25 13:31:49 -05:00
33805ad385 split builds for cross compile 2022-12-25 13:04:59 -05:00
7f51aaf451 Add vault docs 2022-12-25 00:04:52 -05:00
2513250993 Update package-lock 2022-12-24 20:45:35 -05:00
c3b2e08a23 Merge pull request #152 from Zamion101/feat/error-handling
Refactoring backend codebase to use central error handling and logging
2022-12-24 20:43:40 -05:00
a12ae350a7 Merge branch 'main' into feat/error-handling 2022-12-24 20:34:55 -05:00
f22d4277dc add support for multiple keyring 2022-12-24 19:11:29 -05:00
d72e113570 feat: Patch Router#handle to catch Promise Rejection
In order to catch Promise rejections inside and outside of middlewares
as well as in route logic we need to patch `Router#handle` and add
.catch() to the function. With that addition it is possible to
catch rejections and handle inside `requestErrorHandler` middleware.
2022-12-24 23:36:58 +01:00
22d6aa8e46 fix: Fixed wrong naming for error handler middleware 2022-12-24 21:45:53 +01:00
6216f70950 chore: Split error handler middleware to a seperate file 2022-12-24 21:43:02 +01:00
28818db757 refactor: Prefer use of RequestError and next() function on middlewares
Added:
- New error types such as `IntegrationNotFoundError`,
  `WorkspaceNotFoundError`, `AccountNotFoundError' and more.

Refactored:
- Refactored most of the middlewares and very little number of helper
  functions to use RequestError
- Deleted unused imports

Changed:
- Some of the error types in middlewares changed to more related error
  types.
- Environment variable of 'VERBOSE_ERROR_OUTPUT' changed to more
  reliable validation method in `config/index.ts` as per @dangtony98
  requested.
2022-12-24 20:52:26 +01:00
79333657ed Add parseInt to SMTP_PORT envar 2022-12-24 14:25:39 -05:00
3bcc4ead1f Correct SMTP_HOST default envar in docs 2022-12-24 14:22:14 -05:00
3341b08b22 Revamp contributing development docs 2022-12-24 14:21:21 -05:00
d4bab09927 Add self-hosting email configuration docs + examples, correct SMTP setup for SendGrid/Mailgun-specific setup 2022-12-24 11:10:56 -05:00
9d0efc5a22 Updated contributors and links 2022-12-23 23:26:25 -05:00
34d7db5c1e Merge pull request #166 from naorpeled/feat/frontend/add-resend-code-to-signup
feat(frontend/signup): add resend code functionality
2022-12-23 23:22:31 -05:00
5eefe75dac initial commit 2022-12-24 04:00:13 +02:00
5324355006 Merge pull request #158 from Infisical/snyk-upgrade-678d7b4eeea6a4d9172b69ed3febbea2
[Snyk] Upgrade posthog-node from 2.2.0 to 2.2.2
2022-12-23 08:28:20 -05:00
baba99e19b Added 404 page 2022-12-23 08:15:46 -05:00
54f99f3db0 Added contributor to Readme 2022-12-23 07:50:09 -05:00
fc32647201 Embed project id into exported service tokens 2022-12-22 22:42:45 -05:00
4c62783a8e Merge pull request #161 from wjhurley/patch-1
Fix typo in quickstart
2022-12-22 15:01:53 -05:00
ae6de31774 Fix typo in quickstart 2022-12-22 19:59:55 +00:00
41f6255b7b updated the nopojects page design 2022-12-22 14:53:27 -05:00
e4c3db074d Added contributor 2022-12-22 14:32:17 -05:00
13a650fb47 Merge branch 'main' of https://github.com/Infisical/infisical 2022-12-22 14:31:29 -05:00
d2c2ca89cb Added dragon to no projects and addec contributor to readme 2022-12-22 14:28:29 -05:00
fd6b3e96a3 Update GitHub actions docs and README integrations 2022-12-22 14:22:45 -05:00
95ff1f185b Merge branch 'main' of https://github.com/Infisical/infisical 2022-12-22 14:09:43 -05:00
ed2558a7ad Add GitHub Actions docs and remove GET Infisical public key route 2022-12-22 14:09:34 -05:00
c20f63633b update k8 self hosting doc 2022-12-22 13:39:19 -05:00
dc567b88ca update k8 self hosting doc 2022-12-22 13:39:19 -05:00
509a92a00d Merge branch 'main' of https://github.com/Infisical/infisical 2022-12-22 13:36:23 -05:00
3f2d011fba Update contributing dev docs and envars, remove PUBLIC_KEY, PRIVATE_KEY server envar requirement 2022-12-22 13:36:15 -05:00
ca8bc212a3 update helm install command 2022-12-22 13:35:36 -05:00
ec0313ffa0 update helm chart 2022-12-22 13:26:51 -05:00
d8d3e02e92 Add ingress annotation as default value 2022-12-22 13:26:13 -05:00
ea5a758959 fix: upgrade posthog-node from 2.2.0 to 2.2.2
Snyk has created this PR to upgrade posthog-node from 2.2.0 to 2.2.2.

See this package in npm:
https://www.npmjs.com/package/posthog-node

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-12-22 18:07:27 +00:00
e0973f0b28 Delete close_inactive_issues.yml 2022-12-22 13:04:30 -05:00
06e6443225 Merge pull request #149 from arthurzenika/patch-1
fix(helm-charts/frontend): port listenning to 3000
2022-12-22 13:02:53 -05:00
843757fcf5 fix: Wrong error handler middleware position
Error handler is a middleware that captures errors from throw and
next(...), so in order to be able to handle it needs to come after
all of the routing and other middlewares. Previously it was before
routuing logic and wasn't working, now it works as intended.

Also added check for LogLevel to Sentry for not sending false-positive
errors like `BadRequestError`, `UnauthorizedRequestError` and etc.
2022-12-22 17:51:53 +01:00
1dc03399ce Update close_inactive_issues.yml 2022-12-22 10:52:09 -05:00
a939d98099 Update README.md 2022-12-22 00:50:05 -05:00
55996440da Merge pull request #155 from jonerrr/main
Update envars for SMTP for greater versatility.
2022-12-22 00:22:47 -05:00
9643c75b42 Merge pull request #156 from Infisical/dependabot/npm_and_yarn/backend/jsonwebtoken-9.0.0
Bump jsonwebtoken from 8.5.1 to 9.0.0 in /backend
2022-12-21 23:42:45 -05:00
ebdf04b39f Merge pull request #125 from Infisical/snyk-upgrade-5062255a2e34f7370d0aefcb1261369d
[Snyk] Upgrade posthog-node from 2.1.0 to 2.2.0
2022-12-21 23:41:38 -05:00
6f40427425 Bump jsonwebtoken from 8.5.1 to 9.0.0 in /backend
Bumps [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) from 8.5.1 to 9.0.0.
- [Release notes](https://github.com/auth0/node-jsonwebtoken/releases)
- [Changelog](https://github.com/auth0/node-jsonwebtoken/blob/master/CHANGELOG.md)
- [Commits](https://github.com/auth0/node-jsonwebtoken/compare/v8.5.1...v9.0.0)

---
updated-dependencies:
- dependency-name: jsonwebtoken
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-22 04:40:42 +00:00
aaa87b2b85 Fix errors and complete v1 full-loop of GitHub integration with repository secrets 2022-12-21 23:16:50 -05:00
7763e33de6 Fix errors and complete v1 full-loop of GitHub integration with repository secrets 2022-12-21 23:15:22 -05:00
68bf1f6c8a update env vars for mail 2022-12-21 21:27:37 -05:00
6f5d7a831b refactor: Refactored middlewares to use RequestError
Refactored middlewares to use RequestError rather than using
`try {...}catch (err){...}`. With this change it's possible to manage
all error details within one place.

Added:
- Added Sentry.captureException to Error Handler
2022-12-22 01:42:02 +01:00
bd9041a62c feat: Add central error handler
Added:
- Added Midleware to capture and handle all errors
    - will catch error from `throw ...` and `next(...)`
- Added RequestError base error class to build consistent error objects
- Added common errors like `UnauthorizedRequestError`, `BadRequestError`
- Added consistent Logging solution using `winston`
    - Supports Loki Transporter using `winston-loki`
- Outputing Legal disclaimer to console when `TELEMETRY_ENABLED=true`

Changed:
- Changed console.log to getLogger() favor of using consistent logging
2022-12-22 00:55:41 +01:00
54b0285cbd Merge pull request #130 from gmgale/bot
Bot - Started Github integration
2022-12-21 17:11:41 -05:00
96caa587d6 Merge branch 'bot' into bot 2022-12-21 17:11:09 -05:00
2d3255edc0 Merge pull request #140 from reginaldbondoc/main
Backend test setup
2022-12-21 16:06:58 -05:00
0ee563948d Update package-lock 2022-12-21 20:34:44 +01:00
95b8d27c59 Merge branch 'main' of github.com:reginaldbondoc/infisical 2022-12-21 20:33:14 +01:00
5790af7b8b Merge branch 'Infisical:main' into main 2022-12-21 20:32:59 +01:00
a3609b9b6f Align with comments 2022-12-21 20:32:38 +01:00
42a0a9d0c8 fix(helm-charts/frontend): port listenning to 3000 2022-12-21 17:34:35 +01:00
f31424dc6a update helm chart to make envfrom optional 2022-12-21 10:49:10 -05:00
2bd9f4e6dd Merge pull request #128 from SH5H/fixError
Fix login runtime error
2022-12-21 07:55:36 -05:00
fdcf2dd716 Merge branch 'main' of https://github.com/Infisical/infisical 2022-12-21 01:55:30 -05:00
78a52422a2 Patch Netlify integration cases, add new docs for Heroku, Vercel, and Netlify 2022-12-21 01:55:23 -05:00
4b261c4a87 Update README.md 2022-12-20 22:53:02 -05:00
d93bdff2d4 Merge pull request #144 from Zamion101/fix/issue-142
Fix `JWT_SERVICE_SECRET` not present in docs
2022-12-20 22:06:44 -05:00
66cf2f6091 Update develoing.mdx related to issue #142 2022-12-21 02:45:42 +01:00
44482dda26 fix: Relocate 'JWT_SERVICE_SECRET' related to issue #142 2022-12-21 02:35:05 +01:00
f7b110759d add support for arch linux 2022-12-20 20:11:28 -05:00
45f92bdd93 update infisical helm chart version 2022-12-20 18:49:36 -05:00
ed8134da4f allow helm template to add secret ref 2022-12-20 18:48:53 -05:00
75e3a3b2d6 Merge branch 'Infisical:main' into main 2022-12-20 21:24:11 +01:00
958ef560f7 Merge pull request #6 from reginaldbondoc/reginaldbondoc-patch-5
Clean up
2022-12-20 21:23:58 +01:00
2eb2f3ff1c Use GH secret 2022-12-20 21:19:04 +01:00
4aaa01fb0e Clean up 2022-12-20 20:39:55 +01:00
83c3745045 PR comment on coverage 2022-12-20 20:39:08 +01:00
ecbacfb0c9 Make coverage comment 2022-12-20 20:34:26 +01:00
507b0bd97f Add yaml output 2022-12-20 14:10:53 -05:00
2bff7bbb5a Merge pull request #139 from Infisical/cli-multi-command
Cli multi command support
2022-12-20 13:19:10 -05:00
b09ae054dd Add docs for chained commands 2022-12-20 13:18:20 -05:00
dc9c6b9d13 add chained command support 2022-12-20 12:54:41 -05:00
fcf300fa96 Set output mode and file 2022-12-20 18:14:58 +01:00
3eb33d22cf Back to report.json 2022-12-20 17:55:05 +01:00
8a209049c5 Point to different coverage file 2022-12-20 17:49:29 +01:00
eee40fd583 Fix report steps 2022-12-20 16:50:43 +01:00
483d4f831d Merge branch 'main' of github.com:reginaldbondoc/infisical 2022-12-20 15:50:07 +01:00
4a1cf88ab4 Use different GHA for downloading artifact 2022-12-20 14:15:24 +01:00
f01e8cb33b add multi command 2022-12-19 20:21:23 -05:00
be35da7ac4 Merge pull request #2 from reginaldbondoc/reginaldbondoc-patch-1
Update app.ts
2022-12-20 01:27:32 +01:00
7eedefa0c9 Update app.ts 2022-12-20 01:17:04 +01:00
113ec55681 Correct workflow test 2022-12-20 01:13:13 +01:00
bd207dc65f Merge branch 'main' of github.com:reginaldbondoc/infisical 2022-12-20 01:04:33 +01:00
fc906e7dc7 Setup test, break apart entrypoint, & align GHA workflow 2022-12-20 00:56:53 +01:00
3ba62d1d97 Update README.md 2022-12-19 13:05:27 -05:00
5816ce3bf7 Patch lint integration issues on frontend 2022-12-19 12:14:58 -05:00
2d77fe9ca3 Fix lint issues 2022-12-19 12:05:19 -05:00
8bfab95cc7 Merging upstream changes. 2022-12-19 10:58:57 +01:00
11e64febdb Added frontend for Github integration. 2022-12-19 10:24:16 +01:00
6bb24933bf Add back health check and fix json file not found 2022-12-18 20:10:17 -05:00
fbc38c553a Remove tests from backend docker img 2022-12-18 19:27:45 -05:00
6ba4701db8 delete Dockerfile.prod from frontend 2022-12-18 19:27:45 -05:00
c15a9301af Temp disable healthcheck 2022-12-18 19:16:56 -05:00
91052df5f9 Fix Typescript issues for frontend integrations 2022-12-18 18:57:50 -05:00
6ea26c135a Merge branch 'bot' 2022-12-18 17:34:18 -05:00
5444382d5a Remove unecessary import in integrations 2022-12-18 16:27:38 -05:00
200bb12ad8 Move CLIENT_ID envars to be fetched from backend for (cloud) integration options 2022-12-18 16:10:58 -05:00
1447e055d1 Remove SITE_URL from frontend 2022-12-18 14:30:31 -05:00
4dac03ab94 Patch undefined siteId passthrough to API 2022-12-18 14:09:04 -05:00
20ea50bfaf Added intercom to docs 2022-12-18 13:32:08 -05:00
5474096ca9 Move Stay Up-to-Date to bottom to give integrations more spot light 2022-12-18 12:47:33 -05:00
06c1827f38 Remove prettier 2022-12-18 12:29:21 -05:00
9b7f036fd0 Merge pull request #127 from SH5H/testtest
Adding automatic linter for checking commit
2022-12-18 12:25:34 -05:00
547555591b Refactor integrations logic and replace hardcoded client ids with envars 2022-12-18 12:18:50 -05:00
516819507a Merge tag 'main' of https://github.com/Infisical/infisical into bot 2022-12-18 12:11:00 -05:00
75020568d3 Started Github integration. 2022-12-18 17:45:31 +01:00
02e5be20c2 Update README.md 2022-12-18 08:39:40 -05:00
6ed8f8af08 Fix login runtime error 2022-12-18 15:50:46 +09:00
de11c50563 Delete Prettier 2022-12-18 15:41:22 +09:00
33dddd440c move coming soon integ to bottom 2022-12-17 23:58:45 -05:00
19b909cd12 modify verify step in docs 2022-12-17 23:57:06 -05:00
cd59ca745d Add kubernetes operator docs 2022-12-17 23:48:12 -05:00
e013a4ab93 remove namespace from sample 2022-12-17 13:59:54 -05:00
19daf1410a allow host api in spec and update spec names 2022-12-17 13:27:36 -05:00
4c29c88fde Adding automatic linter for checking commit 2022-12-18 00:36:08 +09:00
6af59e47f5 yaml for kubectl install secrets operator 2022-12-16 16:48:21 -05:00
8183e61403 Update k8 read me 2022-12-16 16:37:31 -05:00
b4c616edd6 set image name for k8 2022-12-16 16:35:49 -05:00
c12eeac9b3 Add placeholder upcoming integrations to docs 2022-12-16 16:35:08 -05:00
033275ed69 update read me helm chart 2022-12-16 16:14:05 -05:00
a799e1bffc Add new workflow to push k8 operator to prod 2022-12-16 16:12:13 -05:00
36300cd19d Begin personal access token-based integrations 2022-12-16 15:44:50 -05:00
c8633bf546 Add new workflow to push k8 operator to prod 2022-12-16 13:57:04 -05:00
7fe2e15a98 add substitution into k8 and fix loading token 2022-12-15 22:25:44 -05:00
72bf160f2e add secrets ready to sync condition 2022-12-15 20:02:22 -05:00
0ef9db99b4 Add reconcile loop 2022-12-15 19:08:30 -05:00
805f733499 authenticate k8 operator via token 2022-12-15 16:36:33 -05:00
595dc78e75 Add kubebuiler and types for secret groups 2022-12-15 16:29:43 -05:00
6c7d232a9e Fix merge conflicts for index 2022-12-15 15:31:14 -05:00
35fd1520e2 Add integration auth revocation 2022-12-15 15:27:01 -05:00
3626ef2ec2 add yaml export 2022-12-15 14:20:20 -05:00
a49fcf49f1 Rotate test OAuth2 token 2022-12-14 18:43:21 -05:00
787e54fb91 Finish Netlify integration v1 full-loop 2022-12-14 18:18:21 -05:00
0c30ee8ffa fix: upgrade posthog-node from 2.1.0 to 2.2.0
Snyk has created this PR to upgrade posthog-node from 2.1.0 to 2.2.0.

See this package in npm:
https://www.npmjs.com/package/posthog-node

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-12-14 18:05:02 +00:00
90537f2e6d Fixed the drag & drop zone 2022-12-14 00:36:05 -05:00
c9448656bf Fixed the TS error with signup invites 2022-12-13 23:30:53 -05:00
f3900213b5 Add telemetry output at backend container start 2022-12-13 21:59:12 -05:00
fe17d8459b Begin Netlify integration 2022-12-13 21:12:40 -05:00
2f54c4dd7e add sharp for prod nextjs build 2022-12-13 20:07:53 -05:00
c33b043f5f remove cookie path temporary 2022-12-13 18:54:45 -05:00
5db60c0dad update helm template; check for non string 2022-12-13 17:55:49 -05:00
1a3d3906da Change jid path 2022-12-13 16:25:19 -05:00
d86c335671 Begin Netlify integration 2022-12-13 15:47:27 -05:00
62f0b3f6df Patch EMAIL_TOKEN_LIFETIME expiring early 2022-12-13 15:22:07 -05:00
3e623922b4 Preliminary Vercel integration 2022-12-13 13:59:21 -05:00
d1c38513f7 update chart version 2022-12-13 12:48:29 -05:00
63253d515f quote secrets for template 2022-12-13 12:46:35 -05:00
584d309b80 Fixed the TS bug in signupinvite 2022-12-13 09:52:30 -05:00
07bb3496e7 Added the account recovery flow 2022-12-12 20:42:16 -05:00
c83c75db96 Merge pull request #116 from reginaldbondoc/main
Add healthchecks and test image before push
2022-12-12 13:31:44 -05:00
bcd18ab0af Ignore linting healthcheck & exclue in rate-limiting 2022-12-12 19:11:44 +01:00
1ea75eb840 Merge branch 'Infisical:main' into main 2022-12-12 18:15:13 +01:00
271c810692 Remove awkward lag when integration is loading its apps 2022-12-12 10:58:49 -05:00
dd05e2ac01 Merge branch 'main' of https://github.com/Infisical/infisical into bot 2022-12-12 10:58:26 -05:00
c6c2cfaaa5 Continue trimming frontend integration page and renaming variables 2022-12-12 10:36:13 -05:00
6f90064400 Add required backup key return fields for GET backup key 2022-12-12 08:31:20 -05:00
397c15d61e Continue integration frontend refactor 2022-12-12 08:26:09 -05:00
9e3ac6c31d Fix merge conflicts 2022-12-12 00:26:37 -05:00
10d57e9d88 Modularize integrations into json files, continue refactoring integrations frontend 2022-12-12 00:23:13 -05:00
95a1e9560e Added automatic secret sorting on the first pull 2022-12-11 21:28:59 -05:00
11e0790f13 Merge pull request #117 from LemmyMwaura/secrets-dash-jumping-bug-#51
🐛 Dashboard bugfix - Jumping Secrets #51
2022-12-11 21:20:04 -05:00
099ddd6805 fix:quotes - prettier 2022-12-12 01:08:59 +03:00
182db69ee1 Merge branch 'main' of https://github.com/Infisical/infisical into bot 2022-12-11 17:06:23 -05:00
01982b585f Add empty Vercel docs for integration reference 2022-12-11 17:02:06 -05:00
752ebaa3eb Add healthchecks and test image before push 2022-12-11 22:36:46 +01:00
117acce3f6 Merge branch 'main' of github.com:LemmyMwaura/infisical into secrets-dash-jumping-bug-#51 2022-12-11 23:50:35 +03:00
f94bf1f206 feat:only sort on reorder and cleanup changes 2022-12-11 23:31:31 +03:00
74d5586005 Modularize integration sections in frontend 2022-12-11 15:20:10 -05:00
8896e1232b Merge pull request #114 from Infisical/account-recovery
Begin reset password backend functionality
2022-12-11 14:55:54 -05:00
d456dcef28 refactor(perf):go back to passing index and using it to mutate keypair 2022-12-11 22:25:16 +03:00
eae2fc813a Convert JS to TS (#47) 2022-12-11 13:06:25 -05:00
7cdafe0eed Updated notifications 2022-12-11 13:01:40 -05:00
d410b42a34 Fix more merge conflicts and continue cleaning up integrations frontend 2022-12-11 12:21:49 -05:00
bacf9f2d91 Fix merge conflicts frontend integrations 2022-12-11 11:45:47 -05:00
3fc6b0c194 Refactoring integrations frontend (cleanup) 2022-12-11 11:44:19 -05:00
9dc1645559 Merge branch 'main' of https://github.com/Infisical/infisical 2022-12-11 11:38:25 -05:00
158c51ff3c Merge pull request #115 from SH5H/main
Convert JS to TS
2022-12-11 11:31:21 -05:00
864757e428 fix: add new row as map instead of list, bug fix due to updates 2022-12-11 17:58:41 +03:00
fa53a9e41d refactor: remove unused index values 2022-12-11 17:54:19 +03:00
36372ebef3 feat: find keypair by id during mutation 2022-12-11 17:53:49 +03:00
b3a50d657d refactor: call modify fns with id instead of index 2022-12-11 17:00:25 +03:00
c2f5f19f55 refactor: instead of the index and we pass the keypair id 2022-12-11 16:59:32 +03:00
2b6e69ce1b Fix Build Error 2022-12-11 15:51:29 +09:00
c4b4829694 Linting 2022-12-11 15:07:53 +09:00
d503102f75 Change tsconfig 2022-12-11 15:02:15 +09:00
4a14753b8c Convert TS to JS 2022-12-11 15:02:15 +09:00
2c834040b4 Merge branch 'main' of https://github.com/Infisical/infisical into bot 2022-12-10 23:25:46 -05:00
6f682250b6 Begin reset password backend functionality 2022-12-10 23:06:54 -05:00
52285a1f38 added long term secret token options 2022-12-10 22:29:47 -05:00
31d6191251 Updated query-string package 2022-12-10 19:01:20 -05:00
d14ed06d4f Change frontend integration-bot wording 2022-12-10 17:46:14 -05:00
e2a84ce52e Merge branch 'main' of https://github.com/Infisical/infisical into bot 2022-12-10 17:00:10 -05:00
999f668f39 Merge pull request #112 from reginaldbondoc/main
Add local SMTP server for development
2022-12-10 16:56:36 -05:00
6b546034f4 Use Sentry instead of console logs 2022-12-10 22:05:19 +01:00
c2eaea21f0 Modify frontend to be compatible with full-loop for bot-based integrations 2022-12-10 15:43:22 -05:00
436f408fa8 Merge branch 'main' of https://github.com/Infisical/infisical into bot 2022-12-10 15:41:04 -05:00
da999107f3 Add local SMTP server for development 2022-12-10 21:10:05 +01:00
00ea296138 update cli install to remove $ from commands 2022-12-10 14:27:50 -05:00
968af64ee7 refactor: set data as a map instead of list 2022-12-10 22:24:02 +03:00
589e811e9b disable eslint for TELEMETRY_CAPTURING_ENABLED 2022-12-10 13:49:37 -05:00
4f312cfd1a update helm default values 2022-12-10 13:42:21 -05:00
3dccfc5404 add envs to frontend k8 2022-12-10 13:39:00 -05:00
2b3a996114 Update helm values example 2022-12-10 12:15:04 -05:00
93da106dbc test 2022-12-10 12:15:04 -05:00
7e544fcac8 Update Chart.yaml 2022-12-10 11:51:34 -05:00
c2eac43b4f remove prepare temporary 2022-12-10 11:21:35 -05:00
138acd28e8 Updated Infisical onboarding guide 2022-12-10 10:18:57 -05:00
1195398f15 update package lock 2022-12-10 09:35:32 -05:00
1fff273abb modify eslint rules 2022-12-10 09:28:19 -05:00
b586fcfd2e Add eslint disable next line 2022-12-10 09:26:54 -05:00
45ad639eaf Merge pull request #111 from SH5H/main
Add husky and lint-staged
2022-12-10 09:25:02 -05:00
e5342bd757 Added contributor to README.md 2022-12-10 08:54:35 -05:00
834c32aa7e Merge pull request #108 from adrianmarinwork/main
Updated the start developing guide as suggested in issue #75
2022-12-10 08:48:06 -05:00
df4dcc87e7 Improved MongoDB environment variables part
I have improved MongoDB environment variables part as requested in the pull request #108
2022-12-10 14:46:19 +01:00
d883c7ea96 Bring the last version of package file 2022-12-10 15:25:40 +09:00
5ed02955f8 Update lock file with npm audit fix command 2022-12-10 15:21:42 +09:00
064a9eb9cb Update root lock file 2022-12-10 15:13:23 +09:00
f805691c4f Update lock file in the frontend 2022-12-10 15:01:38 +09:00
4548931df3 Downgrade the version of lock file in frontend 2022-12-10 14:58:13 +09:00
6d1dc3845b Update README.md 2022-12-09 22:58:10 -05:00
40b42fdcb5 Update package-lock.json 2022-12-10 12:36:18 +09:00
ad907fa373 Remove useless dependencies 2022-12-10 12:28:46 +09:00
ae1088d3f6 Merge pull request #110 from arjunyel/password-autocomplete
Add autocomplete to sign in/up forms
2022-12-09 22:27:20 -05:00
9a3caac75f Add husky and lint-staged 2022-12-10 12:13:48 +09:00
1808ab6db8 fix variable assignment when pulling via token 2022-12-09 19:44:00 -05:00
aa554405c1 fix variable assignment when pulling via token 2022-12-09 19:36:55 -05:00
cd70128ff8 Add autocomplete to sign in/up forms 2022-12-09 17:26:08 -06:00
f49fe3962d Added progress bar for the Infisical Guide 2022-12-09 18:02:22 -05:00
9ee0c8f1b7 Fixed typescript issue for signupinvites 2022-12-09 17:20:17 -05:00
a9bd878057 Updated the start developing guide 2022-12-09 22:18:20 +01:00
a58f91f06b Merge branch 'main' of https://github.com/Infisical/infisical into bot 2022-12-09 13:23:52 -05:00
059f15b172 Merge branch 'main' of https://github.com/Infisical/infisical 2022-12-09 13:20:07 -05:00
caddb45394 Revise integrations docs 2022-12-09 13:00:07 -05:00
8266c4dd6d Fixed the undefined private key issue 2022-12-09 12:45:47 -05:00
9f82220f4e Remove frontend/backend sync operation for envars to integrations in favor of bot 2022-12-09 10:59:54 -05:00
3d25baa319 Add parseInt to token expiration 2022-12-09 10:10:38 -05:00
a8dfcae777 Merge pull request #106 from wanjohiryan/patch-2 2022-12-09 08:34:35 -05:00
228c8a7609 Rectified typo in README :) 2022-12-09 11:27:25 +03:00
a763d8b8ed Delete actions folder 2022-12-08 23:55:33 -05:00
d4e0a4992c Merge branch 'main' of https://github.com/Infisical/infisical into bot 2022-12-08 23:27:08 -05:00
1757f0d690 Complete v1 loop for bot-based integrations 2022-12-08 23:22:44 -05:00
b25908d91f add mono repo support 2022-12-08 19:02:39 -05:00
68d51d402a update docker docs, fix links, add export cmd 2022-12-08 19:02:17 -05:00
aa218d2ddc Update email regex 2022-12-08 11:23:53 -05:00
c36aa3591a Merge branch 'main' of https://github.com/Infisical/infisical into main 2022-12-08 09:01:36 -05:00
58b252a9e9 Loosen email regex 2022-12-08 09:01:24 -05:00
dba4c03e37 Merge pull request #102 from Infisical/mv-turtle-patch-1
Update README.md
2022-12-07 23:49:35 -05:00
7b1be82bac Update README.md 2022-12-07 23:45:29 -05:00
3c449750d3 Merge pull request #92 from asharonbaltazar/feat-add-notif-timeout
Add Notification Timeout
2022-12-07 19:59:39 -05:00
006f61a0e8 Merge pull request #99 from reginaldbondoc/add-initial-quality-gate
Add PR gate workflows
2022-12-07 18:13:34 -05:00
ec85bfca04 Fix frontend build-arg 2022-12-07 23:45:19 +01:00
27353848c1 Add PR gate workflows 2022-12-07 23:25:41 +01:00
da6a8ccdea fix typo for layout 2022-12-07 16:45:45 -05:00
b5a4e42281 Merge pull request #98 from reginaldbondoc/main
Fix workspace type definition
2022-12-07 16:33:25 -05:00
1212b5a9db Change name 2022-12-07 22:23:47 +01:00
d99e21a91b Fix workspace type 2022-12-07 22:20:25 +01:00
112fc77a06 Merge branch 'main' of https://github.com/Infisical/infisical 2022-12-07 15:08:52 -05:00
c23be77738 Fixed the typescript error in Layout 2022-12-07 15:08:44 -05:00
0cea019bc2 Update push to docker action 2022-12-07 14:26:56 -05:00
1218dc09ed Merge pull request #93 from Infisical/snyk-upgrade-5c827737012080ebf6157c6e6fcd4f49
[Snyk] Upgrade typescript from 4.8.4 to 4.9.3
2022-12-07 13:47:22 -05:00
b89a221a5a Merge pull request #94 from Infisical/snyk-upgrade-5d7f8d449135ab93a65e39dda5bbc136
[Snyk] Upgrade @sentry/tracing from 7.17.4 to 7.19.0
2022-12-07 13:47:03 -05:00
c4124cc865 Merge branch 'main' into snyk-upgrade-5d7f8d449135ab93a65e39dda5bbc136 2022-12-07 13:46:50 -05:00
94b7a0aead Added another contributing option to readme 2022-12-07 13:44:11 -05:00
689f1d0d43 Merge pull request #95 from Infisical/snyk-upgrade-2f45d7571b1d2d04216e214a24b9e6c3
[Snyk] Upgrade @sentry/node from 7.17.4 to 7.19.0
2022-12-07 13:43:39 -05:00
c75c24d44e Merge pull request #96 from Infisical/snyk-upgrade-35bcf68242318cba0ddafd09b0bc335e
[Snyk] Upgrade express-rate-limit from 6.6.0 to 6.7.0
2022-12-07 13:43:26 -05:00
febdf48dea Merge pull request #97 from Infisical/snyk-upgrade-7100c673d3b4c2351515934460524fde
[Snyk] Upgrade mongoose from 6.7.1 to 6.7.2
2022-12-07 13:43:10 -05:00
688b383d8b Merge pull request #80 from reginaldbondoc/I-36-use-pre-built-frontend-image
I-36 Use pre-built frontend image instead of building Next.js app on boot
2022-12-07 13:36:43 -05:00
9436f40eac Merge pull request #90 from edgarrmondragon/feat/export-cmd
Add an `export` command
2022-12-07 13:35:26 -05:00
d45eff621b fix: upgrade mongoose from 6.7.1 to 6.7.2
Snyk has created this PR to upgrade mongoose from 6.7.1 to 6.7.2.

See this package in npm:
https://www.npmjs.com/package/mongoose

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-12-07 18:15:40 +00:00
761a60a216 fix: upgrade express-rate-limit from 6.6.0 to 6.7.0
Snyk has created this PR to upgrade express-rate-limit from 6.6.0 to 6.7.0.

See this package in npm:
https://www.npmjs.com/package/express-rate-limit

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-12-07 18:15:35 +00:00
0466bf4e3d fix: upgrade @sentry/node from 7.17.4 to 7.19.0
Snyk has created this PR to upgrade @sentry/node from 7.17.4 to 7.19.0.

See this package in npm:
https://www.npmjs.com/package/@sentry/node

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-12-07 18:15:31 +00:00
16a1366e6c fix: upgrade @sentry/tracing from 7.17.4 to 7.19.0
Snyk has created this PR to upgrade @sentry/tracing from 7.17.4 to 7.19.0.

See this package in npm:
https://www.npmjs.com/package/@sentry/tracing

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-12-07 18:15:27 +00:00
b06c8d241e fix: upgrade typescript from 4.8.4 to 4.9.3
Snyk has created this PR to upgrade typescript from 4.8.4 to 4.9.3.

See this package in npm:
https://www.npmjs.com/package/typescript

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-12-07 18:15:22 +00:00
7bbaf4fee8 feat: move notifs to bottom right 2022-12-07 12:16:53 -05:00
91c8fd14df feat: add notif type 2022-12-07 12:15:17 -05:00
9e1112eb52 fix: hide notifs wrapper if no notifs 2022-12-07 12:04:53 -05:00
6050e65a59 fix: notif type is optional 2022-12-07 12:02:01 -05:00
bbc23aca55 Use encoding/csv to build the CSV output 2022-12-07 10:59:10 -06:00
9098bdc751 refactor: Change type to ensure notifs within state are Required<> 2022-12-07 11:58:17 -05:00
251426b559 feat: Add notification timeout 2022-12-07 11:57:39 -05:00
793eaee0c6 Address PR feedback 2022-12-07 10:54:24 -06:00
349865e6ef fix: clearNotification now requires string 2022-12-07 11:52:51 -05:00
add3075439 feat: create prop for Notification; small readability changes 2022-12-07 11:52:18 -05:00
3db1ff2411 modify error model 2022-12-07 11:06:30 -05:00
8cd9e20fa6 Update readme star gif 2022-12-07 09:30:38 -05:00
29ab13430f Merge branch 'main' into I-36-use-pre-built-frontend-image 2022-12-07 09:18:50 +01:00
90eb292721 Add an export command 2022-12-07 01:44:06 -06:00
5e1f6d3884 Merge pull request #84 from SH5H/main
Convert JS to TS
2022-12-06 23:36:56 -05:00
1310b176a9 Update README.md 2022-12-06 21:23:28 -05:00
ccf1010e94 Merge pull request #82 from asharonbaltazar/feat-error-notifs
Add Error Notifications
2022-12-06 21:09:07 -05:00
d46bf54a8d fix: wrap pushKeys args in obj 2022-12-06 21:07:40 -05:00
93703475fe Limit replacement to JS files only 2022-12-06 22:45:26 +01:00
991b10cc17 Convert JS to TS 2022-12-07 04:58:27 +09:00
60fcd34af5 Merge branch 'main' into feat-error-notifs 2022-12-06 13:10:31 -05:00
f60e0cf7ee feat: separate components from Notification Provider 2022-12-06 12:50:49 -05:00
9071fafd06 feat: fix import paths 2022-12-06 12:50:09 -05:00
9499aa1097 feat: move NotificationProvider into Notifications folder 2022-12-06 12:49:44 -05:00
389bf0b41f feat: add NotificationProvider to _app.tsx 2022-12-06 12:26:19 -05:00
6ed5b9e706 feat: create initial NotificationProvider component 2022-12-06 12:25:55 -05:00
209673d744 Merge branch 'main' into I-36-use-pre-built-frontend-image 2022-12-06 18:13:10 +01:00
baacc310bb Merge pull request #77 from SH5H/jsconvert
Convert JS to TS
2022-12-06 09:04:50 -05:00
1e16a18469 Merge branch 'main' of https://github.com/Infisical/infisical 2022-12-06 02:28:40 -05:00
df1ade4f5f Changed [NAME]'s project to Example Project 2022-12-06 02:28:34 -05:00
944cc5b32c Fixed the bug with start keys 2022-12-06 02:27:28 -05:00
46fe724012 Begin developing bot, event, and integration pipeline 2022-12-06 00:23:16 -05:00
a83d536ea4 Improve GH Action workflow for building images 2022-12-06 00:17:24 +01:00
1454911085 Merge branch 'Infisical:main' into I-36-use-pre-built-frontend-image 2022-12-05 23:40:14 +01:00
9e73b3431e Delete workspace type file 2022-12-06 06:15:58 +09:00
4a105a72e9 Update comment for new util 2022-12-06 05:55:14 +09:00
a47decd31f Convert JS to TS 2022-12-06 05:53:34 +09:00
c5a422fe64 update self host docs 2022-12-05 15:26:20 -05:00
bb47f7a92f Pre-bake some vars and change telemetry handling 2022-12-05 21:17:59 +01:00
13f2ab9425 update steps number 2022-12-05 14:41:09 -05:00
ac2c50b161 Merge pull request #76 from Infisical/upload-helm-chart-action
Upload helm chart action and docs for k8
2022-12-05 14:33:03 -05:00
afb374ff13 Add docs for k8 support 2022-12-05 14:32:16 -05:00
e98b76cba5 remove manual namespace 2022-12-05 12:57:18 -05:00
3e2ed62e50 update readme of helm repo 2022-12-05 12:31:48 -05:00
8e15dfc3d9 Merge pull request #73 from Infisical/upload-helm-chart-action
Upload helm chart action
2022-12-05 12:20:32 -05:00
6fb22b68dd update workflow file name 2022-12-05 12:17:06 -05:00
05a19a2201 Rewrite upload steps action 2022-12-05 12:13:52 -05:00
9ee5f3d41b upload helm chart to cloudsmith 2022-12-05 12:04:23 -05:00
142a38ae3c Merge branch 'main' into I-36-use-pre-built-frontend-image 2022-12-05 17:50:11 +01:00
e67620c3ce helm release on dispatch 2022-12-05 10:48:18 -05:00
e8e6b72422 pause helm chart release 2022-12-05 10:46:56 -05:00
4cc4edcb7e Merge branch 'main' of https://github.com/Infisical/infisical into main 2022-12-05 10:45:09 -05:00
2acea4f085 push new cli release to populate release page 2022-12-05 10:45:02 -05:00
0dd546813a Create helm-chart-release.yaml 2022-12-05 10:37:33 -05:00
d82dfa5504 create helm repo install file 2022-12-05 10:28:49 -05:00
b13b0693ba Updated menu in docs 2022-12-05 00:11:47 -05:00
e00c3ab9e2 Expanded contributing docs 2022-12-05 00:07:03 -05:00
088668e1b0 Fix let -> const 2022-12-04 23:18:39 -05:00
b21cb521da Tranforming more components to typescript (#47) 2022-12-04 23:03:57 -05:00
90ef67399b Merge branch 'main' of https://github.com/Infisical/infisical into main 2022-12-04 22:37:59 -05:00
5e87a317fa helm mongo db deployment 2022-12-04 22:37:51 -05:00
6cdbc834ab Tranforming more components to typescript (#47) 2022-12-04 22:14:13 -05:00
b677ab6429 feat: add react TS types 2022-12-04 22:03:07 -05:00
3b002abcb6 Merge branch 'main' of https://github.com/Infisical/infisical into main 2022-12-04 17:57:35 -05:00
89c625750a complete helm chart without DB 2022-12-04 17:57:28 -05:00
3de79426e2 Merge pull request #71 from Infisical/maidul98-fixing-dockerhub-upload
Update docker-image.yml to push to docker hub
2022-12-04 13:11:54 -05:00
02284f350b Update docker-image.yml 2022-12-04 13:05:16 -05:00
0e011da41c Update docker-image.yml 2022-12-04 13:01:38 -05:00
4fca41f62c Update docker-image.yml 2022-12-04 12:59:37 -05:00
f7044d37cb Update docker-image.yml 2022-12-04 12:55:10 -05:00
2299cff7d8 Update docker-image.yml 2022-12-04 12:52:29 -05:00
9c66062e6e Update docker-image.yml 2022-12-04 12:49:17 -05:00
127f77d1ce Update docker-image.yml 2022-12-04 12:46:15 -05:00
040fa815df Merge pull request #70 from SH5H/lintRule
Add Lint rules
2022-12-04 10:36:29 -05:00
0eff4a7389 Change eslint config file format 2022-12-05 00:16:05 +09:00
43bf99e659 Add eslint rules 2022-12-05 00:15:22 +09:00
3bb3fd3531 Added another quick start step 2022-12-04 09:22:12 -05:00
2a1cb7c00d Merge branch 'main' of https://github.com/Infisical/infisical 2022-12-04 08:26:41 -05:00
74467320cb Updated onboarding example secrets 2022-12-04 08:26:38 -05:00
86b12b16bf Merge pull request #66 from SH5H/main
Update dashboard component
2022-12-04 07:38:42 -05:00
a5d509c541 Change DropZone component to tsx 2022-12-04 20:46:23 +09:00
249635f0cc Remove useless type 2022-12-04 20:45:56 +09:00
20d8d255cb Added a quick start guide for new users 2022-12-03 23:23:32 -05:00
57762ab73c Merge branch 'main' of https://github.com/Infisical/infisical into main 2022-12-03 20:29:48 -05:00
656d375af0 add frontend/backend deployment Helm 2022-12-03 20:29:42 -05:00
f5035a4169 Update README.md 2022-12-03 20:10:56 -05:00
5f2d3056f1 Merge branch 'main' of https://github.com/Infisical/infisical into main 2022-12-03 19:59:26 -05:00
fa41baa8b6 Update dashboard component 2022-12-04 09:58:51 +09:00
fe15af4c28 add Helm chart 2022-12-03 19:58:23 -05:00
986a9449cf Merge pull request #63 from SH5H/fixBabelError
Fix babel error
2022-12-03 18:46:49 -05:00
de7e5016dd Fix babel error 2022-12-04 08:32:43 +09:00
8bbd5a1184 Fix typo in readme 2022-12-04 08:32:33 +09:00
212f1feeb6 Update README.md 2022-12-03 14:21:56 -05:00
4ae88b2e47 Fix merge conflict 2022-12-03 17:14:48 +01:00
43fb35381f Merge branch 'I-36-use-pre-built-frontend-image' of github.com:reginaldbondoc/infisical into I-36-use-pre-built-frontend-image 2022-12-03 16:24:05 +01:00
023c744a8e Merge branch 'main' into I-36-use-pre-built-frontend-image 2022-12-03 13:11:23 +01:00
da419361cb Update README.md 2022-12-02 12:23:20 -05:00
42087923e0 remove scrollbars 2022-12-01 09:12:48 -05:00
906cedd168 remove scrollbars 2022-12-01 09:07:13 -05:00
f659be446d Fixed the UI bug with overflowing dots (#30) 2022-11-30 21:21:50 -05:00
63c4cfa651 Merge branch 'main' of https://github.com/Infisical/infisical 2022-11-30 20:55:35 -05:00
adf27351a3 Updated layout component 2022-11-30 20:55:32 -05:00
c1d4002551 Add a contributor to README.md 2022-11-30 19:46:10 -05:00
1c56c34211 Merge pull request #59 from reginaldbondoc/I-36-remove-dependency-to-path-const
I-36 Remove dependency to PATH const
2022-11-30 18:48:29 -05:00
d8aa5b5ff4 I-36 Remove dependency to PATH const 2022-11-30 23:39:23 +01:00
a5618681df Merge branch 'Infisical:main' into I-36-use-pre-built-frontend-image 2022-11-30 22:46:52 +01:00
a84fc847db Merge branch 'main' into I-36-use-pre-built-frontend-image 2022-11-30 22:10:27 +01:00
f8e7c3c7c6 Update menu sidebar design 2022-11-30 15:14:32 -05:00
f9bf418bf8 Fix the issues with scrolling in Dashboard Input Field 2022-11-30 14:36:26 -05:00
7950085fba hotfix: remove placeholder for dashboard input field 2022-11-30 08:45:19 -05:00
9e0860b9a6 hotfix: remove placeholder for dashboard input field 2022-11-30 08:43:04 -05:00
c3427d110a Change CSP references from SITE_URL to self 2022-11-29 23:41:31 -05:00
0fde680a11 Fixed the useRef issue 2022-11-29 22:27:41 -05:00
ef248e3944 Fixed the issue with overflowing secrets (#30) 2022-11-29 22:11:47 -05:00
c940e1ad16 Merge branch 'main' of https://github.com/Infisical/infisical 2022-11-29 20:27:44 -05:00
aa700f7670 Fixed the issue with dependencies order 2022-11-29 20:27:40 -05:00
f30da163d8 Modify integration prerequisite steps 2022-11-29 20:09:13 -05:00
1f63454f8d Merge branch 'main' of https://github.com/Infisical/infisical 2022-11-29 20:02:16 -05:00
8e2794f6ab Update the list of integrations in the app 2022-11-29 20:02:14 -05:00
6468b356a6 Add Django, Rails integration instructions to docs and move cli login to overview page 2022-11-29 19:38:41 -05:00
c6777e43ed Add contributors to README.md 2022-11-29 17:00:47 -05:00
c68eaa613c Use pre-built frontend image in prod compose file 2022-11-29 21:09:50 +01:00
00dde5c2b4 Remove Angular integration from docs (not applicable), patch stripe subscriptions attr in billing page 2022-11-29 11:39:38 -05:00
ad19e33638 Merge pull request #57 from 0xflotus/patch-1
fix: small typo error
2022-11-29 10:22:42 -05:00
4117781cd1 fix: small typo error 2022-11-29 16:18:17 +01:00
11d169ad23 Add Nuxt, Remix, Vue framework integration instructions to docs 2022-11-29 01:22:06 -05:00
91827aed3e Fixed the issue with z-index for menu popups 2022-11-28 23:37:54 -05:00
fe339d9c0f Merge branch 'main' of https://github.com/Infisical/infisical 2022-11-28 23:20:53 -05:00
e818029b48 Remove apostrophes because of eslint 2022-11-28 23:20:48 -05:00
4f5ad07ace Add more framework integrations to docs 2022-11-28 22:34:31 -05:00
3ece5a0390 Update README.md 2022-11-28 21:38:33 -05:00
078dbde45a Update README.md 2022-11-28 21:37:47 -05:00
8953fdf1d8 Updated integrations in README.md 2022-11-28 21:12:10 -05:00
bccee0c94d change link for infisical cloud to infisical.com instead of sign up page 2022-11-28 19:28:54 -05:00
7447288e5c Add integrations anchor and preliminary framework integrations to docs 2022-11-28 09:57:17 -05:00
7ab2289c99 Added more function specifications 2022-11-28 09:04:19 -05:00
1b07199383 Merge branch 'main' of https://github.com/Infisical/infisical 2022-11-28 08:41:55 -05:00
3c7cd01dd5 Converted crypto.js to typescript (#47) 2022-11-28 08:41:47 -05:00
4cfb275186 Merge branch 'main' into docs 2022-11-27 23:38:24 -05:00
0a9f4ffc4d Restructure docs around new anchors 2022-11-27 23:36:50 -05:00
e18a44f723 Delete linter.yml 2022-11-27 22:39:54 -05:00
62c2be255d Add a badge to README.md 2022-11-27 22:32:15 -05:00
3246d6b6df Removed useless pictures 2022-11-27 22:23:40 -05:00
5f670cd104 disable linter
disable linter  as there are issues with analyzing go
2022-11-27 21:44:34 -05:00
6722bd7bea Converted Error and ListBox to typescript (#47) 2022-11-27 20:51:41 -05:00
56acc4f888 Create linter.yml 2022-11-27 20:27:58 -05:00
798eb67296 Merge branch 'main' of https://github.com/Infisical/infisical 2022-11-27 19:10:55 -05:00
2fe45ec898 Added frontend checks for secret names (#35) 2022-11-27 19:10:49 -05:00
4d0969fbc3 Remove swagger in favor of Mintlify 2022-11-27 18:32:00 -05:00
c4711fc328 Merge branch 'main' into swagger 2022-11-27 15:58:31 -05:00
938c7bdb93 Initialize Swagger in dev, document /login1 for demonstration 2022-11-27 15:57:45 -05:00
634d5fe5c3 Converted Button component to typescript (#47) 2022-11-27 14:32:27 -05:00
1961b92340 Merge pull request #45 from gangjun06/refactor2
Edit frontend .prettierrc config
2022-11-27 08:29:20 -05:00
ef234a270f lint(frontend): apply two space instead of tab 2022-11-27 16:17:21 +09:00
66d2a2724e ref(frontend): add ~/const path 2022-11-27 16:12:25 +09:00
2729b409e6 lint(frontend): edit .prettierrc config 2022-11-27 15:51:34 +09:00
f5d2836199 add close inactive issues
will be marked stale after 30 days and if 14 days after it is still stale then it will be closed.
2022-11-26 18:10:46 -05:00
065b37ac11 Merge branch 'main' of https://github.com/Infisical/infisical 2022-11-26 17:55:31 -05:00
ca3b2fa1a0 Update Heroku docs 2022-11-26 17:55:26 -05:00
4ea284a1c5 Patch passing through stripe envars into frontend 2022-11-26 17:54:52 -05:00
527 changed files with 42043 additions and 20953 deletions

View File

@ -1,21 +1,19 @@
# Keys
# Required keys for platform encryption/decryption ops
PRIVATE_KEY=replace_with_nacl_sk
PUBLIC_KEY=replace_with_nacl_pk
ENCRYPTION_KEY=replace_with_lengthy_secure_hex
# Required key for platform encryption/decryption ops
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
# JWT
# Required secrets to sign JWT tokens
JWT_SIGNUP_SECRET=replace_with_lengthy_secure_hex
JWT_REFRESH_SECRET=replace_with_lengthy_secure_hex
JWT_AUTH_SECRET=replace_with_lengthy_secure_hex
JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a
JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff
JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
# JWT lifetime
# Optional lifetimes for JWT tokens expressed in seconds or a string
# describing a time span (e.g. 60, "2 days", "10h", "7d")
JWT_AUTH_LIFETIME=
JWT_REFRESH_LIFETIME=
JWT_SERVICE_SECRET=
JWT_SIGNUP_LIFETIME=
# Optional lifetimes for OTP expressed in seconds
@ -33,21 +31,28 @@ MONGO_PASSWORD=example
# Website URL
# Required
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_NAME=Team
SMTP_USERNAME=team@infisical.com
SMTP_PASSWORD=
SMTP_HOST= # required
SMTP_USERNAME= # required
SMTP_PASSWORD= # required
SMTP_PORT=587
SMTP_SECURE=false
SMTP_FROM_ADDRESS= # required
SMTP_FROM_NAME=Infisical
# 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_ID_GITHUB=
CLIENT_SECRET_HEROKU=
CLIENT_SECRET_VERCEL=
CLIENT_SECRET_NETLIFY=
CLIENT_SECRET_GITHUB=
CLIENT_SLUG_VERCEL=
# Sentry (optional) for monitoring errors
SENTRY_DSN=

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
built
healthcheck.js

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View 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
View 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/be-test-report.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: "Backend Test Report"
on:
workflow_run:
workflows: ["Check Backend Pull Request"]
types:
- completed
jobs:
be-report:
name: Backend test report
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 📁 Download test results
id: download-artifact
uses: dawidd6/action-download-artifact@v2
with:
name: be-test-results
path: backend
workflow: check-be-pull-request.yml
workflow_conclusion: success
- name: 📋 Publish test results
uses: dorny/test-reporter@v1
with:
name: Test Results
path: reports/jest-*.xml
reporter: jest-junit
working-directory: backend
- name: 📋 Publish coverage
uses: ArtiomTr/jest-coverage-report-action@v2
id: coverage
with:
output: comment, report-markdown
coverage-file: coverage/report.json
github-token: ${{ secrets.GITHUB_TOKEN }}
working-directory: backend
- uses: marocchino/sticky-pull-request-comment@v2
with:
message: ${{ steps.coverage.outputs.report }}

View File

@ -0,0 +1,42 @@
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: 📁 Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: be-test-results
path: |
./backend/reports
./backend/coverage
- name: 🏗️ Run build
run: npm run build
working-directory: backend

View 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

View File

@ -3,32 +3,84 @@ name: Push to Docker Hub
on: [workflow_dispatch]
jobs:
docker:
backend-image:
name: Build backend image
runs-on: ubuntu-latest
steps:
-
name: Set up QEMU
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 🔧 Set up QEMU
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-
name: Login to Docker Hub
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push backend
- 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
context: backend
tags: infisical/backend:latest
-
name: Build and push frontend
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
context: frontend
tags: infisical/frontend:latest
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}

View 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 }}

View File

@ -1,4 +1,4 @@
name: goreleaser
name: Go releaser
on:
push:
@ -13,7 +13,7 @@ permissions:
jobs:
goreleaser:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
with:
@ -24,6 +24,15 @@ jobs:
go-version: '>=1.19.3'
cache: true
cache-dependency-path: cli/go.sum
- name: libssl1.1 => libssl1.0-dev for OSXCross
run: |
echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
sudo apt update && apt-cache policy libssl1.0-dev
sudo apt-get install libssl1.0-dev
- name: OSXCross for CGO Support
run: |
mkdir ../../osxcross
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
- uses: goreleaser/goreleaser-action@v2
with:
distribution: goreleaser

View 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

4
.gitignore vendored
View File

@ -25,7 +25,9 @@ node_modules
.env
# testing
/coverage
coverage
reports
junit.xml
# next.js
/.next/

View File

@ -7,12 +7,23 @@
# # you may remove this if you don't need go generate
# - cd cli && go generate ./...
builds:
- env:
- CGO_ENABLED=0
- id: darwin-build
binary: infisical
id: infisical
env:
- CGO_ENABLED=1
- CC=/home/runner/work/osxcross/target/bin/o64-clang
- CXX=/home/runner/work/osxcross/target/bin/o64-clang++
goos:
- darwin
ignore:
- goos: darwin
goarch: "386"
dir: ./cli
- id: all-other-builds
env:
- CGO_ENABLED=0
binary: infisical
goos:
- freebsd
- linux
- netbsd
@ -27,8 +38,6 @@ builds:
- 6
- 7
ignore:
- goos: darwin
goarch: "386"
- goos: windows
goarch: "386"
- goos: freebsd
@ -71,7 +80,7 @@ nfpms:
- id: infisical
package_name: infisical
builds:
- infisical
- all-other-builds
vendor: Infisical, Inc
homepage: https://infisical.com/
maintainer: Infisical, Inc
@ -81,6 +90,7 @@ nfpms:
- rpm
- deb
- apk
- archlinux
bindir: /usr/bin
scoop:
bucket:

5
.husky/pre-commit Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

290
README.md
View File

@ -1,5 +1,5 @@
<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">
@ -8,7 +8,7 @@
<h4 align="center">
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">Slack</a> |
<a href="https://infisical.com/signup">Infisical Cloud</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>
@ -21,21 +21,27 @@
<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="">
<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, 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 team's environment variables within projects
- **[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/integrations/heroku)** with CI/CD and production infrastructure (Heroku available, more 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**
@ -45,35 +51,43 @@
And more.
## Get started
## 🚀 Get started
To quickly get started, visit our [get started guide](https://infisical.com/docs/getting-started/introduction).
## What's cool about this?
<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>
Infisical is simple, E2EE, and (soon to be) complete.
## 🔥 What's cool about this?
Infisical makes secret management simple and end-to-end encrypted by default. We're on a mission to make it more accessible to all developers, <i>not just security teams</i>.
According to a [report](https://www.ekransystem.com/en/blog/secrets-management) in 2019, only 10% of organizations use secret management solutions despite all using digital secrets to some extent.
We're on a mission to make secret management more accessible to everyone — that means building for developers, not just security teams.
If you care about efficiency and security, then Infisical is right for you.
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.
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.
## Contributing
## 🌱 Contributing
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).
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!)!
Not sure where to get started? You can:
## Community & Support
- [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.
- [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-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g) for hanging out with the community and quick communication with the team.
## 💚 Community & Support
## Status
- [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.
@ -81,37 +95,225 @@ Not sure where to get started? [Book a free, non-pressure pairing sessions with
We're currently in Public Alpha.
## Stay Up-to-Date
## 🔌 Integrations
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! :)
<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">
<a href="https://infisical.com/docs/integrations/cicd/githubactions">
✔️ GitHub Actions
</a>
</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 (https://github.com/Infisical/infisical/issues/134)
</td>
<td align="left" valign="middle">
🔜 CircleCI (https://github.com/Infisical/infisical/issues/91)
</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">
<a href="https://infisical.com/docs/integrations/cloud/netlify">
✔️ Netlify
</a>
</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">
🔜 Render (https://github.com/Infisical/infisical/issues/132)
</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
Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of new features are coming very quickly. Watch **releases** of this repository to be notified about future updates:
![infisical-star-github](https://github.com/Infisical/infisical/blob/main/.github/images/star-infisical.gif?raw=true)
## Integrations
We're currently setting the foundation and building integrations so secrets can be synced everywhere. Any help is welcome! :)
- [x] [Docker](https://infisical.com/docs/integrations/docker)
- [x] [Docker Compose](https://infisical.com/docs/integrations/docker-compose)
- [x] [Heroku](https://infisical.com/docs/integrations/heroku)
- [ ] Vercel
- [ ] Kubernetes
- [ ] AWS
- [ ] GCP
- [ ] Azure
- [ ] Digital Ocean
- [ ] GitLab
- [ ] CircleCI
## Open-source vs. paid
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.
## 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.
## Contributors 🦸
## 🦸 Contributors
[//]: contributor-faces
@ -119,4 +321,4 @@ Looking to report a security vulnerability? Please don't post about it in GitHub
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/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/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a>
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>

View File

@ -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
}
}

View File

@ -1,7 +0,0 @@
{
"semi": true,
"trailingComma": "none",
"singleQuote": true,
"printWidth": 80,
"useTabs": true
}

View File

@ -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"]

View File

@ -0,0 +1,19 @@
import { server } from '../src/app';
import { describe, expect, it, beforeAll, afterAll } from '@jest/globals';
import supertest from 'supertest';
import { setUpHealthEndpoint } from '../src/services/health';
const requestWithSupertest = supertest(server);
describe('Healthcheck endpoint', () => {
beforeAll(async () => {
setUpHealthEndpoint(server);
});
afterAll(async () => {
server.close();
});
it('GET /healthcheck should return OK', async () => {
const res = await requestWithSupertest.get('/healthcheck');
expect(res.status).toEqual(200);
});
});

View File

@ -14,12 +14,16 @@ 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;
VERBOSE_ERROR_OUTPUT: string;
LOKI_HOST: 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;

24
backend/healthcheck.js Normal file
View 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();

2119
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,11 @@
{
"dependencies": {
"@godaddy/terminus": "^4.11.2",
"@octokit/rest": "^19.0.5",
"@sentry/node": "^7.14.0",
"@sentry/tracing": "^7.14.0",
"@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"axios": "^1.1.3",
"bigint-conversion": "^2.2.2",
"cookie-parser": "^1.4.6",
@ -10,32 +13,40 @@
"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",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
"mongoose": "^6.7.1",
"libsodium-wrappers": "^0.7.10",
"mongoose": "^6.7.2",
"nodemailer": "^6.8.0",
"posthog-node": "^2.1.0",
"query-string": "^7.1.1",
"posthog-node": "^2.2.2",
"query-string": "^7.1.3",
"rimraf": "^3.0.2",
"stripe": "^10.7.0",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"typescript": "^4.8.4"
"typescript": "^4.9.3",
"winston": "^3.8.2",
"winston-loki": "^6.0.6"
},
"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",
"pretest": "docker compose -f test-resources/docker-compose.test.yml up -d",
"test": "cross-env NODE_ENV=test jest --testTimeout=10000 --detectOpenHandles",
"test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json",
"posttest": "docker compose -f test-resources/docker-compose.test.yml down"
},
"repository": {
"type": "git",
@ -49,24 +60,49 @@
"homepage": "https://github.com/Infisical/infisical-api#readme",
"description": "",
"devDependencies": {
"@jest/globals": "^29.3.1",
"@posthog/plugin-scaffold": "^1.3.4",
"@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.14",
"@types/jest": "^29.2.4",
"@types/jsonwebtoken": "^8.5.9",
"@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6",
"@types/supertest": "^2.0.12",
"@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",
"cross-env": "^7.0.3",
"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",
"jest-junit": "^15.0.0",
"nodemon": "^2.0.19",
"npm": "^8.19.3",
"prettier": "^2.7.1",
"supertest": "^6.3.3",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"collectCoverageFrom": [
"src/*.{js,ts}",
"!**/node_modules/**"
],
"setupFiles": [
"<rootDir>/test-resources/env-vars.js"
]
},
"jest-junit": {
"outputDirectory": "reports",
"outputName": "jest-junit.xml",
"ancestorSeparator": " ",
"uniqueOutputName": "false",
"suiteNameTemplate": "{filepath}",
"classNameTemplate": "{classname}",
"titleTemplate": "{title}"
}
}

91
backend/src/app.ts Normal file
View File

@ -0,0 +1,91 @@
import { patchRouterParam } from './utils/patchAsyncRoutes';
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import dotenv from 'dotenv';
dotenv.config();
import { PORT, NODE_ENV, SITE_URL } from './config';
import { apiLimiter } from './helpers/rateLimiter';
import {
signup as signupRouter,
auth as authRouter,
bot as botRouter,
organization as organizationRouter,
workspace as workspaceRouter,
membershipOrg as membershipOrgRouter,
membership as membershipRouter,
key as keyRouter,
inviteOrg as inviteOrgRouter,
user as userRouter,
userAction as userActionRouter,
secret as secretRouter,
serviceToken as serviceTokenRouter,
password as passwordRouter,
stripe as stripeRouter,
integration as integrationRouter,
integrationAuth as integrationAuthRouter
} from './routes';
import { getLogger } from './utils/logger';
import { RouteNotFoundError } from './utils/errors';
import { requestErrorHandler } from './middleware/requestErrorHandler';
//* Patch Async route params to handle Promise Rejections
patchRouterParam()
export const app = express();
app.enable('trust proxy');
app.use(express.json());
app.use(cookieParser());
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());
}
// 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);
app.use('/api/v1/workspace', workspaceRouter);
app.use('/api/v1/membership-org', membershipOrgRouter);
app.use('/api/v1/membership', membershipRouter);
app.use('/api/v1/key', keyRouter);
app.use('/api/v1/invite-org', inviteOrgRouter);
app.use('/api/v1/secret', secretRouter);
app.use('/api/v1/service-token', serviceTokenRouter);
app.use('/api/v1/password', passwordRouter);
app.use('/api/v1/stripe', stripeRouter);
app.use('/api/v1/integration', integrationRouter);
app.use('/api/v1/integration-auth', integrationAuthRouter);
//* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next)=>{
if(res.headersSent) return next();
next(RouteNotFoundError({message: `The requested source '(${req.method})${req.url}' was not found`}))
})
//* Error Handling Middleware (must be after all routing logic)
app.use(requestErrorHandler)
export const server = app.listen(PORT, () => {
getLogger("backend-main").info(`Server started listening at port ${PORT}`)
});

View File

@ -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,56 +10,78 @@ 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 VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true;
const LOKI_HOST = process.env.LOKI_HOST || undefined;
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_ID_GITHUB = process.env.CLIENT_ID_GITHUB!;
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
const CLIENT_SECRET_GITHUB = process.env.CLIENT_SECRET_GITHUB!;
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 POSTHOG_PROJECT_API_KEY =
process.env.POSTHOG_PROJECT_API_KEY! ||
'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE';
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_NAME = process.env.SMTP_NAME!;
const SMTP_HOST = process.env.SMTP_HOST!;
const SMTP_SECURE = process.env.SMTP_SECURE! === 'true' || false;
const SMTP_PORT = parseInt(process.env.SMTP_PORT!) || 587;
const SMTP_USERNAME = process.env.SMTP_USERNAME!;
const SMTP_PASSWORD = process.env.SMTP_PASSWORD!;
const SMTP_FROM_ADDRESS = process.env.SMTP_FROM_ADDRESS!;
const SMTP_FROM_NAME = process.env.SMTP_FROM_NAME! || 'Infisical';
const STRIPE_PRODUCT_CARD_AUTH = process.env.STRIPE_PRODUCT_CARD_AUTH!;
const STRIPE_PRODUCT_PRO = process.env.STRIPE_PRODUCT_PRO!;
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 TELEMETRY_ENABLED = (process.env.TELEMETRY_ENABLED! !== 'false') && true;
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,
SITE_URL,
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,
TELEMETRY_ENABLED
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,
VERBOSE_ERROR_OUTPUT,
LOKI_HOST,
CLIENT_ID_HEROKU,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB,
CLIENT_SLUG_VERCEL,
POSTHOG_HOST,
POSTHOG_PROJECT_API_KEY,
SENTRY_DSN,
SITE_URL,
SMTP_HOST,
SMTP_PORT,
SMTP_SECURE,
SMTP_USERNAME,
SMTP_PASSWORD,
SMTP_FROM_ADDRESS,
SMTP_FROM_NAME,
STRIPE_PRODUCT_CARD_AUTH,
STRIPE_PRODUCT_PRO,
STRIPE_PRODUCT_STARTER,
STRIPE_PUBLISHABLE_KEY,
STRIPE_SECRET_KEY,
STRIPE_WEBHOOK_SECRET,
TELEMETRY_ENABLED
};

View File

@ -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'
});
}
};

View 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
});
};

View File

@ -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,

View File

@ -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'
});
}

View File

@ -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);

View File

@ -2,7 +2,6 @@ import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Key } from '../models';
import { findMembership } from '../helpers/membership';
import { PUBLIC_KEY } from '../config';
import { GRANTED } from '../variables';
/**
@ -17,16 +16,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,
@ -94,16 +83,4 @@ export const getLatestKey = async (req: Request, res: Response) => {
}
return res.status(200).send(resObj);
};
/**
* Return public key of Infisical
* @param req
* @param res
* @returns
*/
export const getPublicKeyInfisical = async (req: Request, res: Response) => {
return res.status(200).send({
publicKey: PUBLIC_KEY
});
};
};

View File

@ -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'
});
}

View File

@ -8,6 +8,7 @@ import {
STRIPE_PRODUCT_CARD_AUTH
} from '../config';
import Stripe from 'stripe';
const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01'
});

View File

@ -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'
});
}

View File

@ -7,8 +7,9 @@ import {
reformatPullSecrets
} from '../helpers/secret';
import { pushKeys } from '../helpers/key';
import { eventPushSecrets } from '../events';
import { EventService } from '../services';
import { ENV_SET } from '../variables';
import { postHogClient } from '../services';
interface PushSecret {
@ -60,7 +61,8 @@ export const pushSecrets = async (req: Request, res: Response) => {
workspaceId,
keys
});
if (postHogClient) {
postHogClient.capture({
event: 'secrets pushed',
@ -74,6 +76,13 @@ 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);
@ -192,7 +201,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
};
if (postHogClient) {
// capture secrets pushed event in production
// capture secrets pulled event in production
postHogClient.capture({
distinctId: req.serviceToken.user.email,
event: 'secrets pulled',

View File

@ -58,7 +58,8 @@ export const createServiceToken = async (req: Request, res: Response) => {
token = createToken({
payload: {
serviceTokenId: serviceToken._id.toString()
serviceTokenId: serviceToken._id.toString(),
workspaceId
},
expiresIn: expiresIn,
secret: JWT_SERVICE_SECRET

View File

@ -0,0 +1,5 @@
import { eventPushSecrets } from "./secret"
export {
eventPushSecrets
}

View 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
View 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;
}) => {
const 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
}

View 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
}

View File

@ -0,0 +1,358 @@
import * as Sentry from '@sentry/node';
import {
Bot,
Integration,
IntegrationAuth,
} from '../models';
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
import { BotService } from '../services';
import {
ENV_DEV,
EVENT_PUSH_SECRETS,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY
} from '../variables';
import { UnauthorizedRequestError } from '../utils/errors';
import RequestError from '../utils/requestError';
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
const res = await exchangeCode({
integration,
code
});
const 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({ // issue here?
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 UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
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);
if(err instanceof RequestError)
throw err
else
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 UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
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);
if(err instanceof RequestError)
throw err
else
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
}

View File

@ -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 };

View File

@ -1,6 +1,52 @@
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;
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors
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 +143,9 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
return deletedMembership;
};
export { addMemberships, findMembership, deleteMembership };
export {
validateMembership,
addMemberships,
findMembership,
deleteMembership
};

View File

@ -2,21 +2,10 @@ 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_FROM_NAME, SMTP_FROM_ADDRESS } from '../config';
import * as Sentry from '@sentry/node';
// create nodemailer transporter
const transporter = nodemailer.createTransport({
host: SMTP_HOST,
port: 587,
auth: {
user: SMTP_USERNAME,
pass: SMTP_PASSWORD
}
});
transporter
.verify()
.then(() => console.log('SMTP - Successfully connected'))
.catch((err) => console.log('SMTP - Failed to connect'));
let smtpTransporter: nodemailer.Transporter;
/**
* @param {Object} obj
@ -26,33 +15,38 @@ 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 smtpTransporter.sendMail({
from: `"${SMTP_FROM_NAME}" <${SMTP_FROM_ADDRESS}>`,
to: recipients.join(', '),
subject: subjectLine,
html: htmlToSend
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
}
};
export { sendMail };
const setTransporter = (transporter: nodemailer.Transporter) => {
smtpTransporter = transporter;
};
export { sendMail, setTransporter };

View File

@ -2,34 +2,35 @@ import rateLimit from 'express-rate-limit';
// 300 requests per 15 minutes
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 400,
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: 10,
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: 20,
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: 10,
standardHeaders: true,
legacyHeaders: false
windowMs: 60 * 60 * 1000,
max: 10,
standardHeaders: true,
legacyHeaders: false
});
export { apiLimiter, signupLimiter, loginLimiter, passwordLimiter };

View File

@ -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()
});

View File

@ -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
});

View File

@ -1,97 +1,25 @@
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 { PostHogClient } from './services';
import { PORT, SENTRY_DSN, NODE_ENV, MONGO_URL, SITE_URL, POSTHOG_PROJECT_API_KEY, POSTHOG_HOST, TELEMETRY_ENABLED } from './config';
import { apiLimiter } from './helpers/rateLimiter';
import { SENTRY_DSN, NODE_ENV, MONGO_URL } from './config';
import { server } from './app';
import { initDatabase } from './services/database';
import { setUpHealthEndpoint } from './services/health';
import { initSmtp } from './services/smtp';
import { setTransporter } from './helpers/nodemailer';
const app = express();
initDatabase(MONGO_URL);
Sentry.init({
dsn: SENTRY_DSN,
tracesSampleRate: 1.0,
debug: NODE_ENV === 'production' ? false : true,
environment: NODE_ENV
});
setUpHealthEndpoint(server);
// PostHogClient.init({
// projectApiKey: POSTHOG_PROJECT_API_KEY,
// host: POSTHOG_HOST,
// telemetryEnabled: TELEMETRY_ENABLED
// });
setTransporter(initSmtp());
import {
signup as signupRouter,
auth as authRouter,
organization as organizationRouter,
workspace as workspaceRouter,
membershipOrg as membershipOrgRouter,
membership as membershipRouter,
key as keyRouter,
inviteOrg as inviteOrgRouter,
user as userRouter,
userAction as userActionRouter,
secret as secretRouter,
serviceToken as serviceTokenRouter,
password as passwordRouter,
stripe as stripeRouter,
integration as integrationRouter,
integrationAuth as integrationAuthRouter
} 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);
});
if (NODE_ENV !== 'test') {
Sentry.init({
dsn: SENTRY_DSN,
tracesSampleRate: 1.0,
debug: NODE_ENV === 'production' ? false : true,
environment: NODE_ENV
});
}
connectWithRetry();
app.enable('trust proxy');
app.use(cookieParser());
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());
}
app.use(express.json());
// routers
app.use('/api/v1/signup', signupRouter);
app.use('/api/v1/auth', authRouter);
app.use('/api/v1/user', userRouter);
app.use('/api/v1/user-action', userActionRouter);
app.use('/api/v1/organization', organizationRouter);
app.use('/api/v1/workspace', workspaceRouter);
app.use('/api/v1/membership-org', membershipOrgRouter);
app.use('/api/v1/membership', membershipRouter);
app.use('/api/v1/key', keyRouter);
app.use('/api/v1/invite-org', inviteOrgRouter);
app.use('/api/v1/secret', secretRouter);
app.use('/api/v1/service-token', serviceTokenRouter);
app.use('/api/v1/password', passwordRouter);
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);
});

View File

@ -0,0 +1,213 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import { Octokit } from '@octokit/rest';
import { IIntegrationAuth } from '../models';
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_GITHUB_API_URL
} from '../variables';
interface GitHubApp {
name: string;
}
/**
* 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;
case INTEGRATION_GITHUB:
apps = await getAppsGithub({
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;
};
/**
* Return list of names of repositories for Github 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 getAppsGithub = async ({
integrationAuth,
accessToken
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
let apps;
try {
const octokit = new Octokit({
auth: accessToken
});
const repos = (await octokit.request(
'GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}',
{}
)).data;
apps = repos
.filter((a:any) => a.permissions.admin === true)
.map((a: any) => ({
name: a.name
})
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Github repos');
}
return apps;
};
export { getApps };

View File

@ -0,0 +1,286 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITHUB_API_URL
} from '../variables';
import {
SITE_URL,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB
} from '../config';
import { user } from '../routes';
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;
}
interface ExchangeCodeGithubResponse {
access_token: string;
scope: string;
token_type: string;
}
/**
* 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;
case INTEGRATION_GITHUB:
obj = await exchangeCodeGithub({
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;
const 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
};
};
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Github
* 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 Github API
* @returns {String} obj2.refreshToken - refresh token for Github API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeGithub = async ({ code }: { code: string }) => {
let res: ExchangeCodeGithubResponse;
try {
res = (
await axios.get(INTEGRATION_GITHUB_TOKEN_URL, {
params: {
client_id: CLIENT_ID_GITHUB,
client_secret: CLIENT_SECRET_GITHUB,
code: code,
redirect_uri: `${SITE_URL}/github`
},
headers: {
Accept: 'application/json'
}
})
).data;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Github');
}
return {
accessToken: res.access_token,
refreshToken: null,
accessExpiresAt: null
};
};
export { exchangeCode };

View 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
}

View File

@ -0,0 +1,77 @@
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;
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors
try {
const res = await axios.post(
INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_secret: CLIENT_SECRET_HEROKU
} as any)
);
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 };

View File

@ -0,0 +1,47 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import { IIntegrationAuth, IntegrationAuth, Integration } from '../models';
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
} 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;
case INTEGRATION_GITHUB:
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 };

View File

@ -0,0 +1,605 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import { Octokit } from '@octokit/rest';
// import * as sodium from 'libsodium-wrappers';
import sodium from 'libsodium-wrappers';
// const sodium = require('libsodium-wrappers');
import { IIntegration, IIntegrationAuth } from '../models';
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_GITHUB_API_URL
} from '../variables';
import { access, appendFile } from 'fs';
// TODO: need a helper function in the future to handle integration
// envar priorities (i.e. prioritize secrets within integration or those on Infisical)
/**
* Sync/push [secrets] to [app] in integration named [integration]
* @param {Object} obj
* @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;
case INTEGRATION_GITHUB:
await syncSecretsGitHub({
integration,
secrets,
accessToken
});
break;
}
} 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;
}) => {
try {
interface NetlifyValue {
id?: string;
context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production',
value: string;
}
interface NetlifySecret {
key: string;
values: NetlifyValue[];
}
interface NetlifySecretsRes {
[index: string]: NetlifySecret;
}
const getParams = new URLSearchParams({
context_name: 'all', // integration.context or all
site_id: integration.siteId
});
const res = (await axios.get(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
{
params: getParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
))
.data
.reduce((obj: any, secret: any) => ({
...obj,
[secret.key]: secret
}), {});
const newSecrets: NetlifySecret[] = []; // createEnvVars
const deleteSecrets: string[] = []; // deleteEnvVar
const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue
const updateSecrets: NetlifySecret[] = []; // setEnvVarValue
// identify secrets to create and update
Object.keys(secrets).map((key) => {
if (!(key in res)) {
// case: Infisical secret does not exist in Netlify -> create secret
newSecrets.push({
key,
values: [{
value: secrets[key],
context: integration.context
}]
});
} else {
// case: Infisical secret exists in Netlify
const contexts = res[key].values
.reduce((obj: any, value: NetlifyValue) => ({
...obj,
[value.context]: value
}), {});
if (integration.context in contexts) {
// case: Netlify secret value exists in integration context
if (secrets[key] !== contexts[integration.context].value) {
// case: Infisical and Netlify secret values are different
// -> update Netlify secret context and value
updateSecrets.push({
key,
values: [{
context: integration.context,
value: secrets[key]
}]
});
}
} else {
// case: Netlify secret value does not exist in integration context
// -> add the new Netlify secret context and value
updateSecrets.push({
key,
values: [{
context: integration.context,
value: secrets[key]
}]
});
}
}
})
// identify secrets to delete
// TODO: revise (patch case where 1 context was deleted but others still there
Object.keys(res).map((key) => {
// loop through each key's context
if (!(key in secrets)) {
// case: Netlify secret does not exist in Infisical
const numberOfValues = res[key].values.length;
res[key].values.forEach((value: NetlifyValue) => {
if (value.context === integration.context) {
if (numberOfValues <= 1) {
// case: Netlify secret value has less than 1 context -> delete secret
deleteSecrets.push(key);
} else {
// case: Netlify secret value has more than 1 context -> delete secret value context
deleteSecretValues.push({
key,
values: [{
id: value.id,
context: integration.context,
value: value.value
}]
});
}
}
});
}
});
const syncParams = new URLSearchParams({
site_id: integration.siteId
});
if (newSecrets.length > 0) {
await axios.post(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
newSecrets,
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
}
if (updateSecrets.length > 0) {
updateSecrets.forEach(async (secret: NetlifySecret) => {
await axios.patch(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
{
context: secret.values[0].context,
value: secret.values[0].value
},
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
});
}
if (deleteSecrets.length > 0) {
deleteSecrets.forEach(async (key: string) => {
await axios.delete(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`,
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
});
}
if (deleteSecretValues.length > 0) {
deleteSecretValues.forEach(async (secret: NetlifySecret) => {
await axios.delete(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`,
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Heroku');
}
}
/**
* Sync/push [secrets] to GitHub [repo]
* @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 syncSecretsGitHub = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
try {
interface GitHubRepoKey {
key_id: string;
key: string;
}
interface GitHubSecret {
name: string;
created_at: string;
updated_at: string;
}
interface GitHubSecretRes {
[index: string]: GitHubSecret;
}
const deleteSecrets: GitHubSecret[] = [];
const octokit = new Octokit({
auth: accessToken
});
const user = (await octokit.request('GET /user', {})).data;
const repoPublicKey: GitHubRepoKey = (await octokit.request(
'GET /repos/{owner}/{repo}/actions/secrets/public-key',
{
owner: user.login,
repo: integration.app
}
)).data;
// // Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key
const encryptedSecrets: GitHubSecretRes = (await octokit.request(
'GET /repos/{owner}/{repo}/actions/secrets',
{
owner: user.login,
repo: integration.app
}
))
.data
.secrets
.reduce((obj: any, secret: any) => ({
...obj,
[secret.name]: secret
}), {});
Object.keys(encryptedSecrets).map(async (key) => {
if (!(key in secrets)) {
await octokit.request(
'DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}',
{
owner: user.login,
repo: integration.app,
secret_name: key
}
);
}
});
Object.keys(secrets).map((key) => {
// let encryptedSecret;
sodium.ready.then(async () => {
// convert secret & base64 key to Uint8Array.
const binkey = sodium.from_base64(
repoPublicKey.key,
sodium.base64_variants.ORIGINAL
);
const binsec = sodium.from_string(secrets[key]);
// encrypt secret using libsodium
const encBytes = sodium.crypto_box_seal(binsec, binkey);
// convert encrypted Uint8Array to base64
const encryptedSecret = sodium.to_base64(
encBytes,
sodium.base64_variants.ORIGINAL
);
await octokit.request(
'PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}',
{
owner: user.login,
repo: integration.app,
secret_name: key,
encrypted_value: encryptedSecret,
key_id: repoPublicKey.key_id
}
);
});
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to GitHub');
}
};
export { syncSecrets };

View File

@ -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": ""
}
}

View File

@ -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,

View File

@ -0,0 +1,29 @@
import { ErrorRequestHandler } from "express";
import * as Sentry from '@sentry/node';
import { InternalServerError } from "../utils/errors";
import { getLogger } from "../utils/logger";
import RequestError, { LogLevel } from "../utils/requestError";
export const requestErrorHandler: ErrorRequestHandler = (error: RequestError|Error, req, res, next) => {
if(res.headersSent) return next();
//TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError
if(!(error instanceof RequestError)){
error = InternalServerError({context: {exception: error.message}, stack: error.stack})
getLogger('backend-main').log((<RequestError>error).levelName.toLowerCase(), (<RequestError>error).message)
}
//* Set Sentry user identification if req.user is populated
if(req.user !== undefined && req.user !== null){
Sentry.setUser({ email: req.user.email })
}
//* Only sent error to Sentry if LogLevel is one of the following level 'ERROR', 'EMERGENCY' or 'CRITICAL'
//* with this we will eliminate false-positive errors like 'BadRequestError', 'UnauthorizedRequestError' and so on
if([LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes((<RequestError>error).level)){
Sentry.captureException(error)
}
res.status((<RequestError>error).statusCode).json((<RequestError>error).format(req))
next()
}

View File

@ -1,8 +1,8 @@
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
import * as Sentry from '@sentry/node';
import { User } from '../models';
import { JWT_AUTH_SECRET } from '../config';
import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors';
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@ -20,32 +20,25 @@ declare module 'jsonwebtoken' {
*/
const requireAuth = async (req: Request, res: Response, next: NextFunction) => {
// JWT authentication middleware
try {
if (!req.headers?.authorization)
throw new Error('Failed to locate authorization header');
const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`}))
if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`}))
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
const token = req.headers.authorization.split(' ')[1];
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(token, JWT_AUTH_SECRET)
);
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, JWT_AUTH_SECRET)
);
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
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');
if (!user) return next(AccountNotFoundError({message: 'Failed to locate User account'}))
if (!user?.publicKey)
return next(UnauthorizedRequestError({message: 'Unable to authenticate due to partially set up account'}))
req.user = user;
return next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed to authenticate user. Try logging in'
});
}
req.user = user;
return next();
};
export default requireAuth;

View File

@ -0,0 +1,37 @@
import { Request, Response, NextFunction } from 'express';
import { Bot } from '../models';
import { validateMembership } from '../helpers/membership';
import { AccountNotFoundError } from '../utils/errors';
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) => {
const bot = await Bot.findOne({ _id: req[location].botId });
if (!bot) {
return next(AccountNotFoundError({message: 'Failed to locate Bot account'}))
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: bot.workspace.toString(),
acceptedRoles,
acceptedStatuses
});
req.bot = bot;
next();
}
}
export default requireBotAuth;

View File

@ -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 { Integration, IntegrationAuth } from '../models';
import { IntegrationService } from '../services';
import { validateMembership } from '../helpers/membership';
import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/errors';
/**
* Validate if user on request is a member of workspace with proper roles associated
@ -20,56 +21,40 @@ const requireIntegrationAuth = ({
return async (req: Request, res: Response, next: NextFunction) => {
// integration authorization middleware
try {
const { integrationId } = req.params;
const { integrationId } = req.params;
// validate integration accessibility
const integration = await Integration.findOne({
_id: integrationId
});
// validate integration accessibility
const integration = await Integration.findOne({
_id: integrationId
});
if (!integration) {
throw new Error('Failed to find integration');
}
const membership = await Membership.findOne({
user: req.user._id,
workspace: integration.workspace
});
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(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
);
if (!integrationAuth) {
throw new Error('Failed to find integration authorization');
}
req.integration = integration;
req.accessToken = await getOAuthAccessToken({ integrationAuth });
return next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed integration authorization'
});
if (!integration) {
return next(IntegrationNotFoundError({message: 'Failed to locate Integration'}))
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: integration.workspace.toString(),
acceptedRoles,
acceptedStatuses
});
const integrationAuth = await IntegrationAuth.findOne({
_id: integration.integrationAuth
}).select(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
);
if (!integrationAuth) {
return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}))
}
req.integration = integration;
req.accessToken = await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString()
});
return next();
};
};

View File

@ -1,8 +1,9 @@
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';
import { UnauthorizedRequestError } from '../utils/errors';
/**
* Validate if user on request is a member of workspace with proper roles associated
@ -10,62 +11,45 @@ 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
const { integrationAuthId } = req.params;
try {
const { integrationAuthId } = req.params;
const integrationAuth = await IntegrationAuth.findOne({
_id: integrationAuthId
}).select(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
);
const integrationAuth = await IntegrationAuth.findOne({
_id: integrationAuthId
}).select(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
);
if (!integrationAuth) {
return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authorization credentials'}))
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: integrationAuth.workspace.toString(),
acceptedRoles,
acceptedStatuses
});
if (!integrationAuth) {
throw new Error('Failed to find integration authorization');
}
const membership = await Membership.findOne({
user: req.user._id,
workspace: integrationAuth.workspace
});
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 });
return next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed (authorization) integration authorizationt'
req.integrationAuth = integrationAuth;
if (attachAccessToken) {
req.accessToken = await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString()
});
}
return next();
};
};

View File

@ -1,6 +1,6 @@
import * as Sentry from '@sentry/node';
import { Request, Response, NextFunction } from 'express';
import { IOrganization, MembershipOrg } from '../models';
import { UnauthorizedRequestError, ValidationError } from '../utils/errors';
/**
* Validate if user on request is a member with proper roles for organization
@ -19,35 +19,28 @@ const requireOrganizationAuth = ({
return async (req: Request, res: Response, next: NextFunction) => {
// organization authorization middleware
try {
// validate organization membership
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: req.params.organizationId
}).populate<{ organization: IOrganization }>('organization');
// validate organization membership
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: req.params.organizationId
}).populate<{ organization: IOrganization }>('organization');
if (!membershipOrg) {
throw new Error('Failed to find organization membership');
}
if (!acceptedRoles.includes(membershipOrg.role)) {
throw new Error('Failed to validate organization membership role');
}
if (!acceptedStatuses.includes(membershipOrg.status)) {
throw new Error('Failed to validate organization membership status');
}
req.membershipOrg = membershipOrg;
return next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed organization authorization'
});
if (!membershipOrg) {
return next(UnauthorizedRequestError({message: "You're not a member of this Organization."}))
}
//TODO is this important to validate? I mean is it possible to save wrong role to database or get wrong role from databse? - Zamion101
if (!acceptedRoles.includes(membershipOrg.role)) {
return next(ValidationError({message: 'Failed to validate Organization Membership Role'}))
}
if (!acceptedStatuses.includes(membershipOrg.status)) {
return next(ValidationError({message: 'Failed to validate Organization Membership Status'}))
}
req.membershipOrg = membershipOrg;
return next();
};
};

View File

@ -1,8 +1,8 @@
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
import * as Sentry from '@sentry/node';
import { ServiceToken } from '../models';
import { JWT_SERVICE_SECRET } from '../config';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@ -24,33 +24,27 @@ const requireServiceTokenAuth = async (
next: NextFunction
) => {
// JWT service token middleware
try {
if (!req.headers?.authorization)
throw new Error('Failed to locate authorization header');
const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`}))
//TODO: Determine what is the actual Token Type for Service Token Authentication (ex. Bearer)
//if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(UnauthorizedRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`}))
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
const token = req.headers.authorization.split(' ')[1];
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, JWT_SERVICE_SECRET)
);
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(token, JWT_SERVICE_SECRET)
);
const serviceToken = await ServiceToken.findOne({
_id: decodedToken.serviceTokenId
})
.populate('user', '+publicKey')
.select('+encryptedKey +publicKey +nonce');
const serviceToken = await ServiceToken.findOne({
_id: decodedToken.serviceTokenId
})
.populate('user', '+publicKey')
.select('+encryptedKey +publicKey +nonce');
if (!serviceToken) return next(UnauthorizedRequestError({message: 'The service token does not match the record in the database'}))
if (!serviceToken) throw new Error('Failed to find service token');
req.serviceToken = serviceToken;
return next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed to authenticate service token'
});
}
req.serviceToken = serviceToken;
return next();
};
export default requireServiceTokenAuth;

View File

@ -1,8 +1,8 @@
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
import * as Sentry from '@sentry/node';
import { User } from '../models';
import { JWT_SIGNUP_SECRET } from '../config';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@ -21,32 +21,24 @@ const requireSignupAuth = async (
) => {
// JWT (temporary) authentication middleware for complete signup
try {
if (!req.headers?.authorization)
throw new Error('Failed to locate authorization header');
const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`}))
if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`}))
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, JWT_SIGNUP_SECRET)
);
const token = req.headers.authorization.split(' ')[1];
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(token, JWT_SIGNUP_SECRET)
);
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
if (!user)
return next(UnauthorizedRequestError({message: 'Unable to authenticate for User account completion. Try logging in again'}))
if (!user)
throw new Error('Failed to temporarily authenticate unfound user');
req.user = user;
return next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error:
'Failed to temporarily authenticate user for complete account. Try logging in'
});
}
req.user = user;
return next();
};
export default requireSignupAuth;

View File

@ -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';
import { UnauthorizedRequestError } from '../utils/errors';
type req = 'params' | 'body' | 'query';
@ -25,34 +25,18 @@ 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;
return next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed workspace authorization'
});
return next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
}
};
};

View File

@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import * as Sentry from '@sentry/node';
import { validationResult } from 'express-validator';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
/**
* Validate intended inputs on [req] via express-validator
@ -15,16 +15,12 @@ const validate = (req: Request, res: Response, next: NextFunction) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
return next(BadRequestError({context: {errors: errors.array}}))
}
return next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: "Looks like you're unauthenticated . Try logging in"
});
return next(UnauthorizedRequestError({message: 'Unauthenticated requests are not allowed. Try logging in'}))
}
};

57
backend/src/models/bot.ts Normal file
View 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;

View 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;

View File

@ -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,

View File

@ -1,59 +1,83 @@
import { Schema, model, Types } from 'mongoose';
import {
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD,
INTEGRATION_HEROKU,
INTEGRATION_NETLIFY
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
} from '../variables';
export interface IIntegration {
_id: Types.ObjectId;
workspace: Types.ObjectId;
environment: 'dev' | 'test' | 'staging' | 'prod';
isActive: boolean;
app: string;
integration: 'heroku' | 'netlify';
integrationAuth: Types.ObjectId;
_id: Types.ObjectId;
workspace: Types.ObjectId;
environment: 'dev' | 'test' | 'staging' | 'prod';
isActive: boolean;
app: string;
target: string;
context: string;
siteId: string;
integration: 'heroku' | 'vercel' | 'netlify' | 'github';
integrationAuth: Types.ObjectId;
}
const integrationSchema = new Schema<IIntegration>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
isActive: {
type: Boolean,
required: true
},
app: {
// name of app in provider
type: String,
default: null,
required: true
},
integration: {
type: String,
enum: [INTEGRATION_HEROKU, INTEGRATION_NETLIFY],
required: true
},
integrationAuth: {
type: Schema.Types.ObjectId,
ref: 'IntegrationAuth',
required: true
}
},
{
timestamps: true
}
{
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
isActive: {
type: Boolean,
required: true
},
app: {
// name of app in provider
type: String,
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_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
],
required: true
},
integrationAuth: {
type: Schema.Types.ObjectId,
ref: 'IntegrationAuth',
required: true
}
},
{
timestamps: true
}
);
const Integration = model<IIntegration>('Integration', integrationSchema);

View File

@ -1,67 +1,87 @@
import { Schema, model, Types } from 'mongoose';
import { INTEGRATION_HEROKU, INTEGRATION_NETLIFY } from '../variables';
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
} from '../variables';
export interface IIntegrationAuth {
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration: 'heroku' | 'netlify';
refreshCiphertext?: string;
refreshIV?: string;
refreshTag?: string;
accessCiphertext?: string;
accessIV?: string;
accessTag?: string;
accessExpiresAt?: Date;
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration: 'heroku' | 'vercel' | 'netlify' | 'github';
teamId: string;
accountId: string;
refreshCiphertext?: string;
refreshIV?: string;
refreshTag?: string;
accessCiphertext?: string;
accessIV?: string;
accessTag?: string;
accessExpiresAt?: Date;
}
const integrationAuthSchema = new Schema<IIntegrationAuth>(
{
workspace: {
type: Schema.Types.ObjectId,
required: true
},
integration: {
type: String,
enum: [INTEGRATION_HEROKU, INTEGRATION_NETLIFY],
required: true
},
refreshCiphertext: {
type: String,
select: false
},
refreshIV: {
type: String,
select: false
},
refreshTag: {
type: String,
select: false
},
accessCiphertext: {
type: String,
select: false
},
accessIV: {
type: String,
select: false
},
accessTag: {
type: String,
select: false
},
accessExpiresAt: {
type: Date,
select: false
}
},
{
timestamps: true
}
{
workspace: {
type: Schema.Types.ObjectId,
required: true
},
integration: {
type: String,
enum: [
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
],
required: true
},
teamId: {
// vercel-specific integration param
type: String
},
accountId: {
// netlify-specific integration param
type: String
},
refreshCiphertext: {
type: String,
select: false
},
refreshIV: {
type: String,
select: false
},
refreshTag: {
type: String,
select: false
},
accessCiphertext: {
type: String,
select: false
},
accessIV: {
type: String,
select: false
},
accessTag: {
type: String,
select: false
},
accessExpiresAt: {
type: Date,
select: false
}
},
{
timestamps: true
}
);
const IntegrationAuth = model<IIntegrationAuth>(
'IntegrationAuth',
integrationAuthSchema
'IntegrationAuth',
integrationAuthSchema
);
export default IntegrationAuth;

View File

@ -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);

View File

@ -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
View 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;

View File

@ -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,

View File

@ -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
);

View File

@ -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,

View File

@ -34,6 +34,4 @@ router.get(
keyController.getLatestKey
);
router.get('/publicKey/infisical', keyController.getPublicKeyInfisical);
export default router;

View File

@ -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;

View 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;

View 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;

View 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;

View File

@ -1,15 +1,27 @@
import { PostHog } from 'posthog-node';
import { NODE_ENV, POSTHOG_HOST, POSTHOG_PROJECT_API_KEY, TELEMETRY_ENABLED } from '../config';
import {
NODE_ENV,
POSTHOG_HOST,
POSTHOG_PROJECT_API_KEY,
TELEMETRY_ENABLED
} from '../config';
import { getLogger } from '../utils/logger';
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
});
if(TELEMETRY_ENABLED){
getLogger("backend-main").info([
"",
"Infisical collects telemetry data about general usage.",
"The data helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth for investors as we support Infisical as open-source software.",
"To opt out of telemetry, you can set `TELEMETRY_ENABLED=false` within the environment variables",
].join('\n'))
}
export default postHogClient;
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;

View File

@ -0,0 +1,10 @@
import mongoose from 'mongoose';
import { getLogger } from '../utils/logger';
export const initDatabase = (MONGO_URL: string) => {
mongoose
.connect(MONGO_URL)
.then(() => getLogger("database").info("Database connection established"))
.catch((e) => getLogger("database").error(`Unable to establish Database connection due to the error.\n${e}`));
return mongoose.connection;
};

View File

@ -0,0 +1,32 @@
import mongoose from 'mongoose';
import { createTerminus } from '@godaddy/terminus';
import { getLogger } from '../utils/logger';
export const setUpHealthEndpoint = <T>(server: T) => {
const onSignal = () => {
getLogger('backend-main').info('Server is starting clean-up');
return Promise.all([
new Promise((resolve) => {
if (mongoose.connection && mongoose.connection.readyState == 1) {
mongoose.connection.close()
.then(() => resolve('Database connection closed'));
} else {
resolve('Database connection already closed');
}
})
]);
};
const healthCheck = () => {
// `state.isShuttingDown` (boolean) shows whether the server is shutting down or not
// optionally include a resolve value to be included as info in the health check response
return Promise.resolve();
};
createTerminus(server, {
healthChecks: {
'/healthcheck': healthCheck,
onSignal
}
});
};

View File

@ -1,5 +1,11 @@
import postHogClient from './PostHogClient';
import BotService from './BotService';
import EventService from './EventService';
import IntegrationService from './IntegrationService';
export {
postHogClient
postHogClient,
BotService,
EventService,
IntegrationService
}

View File

@ -0,0 +1,52 @@
import nodemailer from 'nodemailer';
import { SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_SECURE } from '../config';
import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from '../variables';
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
};
}
if (SMTP_SECURE) {
switch (SMTP_HOST) {
case SMTP_HOST_SENDGRID:
mailOpts.requireTLS = true;
break;
case SMTP_HOST_MAILGUN:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
break;
default:
mailOpts.secure = true;
break;
}
}
export const initSmtp = () => {
const transporter = nodemailer.createTransport(mailOpts);
transporter
.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}`
);
});
return transporter;
};

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -1,5 +1,6 @@
import * as express from 'express';
// TODO: fix (any) types
declare global {
namespace Express {
@ -11,6 +12,7 @@ declare global {
membershipOrg: any;
integration: any;
integrationAuth: any;
bot: any;
serviceToken: any;
accessToken: any;
query?: any;

View File

@ -1,6 +1,22 @@
import nacl from 'tweetnacl';
import util from 'tweetnacl-util';
import AesGCM from './aes-gcm';
import * as Sentry from '@sentry/node';
/**
* 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
@ -32,6 +48,8 @@ const encryptAsymmetric = ({
util.decodeBase64(privateKey)
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform asymmetric encryption');
}
@ -71,6 +89,8 @@ const decryptAsymmetric = ({
util.decodeBase64(privateKey)
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform asymmetric decryption');
}
@ -81,7 +101,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,
@ -97,6 +117,8 @@ const encryptSymmetric = ({
iv = obj.iv;
tag = obj.tag;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform symmetric encryption');
}
@ -114,7 +136,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 = ({
@ -132,6 +154,8 @@ const decryptSymmetric = ({
try {
plaintext = AesGCM.decrypt(ciphertext, iv, tag, key);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform symmetric decryption');
}
@ -139,6 +163,7 @@ const decryptSymmetric = ({
};
export {
generateKeyPair,
encryptAsymmetric,
decryptAsymmetric,
encryptSymmetric,

116
backend/src/utils/errors.ts Normal file
View File

@ -0,0 +1,116 @@
import RequestError, { LogLevel, RequestErrorContext } from "./requestError"
//* ----->[GENERAL HTTP ERRORS]<-----
export const RouteNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.INFO,
statusCode: error?.statusCode ?? 404,
type: error?.type ?? 'route_not_found',
message: error?.message ?? 'The requested source was not found',
context: error?.context,
stack: error?.stack
})
export const MethodNotAllowedError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.INFO,
statusCode: error?.statusCode ?? 405,
type: error?.type ?? 'method_not_allowed',
message: error?.message ?? 'The requested method is not allowed for the resource',
context: error?.context,
stack: error?.stack
})
export const UnauthorizedRequestError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.INFO,
statusCode: error?.statusCode ?? 401,
type: error?.type ?? 'unauthorized',
message: error?.message ?? 'You are not authorized to access this resource',
context: error?.context,
stack: error?.stack
})
export const ForbiddenRequestError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.INFO,
statusCode: error?.statusCode ?? 403,
type: error?.type ?? 'forbidden',
message: error?.message ?? 'You are not allowed to access this resource',
context: error?.context,
stack: error?.stack
})
export const BadRequestError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.INFO,
statusCode: error?.statusCode ?? 400,
type: error?.type ?? 'bad_request',
message: error?.message ?? 'The request is invalid or cannot be served',
context: error?.context,
stack: error?.stack
})
export const InternalServerError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR,
statusCode: error?.statusCode ?? 500,
type: error?.type ?? 'internal_server_error',
message: error?.message ?? 'The server encountered an error while processing the request',
context: error?.context,
stack: error?.stack
})
export const ServiceUnavailableError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR,
statusCode: error?.statusCode ?? 503,
type: error?.type ?? 'service_unavailable',
message: error?.message ?? 'The service is currently unavailable. Please try again later.',
context: error?.context,
stack: error?.stack
})
export const ValidationError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR,
statusCode: error?.statusCode ?? 400,
type: error?.type ?? 'validation_error',
message: error?.message ?? 'The request failed validation',
context: error?.context,
stack: error?.stack
})
//* ----->[INTEGRATION ERRORS]<-----
export const IntegrationNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR,
statusCode: error?.statusCode ?? 404,
type: error?.type ?? 'integration_not_found_error',
message: error?.message ?? 'The requested integration was not found',
context: error?.context,
stack: error?.stack
})
//* ----->[WORKSPACE ERRORS]<-----
export const WorkspaceNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR,
statusCode: error?.statusCode ?? 404,
type: error?.type ?? 'workspace_not_found_error',
message: error?.message ?? 'The requested workspace was not found',
context: error?.context,
stack: error?.stack
})
//* ----->[ORGANIZATION ERRORS]<-----
export const OrganizationNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR,
statusCode: error?.statusCode ?? 404,
type: error?.type ?? 'organization_not_found_error',
message: error?.message ?? 'The requested organization was not found',
context: error?.context,
stack: error?.stack
})
//* ----->[ACCOUNT ERRORS]<-----
export const AccountNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR,
statusCode: error?.statusCode ?? 404,
type: error?.type ?? 'account_not_found_error',
message: error?.message ?? 'The requested account was not found',
context: error?.context,
stack: error?.stack
})
//* ----->[MISC ERRORS]<-----

View File

@ -0,0 +1,65 @@
/* eslint-disable no-console */
import { createLogger, format, transports } from 'winston';
import LokiTransport from 'winston-loki';
import { LOKI_HOST, NODE_ENV } from '../config';
const { combine, colorize, label, printf, splat, timestamp } = format;
const logFormat = (prefix: string) => combine(
timestamp(),
splat(),
label({ label: prefix }),
printf((info) => `${info.timestamp} ${info.label} ${info.level}: ${info.message}`)
);
const createLoggerWithLabel = (level: string, label: string) => {
const _level = level.toLowerCase() || 'info'
//* Always add Console output to transports
const _transports: any[] = [
new transports.Console({
format: combine(
colorize(),
logFormat(label),
// format.json()
)
})
]
//* Add LokiTransport if it's enabled
if(LOKI_HOST !== undefined){
_transports.push(
new LokiTransport({
host: LOKI_HOST,
handleExceptions: true,
handleRejections: true,
batching: true,
level: _level,
timeout: 30000,
format: format.combine(
format.json()
),
labels: {app: process.env.npm_package_name, version: process.env.npm_package_version, environment: NODE_ENV},
onConnectionError: (err: Error)=> console.error('Connection error while connecting to Loki Server.\n', err)
})
)
}
return createLogger({
level: _level,
transports: _transports,
format: format.combine(
logFormat(label),
format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'label'] })
)
});
}
const DEFAULT_LOGGERS = {
"backend-main": createLoggerWithLabel('info', '[IFSC:backend-main]'),
"database": createLoggerWithLabel('info', '[IFSC:database]'),
}
type LoggerNames = keyof typeof DEFAULT_LOGGERS
export const getLogger = (loggerName: LoggerNames) => {
return DEFAULT_LOGGERS[loggerName]
}

View File

@ -0,0 +1,65 @@
/*
Original work Copyright (c) 2016, Nikolay Nemshilov <nemshilov@gmail.com>
Modified work Copyright (c) 2016, David Banham <david@banham.id.au>
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
*/
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-env node */
const Layer = require('express/lib/router/layer');
const Router = require('express/lib/router');
const last = (arr = []) => arr[arr.length - 1];
const noop = Function.prototype;
function copyFnProps(oldFn, newFn) {
Object.keys(oldFn).forEach((key) => {
newFn[key] = oldFn[key];
});
return newFn;
}
function wrap(fn) {
const newFn = function newFn(...args) {
const ret = fn.apply(this, args);
const next = (args.length === 5 ? args[2] : last(args)) || noop;
if (ret && ret.catch) ret.catch(err => next(err));
return ret;
};
Object.defineProperty(newFn, 'length', {
value: fn.length,
writable: false,
});
return copyFnProps(fn, newFn);
}
export function patchRouterParam() {
const originalParam = Router.prototype.constructor.param;
Router.prototype.constructor.param = function param(name, fn) {
fn = wrap(fn);
return originalParam.call(this, name, fn);
};
}
Object.defineProperty(Layer.prototype, 'handle', {
enumerable: true,
get() {
return this.__handle;
},
set(fn) {
fn = wrap(fn);
this.__handle = fn;
},
});

View File

@ -0,0 +1,113 @@
import { Request } from 'express'
import { VERBOSE_ERROR_OUTPUT } from '../config'
export enum LogLevel {
DEBUG = 100,
INFO = 200,
NOTICE = 250,
WARNING = 300,
ERROR = 400,
CRITICAL = 500,
ALERT = 550,
EMERGENCY = 600,
}
export type RequestErrorContext = {
logLevel?: LogLevel,
statusCode: number,
type: string,
message: string,
context?: Record<string, unknown>,
stack?: string|undefined
}
export default class RequestError extends Error{
private _logLevel: LogLevel
private _logName: string
statusCode: number
type: string
context: Record<string, unknown>
extra: Record<string, string|number|symbol>[]
private stacktrace: string|undefined|string[]
constructor(
{logLevel, statusCode, type, message, context, stack} : RequestErrorContext
){
super(message)
this._logLevel = logLevel || LogLevel.INFO
this._logName = LogLevel[this._logLevel]
this.statusCode = statusCode
this.type = type
this.context = context || {}
this.extra = []
if(stack) this.stack = stack
else Error.captureStackTrace(this, this.constructor)
this.stacktrace = this.stack?.split('\n')
}
static convertFrom(error: Error) {
//This error was not handled by error handler. Please report this incident to the staff.
return new RequestError({
logLevel: LogLevel.ERROR,
statusCode: 500,
type: 'internal_server_error',
message: 'This error was not handled by error handler. Please report this incident to the staff',
context: {
message: error.message,
name: error.name
},
stack: error.stack
})
}
get level(){ return this._logLevel }
get levelName(){ return this._logName }
withTags(...tags: string[]|number[]){
this.context['tags'] = Object.assign(tags, this.context['tags'])
return this
}
withExtras(...extras: Record<string, string|boolean|number>[]){
this.extra = Object.assign(extras, this.extra)
return this
}
private _omit(obj: any, keys: string[]): typeof obj{
const exclude = new Set(keys)
obj = Object.fromEntries(Object.entries(obj).filter(e => !exclude.has(e[0])))
return obj
}
public format(req: Request){
let _context = Object.assign({
stacktrace: this.stacktrace
}, this.context)
//* Omit sensitive information from context that can leak internal workings of this program if user is not developer
if(!VERBOSE_ERROR_OUTPUT){
_context = this._omit(_context, [
'stacktrace',
'exception',
])
}
const formatObject = {
type: this.type,
message: this.message,
context: _context,
level: this.level,
level_name: this.levelName,
status_code: this.statusCode,
datetime_iso: new Date().toISOString(),
application: process.env.npm_package_name || 'unknown',
request_id: req.headers["Request-Id"],
extra: this.extra
}
return formatObject
}
}

View File

@ -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
};

View 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
}

View File

@ -0,0 +1,7 @@
const EVENT_PUSH_SECRETS = 'pushSecrets';
const EVENT_PULL_SECRETS = 'pullSecrets';
export {
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS
}

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