Merge remote-tracking branch 'upstream/main' into feat/39

This commit is contained in:
이강준
2022-11-28 12:24:20 +09:00
174 changed files with 23100 additions and 20563 deletions
@@ -0,0 +1,22 @@
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v4
with:
days-before-issue-stale: 30
days-before-issue-close: 14
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}
+16 -18
View File
@@ -10,7 +10,7 @@
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">Slack</a> | <a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">Slack</a> |
<a href="https://infisical.com/signup">Infisical Cloud</a> | <a href="https://infisical.com/signup">Infisical Cloud</a> |
<a href="https://infisical.com/docs/self-hosting/overview">Self-Hosting</a> | <a href="https://infisical.com/docs/self-hosting/overview">Self-Hosting</a> |
<a href="https://infisical.com/docs/gettingStarted">Docs</a> | <a href="https://infisical.com/docs/getting-started/introduction">Docs</a> |
<a href="https://www.infisical.com">Website</a> <a href="https://www.infisical.com">Website</a>
</h4> </h4>
@@ -31,11 +31,11 @@
**[Infisical](https://infisical.com)** is an open source, E2EE tool to help teams manage and sync environment variables across their development workflow and infrastructure. It's designed to be simple and take minutes to get going. **[Infisical](https://infisical.com)** is an open source, E2EE tool to help teams manage and sync 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** to manage your team's environment variables within projects
- **[Language-Agnostic CLI](https://infisical.com/docs/CLI)** that pulls and injects environment variables into your local workflow - **[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_host_overview)** - host it yourself on any infrastructure - **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure
- **Navigate Multiple Environments** per project (e.g. development, staging, production, etc.) - **Navigate Multiple Environments** per project (e.g. development, staging, production, etc.)
- **Personal/Shared** scoping for environment variables - **Personal/Shared** scoping for environment variables
- **[Integrations](https://infisical.com/docs/Heroku)** with CI/CD and production infrastructure (Heroku available, more coming soon) - **[Integrations](https://infisical.com/docs/integrations/heroku)** with CI/CD and production infrastructure (Heroku available, more coming soon)
- 🔜 **1-Click Deploy** to Digital Ocean and Heroku - 🔜 **1-Click Deploy** to Digital Ocean and Heroku
- 🔜 **Authentication/Authorization** for projects (read/write controls soon) - 🔜 **Authentication/Authorization** for projects (read/write controls soon)
- 🔜 **Automatic Secret Rotation** - 🔜 **Automatic Secret Rotation**
@@ -49,12 +49,6 @@ And more.
To quickly get started, visit our [get started guide](https://infisical.com/docs/getting-started/introduction). To quickly get started, visit our [get started guide](https://infisical.com/docs/getting-started/introduction).
## 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)
## What's cool about this? ## What's cool about this?
Infisical is simple, E2EE, and (soon to be) complete. Infisical is simple, E2EE, and (soon to be) complete.
@@ -65,12 +59,10 @@ We're on a mission to make secret management more accessible to everyone — tha
If you care about efficiency and security, then Infisical is right for you. 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) directly to the repository. 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
For full documentation, visit [infisical.com/docs](https://infisical.com/docs).
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). 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:[email protected]?subject=Pairing%20session&body=I'd%20like%20to%20do%20a%20pairing%20session!)! Not sure where to get started? [Book a free, non-pressure pairing sessions with one of our teammates](mailto:[email protected]?subject=Pairing%20session&body=I'd%20like%20to%20do%20a%20pairing%20session!)!
@@ -79,7 +71,7 @@ Not sure where to get started? [Book a free, non-pressure pairing sessions with
- [GitHub Discussions](https://github.com/Infisical/infisical/discussions) for help with building and discussion. - [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. - [GitHub Issues](https://github.com/Infisical/infisical-cli/issues) for any bugs and errors you encounter using Infisical.
- [Community Slack](https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA) for hanging out with the community and quick communication with the team. - [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.
## Status ## Status
@@ -89,13 +81,19 @@ Not sure where to get started? [Book a free, non-pressure pairing sessions with
We're currently in Public Alpha. We're currently in Public Alpha.
## 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 ## Integrations
We're currently setting the foundation and building integrations so secrets can be synced everywhere. Any help is welcome! :) We're currently setting the foundation and building integrations so secrets can be synced everywhere. Any help is welcome! :)
- [x] Docker - [x] [Docker](https://infisical.com/docs/integrations/docker)
- [x] Docker Compose - [x] [Docker Compose](https://infisical.com/docs/integrations/docker-compose)
- [x] Heroku - [x] [Heroku](https://infisical.com/docs/integrations/heroku)
- [ ] Vercel - [ ] Vercel
- [ ] Kubernetes - [ ] Kubernetes
- [ ] AWS - [ ] AWS
@@ -121,4 +119,4 @@ Looking to report a security vulnerability? Please don't post about it in GitHub
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
<!-- markdownlint-disable --> <!-- markdownlint-disable -->
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/vlad-matsiiako"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?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/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>
+364 -15
View File
@@ -31,6 +31,8 @@
"query-string": "^7.1.1", "query-string": "^7.1.1",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"stripe": "^10.7.0", "stripe": "^10.7.0",
"swagger-jsdoc": "^6.2.5",
"swagger-ui-express": "^4.6.0",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1", "tweetnacl-util": "^0.15.1",
"typescript": "^4.8.4" "typescript": "^4.8.4"
@@ -43,6 +45,8 @@
"@types/jsonwebtoken": "^8.5.9", "@types/jsonwebtoken": "^8.5.9",
"@types/node": "^18.11.3", "@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6", "@types/nodemailer": "^6.4.6",
"@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3",
"@typescript-eslint/eslint-plugin": "^5.40.1", "@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1", "@typescript-eslint/parser": "^5.40.1",
"eslint": "^8.26.0", "eslint": "^8.26.0",
@@ -70,6 +74,46 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@apidevtools/json-schema-ref-parser": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.0.tgz",
"integrity": "sha512-teB30tFooE3iQs2HQIKJ02D8UZA1Xy1zaczzhUjJs0CymYxeC0g+y5rCY2p8NHBM6DBUVoR8rSM4kHLj1WE9mQ==",
"dependencies": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.6",
"call-me-maybe": "^1.0.1",
"js-yaml": "^4.1.0"
}
},
"node_modules/@apidevtools/openapi-schemas": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
"engines": {
"node": ">=10"
}
},
"node_modules/@apidevtools/swagger-methods": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg=="
},
"node_modules/@apidevtools/swagger-parser": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.2.tgz",
"integrity": "sha512-JFxcEyp8RlNHgBCE98nwuTkZT6eNFPc1aosWV6wPcQph72TSEEu1k3baJD4/x1qznU+JiDdz8F5pTwabZh+Dhg==",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^9.0.6",
"@apidevtools/openapi-schemas": "^2.0.4",
"@apidevtools/swagger-methods": "^3.0.2",
"@jsdevtools/ono": "^7.1.3",
"call-me-maybe": "^1.0.1",
"z-schema": "^4.2.3"
},
"peerDependencies": {
"openapi-types": ">=7"
}
},
"node_modules/@aws-crypto/ie11-detection": { "node_modules/@aws-crypto/ie11-detection": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.2.tgz", "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.2.tgz",
@@ -2532,6 +2576,11 @@
"@jridgewell/sourcemap-codec": "^1.4.10" "@jridgewell/sourcemap-codec": "^1.4.10"
} }
}, },
"node_modules/@jsdevtools/ono": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="
},
"node_modules/@juanelas/base64": { "node_modules/@juanelas/base64": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@juanelas/base64/-/base64-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@juanelas/base64/-/base64-1.0.5.tgz",
@@ -2843,8 +2892,7 @@
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.11", "version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
"dev": true
}, },
"node_modules/@types/jsonwebtoken": { "node_modules/@types/jsonwebtoken": {
"version": "8.5.9", "version": "8.5.9",
@@ -2915,6 +2963,22 @@
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
"dev": true "dev": true
}, },
"node_modules/@types/swagger-jsdoc": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.1.tgz",
"integrity": "sha512-+MUpcbyxD528dECUBCEVm6abNuORdbuGjbrUdHDeAQ+rkPuo2a+L4N02WJHF3bonSSE6SJ3dUJwF2V6+cHnf0w==",
"dev": true
},
"node_modules/@types/swagger-ui-express": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.3.tgz",
"integrity": "sha512-jqCjGU/tGEaqIplPy3WyQg+Nrp6y80DCFnDEAvVKWkJyv0VivSSDCChkppHRHAablvInZe6pijDFMnavtN0vqA==",
"dev": true,
"dependencies": {
"@types/express": "*",
"@types/serve-static": "*"
}
},
"node_modules/@types/webidl-conversions": { "node_modules/@types/webidl-conversions": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -3279,8 +3343,7 @@
"node_modules/argparse": { "node_modules/argparse": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
"dev": true
}, },
"node_modules/array-flatten": { "node_modules/array-flatten": {
"version": "1.1.1", "version": "1.1.1",
@@ -3617,6 +3680,11 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/call-me-maybe": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="
},
"node_modules/callsites": { "node_modules/callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -3816,6 +3884,14 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/commander": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
"engines": {
"node": ">= 6"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -4055,7 +4131,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
"dev": true,
"dependencies": { "dependencies": {
"esutils": "^2.0.2" "esutils": "^2.0.2"
}, },
@@ -4396,7 +4471,6 @@
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -5954,7 +6028,6 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
}, },
@@ -6150,6 +6223,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}, },
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
},
"node_modules/lodash.includes": { "node_modules/lodash.includes": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -6160,6 +6238,11 @@
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
}, },
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
},
"node_modules/lodash.isinteger": { "node_modules/lodash.isinteger": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
@@ -6186,6 +6269,11 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true "dev": true
}, },
"node_modules/lodash.mergewith": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="
},
"node_modules/lodash.once": { "node_modules/lodash.once": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
@@ -9247,6 +9335,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/openapi-types": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.0.2.tgz",
"integrity": "sha512-GuTo7FyZjOIWVhIhQSWJVaws6A82sWIGyQogxxYBYKZ0NBdyP2CYSIgOwFfSB+UVoPExk/YzFpyYitHS8KVZtA==",
"peer": true
},
"node_modules/optionator": { "node_modules/optionator": {
"version": "0.9.1", "version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
@@ -10284,6 +10378,74 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/swagger-jsdoc": {
"version": "6.2.5",
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.5.tgz",
"integrity": "sha512-l+cdsKS2y+QDhrH1TJSUiE0y9XKuf5xaGSatjf0hR/wjTlMpO8WfubBK9d/nASdbHPMtj9iJZLBH2ogBEhL7Sw==",
"dependencies": {
"commander": "6.2.0",
"doctrine": "3.0.0",
"glob": "7.1.6",
"lodash.mergewith": "^4.6.2",
"swagger-parser": "10.0.2",
"yaml": "2.0.0-1"
},
"bin": {
"swagger-jsdoc": "bin/swagger-jsdoc.js"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/swagger-jsdoc/node_modules/glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/swagger-parser": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.2.tgz",
"integrity": "sha512-9jHkHM+QXyLGFLk1DkXBwV+4HyNm0Za3b8/zk/+mjr8jgOSiqm3FOTHBSDsBjtn9scdL+8eWcHdupp2NLM8tDw==",
"dependencies": {
"@apidevtools/swagger-parser": "10.0.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/swagger-ui-dist": {
"version": "4.15.5",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz",
"integrity": "sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA=="
},
"node_modules/swagger-ui-express": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.0.tgz",
"integrity": "sha512-ZxpQFp1JR2RF8Ar++CyJzEDdvufa08ujNUJgMVTMWPi86CuQeVdBtvaeO/ysrz6dJAYXf9kbVNhWD7JWocwqsA==",
"dependencies": {
"swagger-ui-dist": ">=4.11.0"
},
"engines": {
"node": ">= v0.10.32"
},
"peerDependencies": {
"express": ">=4.0.0"
}
},
"node_modules/test-exclude": { "node_modules/test-exclude": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@@ -10758,6 +10920,14 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true "dev": true
}, },
"node_modules/yaml": {
"version": "2.0.0-1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
"engines": {
"node": ">= 6"
}
},
"node_modules/yargs": { "node_modules/yargs": {
"version": "17.6.2", "version": "17.6.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz",
@@ -10805,6 +10975,31 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/z-schema": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-4.2.4.tgz",
"integrity": "sha512-YvBeW5RGNeNzKOUJs3rTL4+9rpcvHXt5I051FJbOcitV8bl40pEfcG0Q+dWSwS0/BIYrMZ/9HHoqLllMkFhD0w==",
"dependencies": {
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"validator": "^13.6.0"
},
"bin": {
"z-schema": "bin/z-schema"
},
"engines": {
"node": ">=6.0.0"
},
"optionalDependencies": {
"commander": "^2.7.1"
}
},
"node_modules/z-schema/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"optional": true
} }
}, },
"dependencies": { "dependencies": {
@@ -10818,6 +11013,40 @@
"@jridgewell/trace-mapping": "^0.3.9" "@jridgewell/trace-mapping": "^0.3.9"
} }
}, },
"@apidevtools/json-schema-ref-parser": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.0.tgz",
"integrity": "sha512-teB30tFooE3iQs2HQIKJ02D8UZA1Xy1zaczzhUjJs0CymYxeC0g+y5rCY2p8NHBM6DBUVoR8rSM4kHLj1WE9mQ==",
"requires": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.6",
"call-me-maybe": "^1.0.1",
"js-yaml": "^4.1.0"
}
},
"@apidevtools/openapi-schemas": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ=="
},
"@apidevtools/swagger-methods": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg=="
},
"@apidevtools/swagger-parser": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.2.tgz",
"integrity": "sha512-JFxcEyp8RlNHgBCE98nwuTkZT6eNFPc1aosWV6wPcQph72TSEEu1k3baJD4/x1qznU+JiDdz8F5pTwabZh+Dhg==",
"requires": {
"@apidevtools/json-schema-ref-parser": "^9.0.6",
"@apidevtools/openapi-schemas": "^2.0.4",
"@apidevtools/swagger-methods": "^3.0.2",
"@jsdevtools/ono": "^7.1.3",
"call-me-maybe": "^1.0.1",
"z-schema": "^4.2.3"
}
},
"@aws-crypto/ie11-detection": { "@aws-crypto/ie11-detection": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.2.tgz", "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.2.tgz",
@@ -12978,6 +13207,11 @@
"@jridgewell/sourcemap-codec": "^1.4.10" "@jridgewell/sourcemap-codec": "^1.4.10"
} }
}, },
"@jsdevtools/ono": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="
},
"@juanelas/base64": { "@juanelas/base64": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@juanelas/base64/-/base64-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@juanelas/base64/-/base64-1.0.5.tgz",
@@ -13265,8 +13499,7 @@
"@types/json-schema": { "@types/json-schema": {
"version": "7.0.11", "version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
"dev": true
}, },
"@types/jsonwebtoken": { "@types/jsonwebtoken": {
"version": "8.5.9", "version": "8.5.9",
@@ -13337,6 +13570,22 @@
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
"dev": true "dev": true
}, },
"@types/swagger-jsdoc": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.1.tgz",
"integrity": "sha512-+MUpcbyxD528dECUBCEVm6abNuORdbuGjbrUdHDeAQ+rkPuo2a+L4N02WJHF3bonSSE6SJ3dUJwF2V6+cHnf0w==",
"dev": true
},
"@types/swagger-ui-express": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.3.tgz",
"integrity": "sha512-jqCjGU/tGEaqIplPy3WyQg+Nrp6y80DCFnDEAvVKWkJyv0VivSSDCChkppHRHAablvInZe6pijDFMnavtN0vqA==",
"dev": true,
"requires": {
"@types/express": "*",
"@types/serve-static": "*"
}
},
"@types/webidl-conversions": { "@types/webidl-conversions": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -13569,8 +13818,7 @@
"argparse": { "argparse": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
"dev": true
}, },
"array-flatten": { "array-flatten": {
"version": "1.1.1", "version": "1.1.1",
@@ -13819,6 +14067,11 @@
"get-intrinsic": "^1.0.2" "get-intrinsic": "^1.0.2"
} }
}, },
"call-me-maybe": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="
},
"callsites": { "callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -13959,6 +14212,11 @@
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
} }
}, },
"commander": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q=="
},
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -14140,7 +14398,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
"dev": true,
"requires": { "requires": {
"esutils": "^2.0.2" "esutils": "^2.0.2"
} }
@@ -14386,8 +14643,7 @@
"esutils": { "esutils": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
"dev": true
}, },
"etag": { "etag": {
"version": "1.8.1", "version": "1.8.1",
@@ -15550,7 +15806,6 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"requires": { "requires": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
} }
@@ -15708,6 +15963,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}, },
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
},
"lodash.includes": { "lodash.includes": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -15718,6 +15978,11 @@
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
}, },
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
},
"lodash.isinteger": { "lodash.isinteger": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
@@ -15744,6 +16009,11 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true "dev": true
}, },
"lodash.mergewith": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="
},
"lodash.once": { "lodash.once": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
@@ -17905,6 +18175,12 @@
"mimic-fn": "^2.1.0" "mimic-fn": "^2.1.0"
} }
}, },
"openapi-types": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.0.2.tgz",
"integrity": "sha512-GuTo7FyZjOIWVhIhQSWJVaws6A82sWIGyQogxxYBYKZ0NBdyP2CYSIgOwFfSB+UVoPExk/YzFpyYitHS8KVZtA==",
"peer": true
},
"optionator": { "optionator": {
"version": "0.9.1", "version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
@@ -18649,6 +18925,55 @@
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true "dev": true
}, },
"swagger-jsdoc": {
"version": "6.2.5",
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.5.tgz",
"integrity": "sha512-l+cdsKS2y+QDhrH1TJSUiE0y9XKuf5xaGSatjf0hR/wjTlMpO8WfubBK9d/nASdbHPMtj9iJZLBH2ogBEhL7Sw==",
"requires": {
"commander": "6.2.0",
"doctrine": "3.0.0",
"glob": "7.1.6",
"lodash.mergewith": "^4.6.2",
"swagger-parser": "10.0.2",
"yaml": "2.0.0-1"
},
"dependencies": {
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
}
},
"swagger-parser": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.2.tgz",
"integrity": "sha512-9jHkHM+QXyLGFLk1DkXBwV+4HyNm0Za3b8/zk/+mjr8jgOSiqm3FOTHBSDsBjtn9scdL+8eWcHdupp2NLM8tDw==",
"requires": {
"@apidevtools/swagger-parser": "10.0.2"
}
},
"swagger-ui-dist": {
"version": "4.15.5",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz",
"integrity": "sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA=="
},
"swagger-ui-express": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.0.tgz",
"integrity": "sha512-ZxpQFp1JR2RF8Ar++CyJzEDdvufa08ujNUJgMVTMWPi86CuQeVdBtvaeO/ysrz6dJAYXf9kbVNhWD7JWocwqsA==",
"requires": {
"swagger-ui-dist": ">=4.11.0"
}
},
"test-exclude": { "test-exclude": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@@ -18984,6 +19309,11 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true "dev": true
}, },
"yaml": {
"version": "2.0.0-1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ=="
},
"yargs": { "yargs": {
"version": "17.6.2", "version": "17.6.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz",
@@ -19016,6 +19346,25 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true "dev": true
},
"z-schema": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-4.2.4.tgz",
"integrity": "sha512-YvBeW5RGNeNzKOUJs3rTL4+9rpcvHXt5I051FJbOcitV8bl40pEfcG0Q+dWSwS0/BIYrMZ/9HHoqLllMkFhD0w==",
"requires": {
"commander": "^2.7.1",
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"validator": "^13.6.0"
},
"dependencies": {
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"optional": true
}
}
} }
} }
} }
+4
View File
@@ -22,6 +22,8 @@
"query-string": "^7.1.1", "query-string": "^7.1.1",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"stripe": "^10.7.0", "stripe": "^10.7.0",
"swagger-jsdoc": "^6.2.5",
"swagger-ui-express": "^4.6.0",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1", "tweetnacl-util": "^0.15.1",
"typescript": "^4.8.4" "typescript": "^4.8.4"
@@ -56,6 +58,8 @@
"@types/jsonwebtoken": "^8.5.9", "@types/jsonwebtoken": "^8.5.9",
"@types/node": "^18.11.3", "@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6", "@types/nodemailer": "^6.4.6",
"@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3",
"@typescript-eslint/eslint-plugin": "^5.40.1", "@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1", "@typescript-eslint/parser": "^5.40.1",
"eslint": "^8.26.0", "eslint": "^8.26.0",
+1 -1
View File
@@ -1,5 +1,5 @@
const PORT = process.env.PORT || 4000; 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 ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!;
const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d'; const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d';
const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!; const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!;
@@ -8,6 +8,7 @@ import {
STRIPE_PRODUCT_CARD_AUTH STRIPE_PRODUCT_CARD_AUTH
} from '../config'; } from '../config';
import Stripe from 'stripe'; import Stripe from 'stripe';
const stripe = new Stripe(STRIPE_SECRET_KEY, { const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01' apiVersion: '2022-08-01'
}); });
+4 -4
View File
@@ -3,7 +3,7 @@ import rateLimit from 'express-rate-limit';
// 300 requests per 15 minutes // 300 requests per 15 minutes
const apiLimiter = rateLimit({ const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, windowMs: 15 * 60 * 1000,
max: 300, max: 400,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false legacyHeaders: false
}); });
@@ -11,7 +11,7 @@ const apiLimiter = rateLimit({
// 5 requests per hour // 5 requests per hour
const signupLimiter = rateLimit({ const signupLimiter = rateLimit({
windowMs: 60 * 60 * 1000, windowMs: 60 * 60 * 1000,
max: 5, max: 10,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false legacyHeaders: false
}); });
@@ -19,7 +19,7 @@ const signupLimiter = rateLimit({
// 10 requests per hour // 10 requests per hour
const loginLimiter = rateLimit({ const loginLimiter = rateLimit({
windowMs: 60 * 60 * 1000, windowMs: 60 * 60 * 1000,
max: 10, max: 20,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false legacyHeaders: false
}); });
@@ -27,7 +27,7 @@ const loginLimiter = rateLimit({
// 5 requests per hour // 5 requests per hour
const passwordLimiter = rateLimit({ const passwordLimiter = rateLimit({
windowMs: 60 * 60 * 1000, windowMs: 60 * 60 * 1000,
max: 5, max: 10,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false legacyHeaders: false
}); });
+36 -7
View File
@@ -4,9 +4,11 @@ import cors from 'cors';
import cookieParser from 'cookie-parser'; import cookieParser from 'cookie-parser';
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import swaggerJSDoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';
dotenv.config(); dotenv.config();
import * as Sentry from '@sentry/node'; 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 { 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 { apiLimiter } from './helpers/rateLimiter';
@@ -19,12 +21,6 @@ Sentry.init({
environment: NODE_ENV environment: NODE_ENV
}); });
// PostHogClient.init({
// projectApiKey: POSTHOG_PROJECT_API_KEY,
// host: POSTHOG_HOST,
// telemetryEnabled: TELEMETRY_ENABLED
// });
import { import {
signup as signupRouter, signup as signupRouter,
auth as authRouter, auth as authRouter,
@@ -72,6 +68,39 @@ if (NODE_ENV === 'production') {
app.use(helmet()); app.use(helmet());
} }
if (NODE_ENV === 'development') {
const swaggerDefinition = {
openapi: '3.0.0',
info: {
title: 'Infisical API',
version: '1.0.0',
description: 'Infisical is an open-source, E2EE tool to sync environment variables across your team and infrastructure.',
license: {
name: 'License',
url: 'https://github.com/Infisical/infisical/blob/main/LICENSE'
},
contact: {
name: 'Infisical',
url: 'https://infisical.com'
},
},
servers: [
{
url: 'http://localhost:8080/api/v1',
description: 'Development server'
}
]
}
const options = {
swaggerDefinition,
apis: ['./src/routes/*.ts']
}
const swaggerSpec = swaggerJSDoc(options);
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
}
app.use(express.json()); app.use(express.json());
// routers // routers
+47
View File
@@ -11,6 +11,53 @@ router.post(
authController.getNewToken authController.getNewToken
); );
/**
* @swagger
* /auth/login1:
* post:
* summary: Return server's public key and user's salt for login via SRP.
* description: As part of the 1st step of secure remote password (SRP), return server's public key and user's salt.
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* email:
* type: string
* description: Email of user
* example: '[email protected]'
* clientPublicKey:
* type: string
* description: Client-side SRP-generated public key
* example: 'b13dc83cb...'
* responses:
* 200:
* description: Object containing the server's public key and user's salt.
* content:
* application/json:
* schema:
* type: object
* properties:
* serverPublicKey:
* type: string
* description: Server-side SRP-generated public key
* example: 'b13dc83cb...'
* salt:
* type: string
* description: User's salt
* example: 'dc1ac694f...'
* 400:
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: 'Failed to start authentication process'
*/
router.post( router.post(
'/login1', '/login1',
loginLimiter, loginLimiter,
+1 -1
View File
@@ -15,7 +15,7 @@ var rootCmd = &cobra.Command{
Short: "Infisical CLI is used to inject environment variables into any process", Short: "Infisical CLI is used to inject environment variables into any process",
Long: `Infisical is a simple, end-to-end encrypted service that enables teams to sync and manage their environment variables across their development life cycle.`, Long: `Infisical is a simple, end-to-end encrypted service that enables teams to sync and manage their environment variables across their development life cycle.`,
CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true}, CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true},
Version: "0.1.4", Version: "0.1.5",
} }
// Execute adds all child commands to the root command and sets flags appropriately. // Execute adds all child commands to the root command and sets flags appropriately.
+15 -1
View File
@@ -33,6 +33,13 @@ var runCmd = &cobra.Command{
return return
} }
shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
if err != nil {
log.Errorln("Unable to parse the substitute flag")
log.Debugln(err)
return
}
projectId, err := cmd.Flags().GetString("projectId") projectId, err := cmd.Flags().GetString("projectId")
if err != nil { if err != nil {
log.Errorln("Unable to parse the project id flag") log.Errorln("Unable to parse the project id flag")
@@ -82,7 +89,13 @@ var runCmd = &cobra.Command{
} }
} }
execCmd(args[0], args[1:], envsFromApi) if shouldExpandSecrets {
substitutions := util.SubstituteSecrets(envsFromApi)
execCmd(args[0], args[1:], substitutions)
} else {
execCmd(args[0], args[1:], envsFromApi)
}
}, },
} }
@@ -90,6 +103,7 @@ func init() {
rootCmd.AddCommand(runCmd) rootCmd.AddCommand(runCmd)
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from") runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
runCmd.Flags().String("projectId", "", "The project ID from which your secrets should be pulled from") runCmd.Flags().String("projectId", "", "The project ID from which your secrets should be pulled from")
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
} }
// Credit: inspired by AWS Valut // Credit: inspired by AWS Valut
+14
View File
@@ -0,0 +1,14 @@
package models
import log "github.com/sirupsen/logrus"
// Custom error type so that we can give helpful messages in CLI
type Error struct {
Err error
DebugMessage string
FriendlyMessage string
}
func (e *Error) printFriendlyMessage() {
log.Infoln(e.FriendlyMessage)
}
+71
View File
@@ -4,6 +4,7 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"regexp"
"strings" "strings"
"github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/models"
@@ -205,3 +206,73 @@ func GetWorkSpacesFromAPI(userCreds models.UserCredentials) (workspaces []models
return getWorkSpacesResponse.Workspaces, nil return getWorkSpacesResponse.Workspaces, nil
} }
func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variableWeAreLookingFor string, hashMapOfCompleteVariables map[string]string, hashMapOfSelfRefs map[string]string) string {
if value, found := hashMapOfCompleteVariables[variableWeAreLookingFor]; found {
return value
}
for _, secret := range secrets {
if secret.Key == variableWeAreLookingFor {
regex := regexp.MustCompile(`\${([^\}]*)}`)
variablesToPopulate := regex.FindAllString(secret.Value, -1)
// case: variable is a constant so return its value
if len(variablesToPopulate) == 0 {
return secret.Value
}
valueToEdit := secret.Value
for _, variableWithSign := range variablesToPopulate {
variableWithoutSign := strings.Trim(variableWithSign, "}")
variableWithoutSign = strings.Trim(variableWithoutSign, "${")
// case: reference to self
if variableWithoutSign == secret.Key {
hashMapOfSelfRefs[variableWithoutSign] = variableWithoutSign
continue
} else {
var expandedVariableValue string
if preComputedVariable, found := hashMapOfCompleteVariables[variableWithoutSign]; found {
expandedVariableValue = preComputedVariable
} else {
expandedVariableValue = getExpandedEnvVariable(secrets, variableWithoutSign, hashMapOfCompleteVariables, hashMapOfSelfRefs)
hashMapOfCompleteVariables[variableWithoutSign] = expandedVariableValue
}
// If after expanding all the vars above, is the current var a self ref? if so no replacement needed for it
if _, found := hashMapOfSelfRefs[variableWithoutSign]; found {
continue
} else {
valueToEdit = strings.ReplaceAll(valueToEdit, variableWithSign, expandedVariableValue)
}
}
}
return valueToEdit
} else {
continue
}
}
return "${" + variableWeAreLookingFor + "}"
}
func SubstituteSecrets(secrets []models.SingleEnvironmentVariable) []models.SingleEnvironmentVariable {
hashMapOfCompleteVariables := make(map[string]string)
hashMapOfSelfRefs := make(map[string]string)
expandedSecrets := []models.SingleEnvironmentVariable{}
for _, secret := range secrets {
expandedVariable := getExpandedEnvVariable(secrets, secret.Key, hashMapOfCompleteVariables, hashMapOfSelfRefs)
expandedSecrets = append(expandedSecrets, models.SingleEnvironmentVariable{
Key: secret.Key,
Value: expandedVariable,
})
}
return expandedSecrets
}
+160
View File
@@ -0,0 +1,160 @@
package util
import (
"testing"
"github.com/Infisical/infisical-merge/packages/models"
)
// References to self should return the value unaltered
func Test_SubstituteSecrets_When_ReferenceToSelf(t *testing.T) {
var tests = []struct {
Key string
Value string
ExpectedValue string
}{
{Key: "A", Value: "${A}", ExpectedValue: "${A}"},
{Key: "A", Value: "${A} ${A}", ExpectedValue: "${A} ${A}"},
{Key: "A", Value: "${A}${A}", ExpectedValue: "${A}${A}"},
}
for _, test := range tests {
secret := models.SingleEnvironmentVariable{
Key: test.Key,
Value: test.Value,
}
secrets := []models.SingleEnvironmentVariable{secret}
result := SubstituteSecrets(secrets)
if result[0].Value != test.ExpectedValue {
t.Errorf("Test_SubstituteSecrets_When_ReferenceToSelf: expected %s but got %s for input %s", test.ExpectedValue, result[0].Value, test.Value)
}
}
}
func Test_SubstituteSecrets_When_ReferenceDoesNotExist(t *testing.T) {
var tests = []struct {
Key string
Value string
ExpectedValue string
}{
{Key: "A", Value: "${X}", ExpectedValue: "${X}"},
{Key: "A", Value: "${H}HELLO", ExpectedValue: "${H}HELLO"},
{Key: "A", Value: "${L}${S}", ExpectedValue: "${L}${S}"},
}
for _, test := range tests {
secret := models.SingleEnvironmentVariable{
Key: test.Key,
Value: test.Value,
}
secrets := []models.SingleEnvironmentVariable{secret}
result := SubstituteSecrets(secrets)
if result[0].Value != test.ExpectedValue {
t.Errorf("Test_SubstituteSecrets_When_ReferenceToSelf: expected %s but got %s for input %s", test.ExpectedValue, result[0].Value, test.Value)
}
}
}
func Test_SubstituteSecrets_When_ReferenceDoesNotExist_And_Self_Referencing(t *testing.T) {
tests := []struct {
Key string
Value string
ExpectedValue string
}{
{
Key: "O",
Value: "${P} ==$$ ${X} ${UNKNOWN} ${A}",
ExpectedValue: "DOMAIN === ${A} DOMAIN >>> ==$$ DOMAIN ${UNKNOWN} ${A}",
},
{
Key: "X",
Value: "DOMAIN",
ExpectedValue: "DOMAIN",
},
{
Key: "A",
Value: "*${A}* ${X}",
ExpectedValue: "*${A}* DOMAIN",
},
{
Key: "H",
Value: "${X} >>>",
ExpectedValue: "DOMAIN >>>",
},
{
Key: "P",
Value: "DOMAIN === ${A} ${H}",
ExpectedValue: "DOMAIN === ${A} DOMAIN >>>",
},
{
Key: "T",
Value: "${P} ==$$ ${X} ${UNKNOWN} ${A} ${P} ==$$ ${X} ${UNKNOWN} ${A}",
ExpectedValue: "DOMAIN === ${A} DOMAIN >>> ==$$ DOMAIN ${UNKNOWN} ${A} DOMAIN === ${A} DOMAIN >>> ==$$ DOMAIN ${UNKNOWN} ${A}",
},
{
Key: "S",
Value: "${ SSS$$ ${HEY}",
ExpectedValue: "${ SSS$$ ${HEY}",
},
}
secrets := []models.SingleEnvironmentVariable{}
for _, test := range tests {
secrets = append(secrets, models.SingleEnvironmentVariable{Key: test.Key, Value: test.Value})
}
results := SubstituteSecrets(secrets)
for index, expanded := range results {
if expanded.Value != tests[index].ExpectedValue {
t.Errorf("Test_SubstituteSecrets_When_ReferenceToSelf: expected [%s] but got [%s] for input [%s]", tests[index].ExpectedValue, expanded.Value, tests[index].Value)
}
}
}
func Test_SubstituteSecrets_When_No_SubstituteNeeded(t *testing.T) {
tests := []struct {
Key string
Value string
ExpectedValue string
}{
{
Key: "DOMAIN",
Value: "infisical.com",
ExpectedValue: "infisical.com",
},
{
Key: "API_KEY",
Value: "hdgsvjshcgkdckhevdkd",
ExpectedValue: "hdgsvjshcgkdckhevdkd",
},
{
Key: "ENV",
Value: "PROD",
ExpectedValue: "PROD",
},
}
secrets := []models.SingleEnvironmentVariable{}
for _, test := range tests {
secrets = append(secrets, models.SingleEnvironmentVariable{Key: test.Key, Value: test.Value})
}
results := SubstituteSecrets(secrets)
for index, expanded := range results {
if expanded.Value != tests[index].ExpectedValue {
t.Errorf("Test_SubstituteSecrets_When_ReferenceToSelf: expected [%s] but got [%s] for input [%s]", tests[index].ExpectedValue, expanded.Value, tests[index].Value)
}
}
}
+2
View File
@@ -57,6 +57,8 @@ services:
- NEXT_PUBLIC_WEBSITE_URL=${SITE_URL} - NEXT_PUBLIC_WEBSITE_URL=${SITE_URL}
- NEXT_PUBLIC_POSTHOG_HOST=${POSTHOG_HOST} - NEXT_PUBLIC_POSTHOG_HOST=${POSTHOG_HOST}
- NEXT_PUBLIC_POSTHOG_API_KEY=${POSTHOG_PROJECT_API_KEY} - NEXT_PUBLIC_POSTHOG_API_KEY=${POSTHOG_PROJECT_API_KEY}
- NEXT_PUBLIC_STRIPE_PRODUCT_PRO=${STRIPE_PRODUCT_PRO}
- NEXT_PUBLIC_STRIPE_PRODUCT_STARTER=${STRIPE_PRODUCT_STARTER}
- NEXT_PUBLIC_TELEMETRY_ENABLED=${TELEMETRY_ENABLED} - NEXT_PUBLIC_TELEMETRY_ENABLED=${TELEMETRY_ENABLED}
networks: networks:
- infisical-dev - infisical-dev
+3
View File
@@ -49,6 +49,9 @@ services:
- NEXT_PUBLIC_WEBSITE_URL=${SITE_URL} - NEXT_PUBLIC_WEBSITE_URL=${SITE_URL}
- NEXT_PUBLIC_POSTHOG_HOST=${POSTHOG_HOST} - NEXT_PUBLIC_POSTHOG_HOST=${POSTHOG_HOST}
- NEXT_PUBLIC_POSTHOG_API_KEY=${POSTHOG_PROJECT_API_KEY} - NEXT_PUBLIC_POSTHOG_API_KEY=${POSTHOG_PROJECT_API_KEY}
- NEXT_PUBLIC_STRIPE_PRODUCT_PRO=${STRIPE_PRODUCT_PRO}
- NEXT_PUBLIC_STRIPE_PRODUCT_STARTER=${STRIPE_PRODUCT_STARTER}
- NEXT_PUBLIC_TELEMETRY_ENABLED=${TELEMETRY_ENABLED}
networks: networks:
- infisical - infisical
@@ -1,10 +1,12 @@
--- ---
title: "Installation" title: "Overview"
--- ---
Prerequisite: [Setup an account](../../getting-started/dashboard/create-account) with Infisical Cloud or via self-hosted installation. Prerequisite: Set up an account with [Infisical Cloud](https://app.infisical.com) or via a [self-hosted installation](/self-hosting/overview).
Follow the guide for your OS below to install the CLI. The Infisical CLI provides a way to inject environment variables from the platform into your apps and infrastructure.
## Installation
<Tabs> <Tabs>
<Tab title="MacOS"> <Tab title="MacOS">
@@ -18,7 +20,7 @@ Follow the guide for your OS below to install the CLI.
infisical --version infisical --version
``` ```
To update: ## Updates
```bash ```bash
brew upgrade infisical brew upgrade infisical
@@ -37,7 +39,7 @@ Follow the guide for your OS below to install the CLI.
infisical --version infisical --version
``` ```
To update: ## Updates
```bash ```bash
scoop update infisical scoop update infisical
+20
View File
@@ -0,0 +1,20 @@
---
title: "Commands"
---
## Commands
| Command | Description |
| ------- | -------------------------------------------------------------------- |
| `login` | Used to authenticate and set the logged in user. |
| `init` | Used to link a local project to the platform. |
| `run` | Used to inject envars from the platform into an application process. |
## Global options
| Option | Description |
| ----------------- | ----------------------------------------------- |
| `--help`, `-h` | List help for any command |
| `--debug`, `-d` | Enable verbose logging |
| `--domain` | Use to direct Infisical to a self-hosted domain |
| `--version`, `-v` | Print version information and quit |
+13
View File
@@ -0,0 +1,13 @@
---
title: "infisical init"
---
```bash
infisical init
```
## Description
Link a local project to the platform
The command creates a `infisical.json` file containing your Project ID.
+13
View File
@@ -0,0 +1,13 @@
---
title: "infisical login"
---
```bash
infisical login
```
## Description
Verify a user and save credentials to the system keyring.
To change the logged in user, run the command again to overwrite the previous login.
+19
View File
@@ -0,0 +1,19 @@
---
title: "infisical run"
---
```bash
infisical run [options] -- [your application start command]
```
## Description
Inject environment variables from the platform into an application process.
## Options
| Option | Description | Default value |
| -------------- | ----------------------------------------------------------------------------------------------------------- | ------------- |
| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` |
| `--projectId` | Used to link a local project to the platform (required only if injecting via the service token method) | `None` |
| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` |
+52
View File
@@ -0,0 +1,52 @@
---
title: "Usage"
---
Prerequisite: [Install the CLI](/cli/overview)
## Log in to the Infisical CLI
```bash
infisical login
```
## Initialize Infisical for your project
```bash
# move to your project
cd /path/to/project
# initialize infisical
infisical init
```
## Inject environment variables
```bash
# inject environment variables into app
infisical run -- [your application start command]
```
Options you can specify:
| Option | Description | Default value |
| ------------- | ----------------------------------------------------------------------------------------------------------- | ------------- |
| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` |
| `--projectId` | Used to link a local project to the platform (required only if injecting via the service token method) | `None` |
| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` |
## Examples:
```bash
# example with node
infisical run -- node index.js
# example with node (nodemon)
infisical run -- nodemon index.js
# example with node (nodemon) pulling in secrets from test environment
infisical run --env=test -- nodemon index.js
# example with flask
infisical run -- flask run
```
-27
View File
@@ -1,27 +0,0 @@
---
title: "Architecture"
---
Infisical is an open-source collection of services for simple secret management built on top of Typescript, Javascript (ongoing conversion to TS), and Go. It's all dockerized and can be spun up with Docker Compose.
![architecture](../images/architecture-diagram2.png)
## NGINX
NGINX is a reverse-proxy and load balancer that sits in front of Infisical. It forwards requests to the frontend and backend services.
## Frontend
The frontend service renders the Web UI using Next.js.
## Backend
The backend service provides the back-of-house logic for secret management.
## Database
The (MongoDB) database stores all data and (encrypted) secrets.
## CLI
The platform-agnostic CLI allows you to inject environment variables from Infisical into apps and infrastructure.
+1
View File
@@ -40,6 +40,7 @@ docker-compose -f docker-compose.dev.yml up
The docker-compose development environment consists of: The docker-compose development environment consists of:
- nginx
- frontend - frontend
- backend - backend
- mongo - mongo
-56
View File
@@ -1,56 +0,0 @@
---
title: "Usage"
---
Prerequisite: [Install the CLI](../../getting-started/cli/installation)
## Login
Login in using the `login` command in your terminal. Logging in is a one-time, post-installation action that authenticates you with the platform — to change users, you can run the command again.
```bash
infisical login
```
## Initialization
In the root of your local project, initialize Infisical and follow steps to connect your project to the platform.
```bash
cd /path/to/project
# initialization
infisical init
```
## Injecting environment variables
To inject environment variables from the platform to your project, use the `run` command.
```bash
# command
infisical run -- [your application start command]
```
Options you can specify:
| Option | Description | Default value |
| ------------- | ----------------------------------------------------------------------------------------------------------- | ------------- |
| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` |
| `--projectId` | Used to link a local project to the platform (required only if injecting via the service token method) | `None` |
Examples:
```bash
# example with node
infisical run -- node index.js
# example with node (nodemon)
infisical run -- nodemon index.js
# example with node (nodemon) pulling in secrets from test environment
infisical run --env=test -- nodemon index.js
# example with flask
infisical run -- flask run
```
-55
View File
@@ -1,55 +0,0 @@
---
title: "Reference"
---
## Commands
| Command | Description | Options |
| ------- | -------------------------------------------------------------------- | ---------------------- |
| `login` | Used to authenticate and set the logged in user. |
| `init` | Used to link a local project to the platform. |
| `run` | Used to inject envars from the platform into an application process. | `--projectId`, `--env` |
## Global options
| Option | Description |
| ----------------- | ---------------------------------- |
| `--help`, `-h` | List help for any command |
| `--debug`, `-d` | Enable verbose logging |
| `--domain` | Use to direct Infisical to |
| `--version`, `-v` | Print version information and quit |
### Login
Used to authenticate and set the logged in user.
Post-authentication credentials are saved securely in your system keyring. Since only one user can be logged in at a time, to change the logged in user, run the command again to overwrite the previous login.
```bash
infisical login
```
### Init
Used to link a local project to the platform (cloud or self-hosted)
Run this command at the root of your local project. You will have to run this command for each new project you create locally.
```bash
infisical init
```
### Run
Used to inject environment variables from the platform into an application process.
```bash
infisical run [options] -- [your application start command]
```
Options you can specify:
| Option | Description | Default value |
| ------------- | ----------------------------------------------------------------------------------------------------------- | ------------- |
| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` |
| `--projectId` | Used to link a local project to the platform (required only if injecting via the service token method) | `None` |
@@ -4,7 +4,7 @@ title: "Sign up"
## Self-hosted ## Self-hosted
If you're using a self-hosted installation, follow the [setup](/self-hosting/overview) then open your website URL `{WEBSITE_URL}/login`. If you're using a self-hosted installation, follow the [setup](/self-hosting/overview) then open your site URL `{SITE_URL}`.
## Infisical Cloud ## Infisical Cloud
@@ -8,6 +8,6 @@ We’re still early with integrations but you’ll be able to sync environment v
Check out integrations: Check out integrations:
- Heroku - [Heroku](/integrations/heroku)
- Docker - [Docker](/integrations/docker)
- Docker Compose - [Docker Compose](/integrations/docker-compose)
+2 -4
View File
@@ -4,11 +4,11 @@ title: "Infisical Token"
An Infisical Token is needed to authenticate the CLI when there isn't an easy way to manually type in your login credentials to sync environment variables to your applications. An Infisical Token is needed to authenticate the CLI when there isn't an easy way to manually type in your login credentials to sync environment variables to your applications.
It grants read-only access to a particular environment and project for a specified amount of time; once the token expires, any CLI application that relies on it for authentication will be denied access to retrieve related secrets. It grants read-only access to a particular environment and project for a specified amount of time.
This is useful in the following contexts: This is useful in the following contexts:
- [Docker](../../integrations/docker)/[Docker-Compose](../../integrations/docker-compose) integration: An Infisical Token can be passed to a Docker container as an environment variable for the CLI to authenticate and pull its corresponding secrets. - [Docker](/integrations/docker)/[Docker-Compose](/integrations/docker-compose) integration: An Infisical Token can be passed to a Docker container as an environment variable for the CLI to authenticate and pull its corresponding secrets.
## Generate an Infisical Token ## Generate an Infisical Token
@@ -19,5 +19,3 @@ It's possible to generate an Infisical token in the settings of a project.
![token name](../../images/project-token-name.png) ![token name](../../images/project-token-name.png)
![token added](../../images/project-token-added.png) ![token added](../../images/project-token-added.png)
To use the Infisical Token in the CLI, check out the docs for that [here](../../getting-started/cli/token).
+16 -38
View File
@@ -20,46 +20,24 @@ The CLI is used to inject environment variables into applications and infrastruc
- Inject environment variables. - Inject environment variables.
- Inject environment variables into containers via service tokens for Docker. - Inject environment variables into containers via service tokens for Docker.
## Integrations
We're still early with integrations but you'll be able to sync environment variables across your entire infrastructure from local development to CI/CD and production.
| Integration | Status |
| -------------- | ----------- |
| Docker | Available |
| Docker-Compose | Available |
| Kubernetes | Coming soon |
| Vercel | Coming soon |
| AWS | Coming soon |
| GCP | Coming soon |
| Azure | Coming soon |
| DigitalOcean | Coming soon |
| GitLab | Coming soon |
| CircleCI | Coming soon |
| TravisCI | Coming soon |
| GitHub Actions | Coming soon |
| Jenkins | Coming soon |
Missing an integration? Throw in a request.
## Roadmap ## Roadmap
We're building the future of secret management, one that's comprehensive and accessible to all. Some high-level features we have in mind: We're building the future of secret management, one that's comprehensive and accessible to all. Some high-level features we have in mind:
| Feature | Status | | Feature | Status |
| ------------------------------------- | --------------- | | ------------------------------------- | ----------- |
| Integrations | Ongoing | | Integrations | Ongoing |
| More hosting options | Ongoing | | More hosting options | Ongoing |
| 1-Click Deploys | Ongoing | | 1-Click Deploys | Ongoing |
| Account recovery: Backup key | Ongoing | | Account recovery: Backup key | Ongoing |
| Account recovery: Member-assisted | Not yet started | | Account recovery: Member-assisted | Coming soon |
| Slack & MS teams integrations | Not yet started | | Slack & MS teams integrations | Coming soon |
| Access logs | Not yet started | | Access logs | Coming soon |
| Version control for secrets | Not yet started | | Version control for secrets | Coming soon |
| 2FA | Not yet started | | 2FA | Coming soon |
| Restricted IPs | Not yet started | | Restricted IPs | Coming soon |
| Read/write access controls | Not yet started | | Read/write access controls | Coming soon |
| Secret rotation | Not yet started | | Secret rotation | Coming soon |
| Comparing secrets across environments | Not yet started | | Comparing secrets across environments | Coming soon |
Interested in contributing? Check out the guide. Interested in contributing? Check out the [guide](/contributing/overview).
+31 -15
View File
@@ -2,20 +2,36 @@
title: "Introduction" title: "Introduction"
--- ---
<iframe Infisical is an [open-source](https://opensource.com/resources/what-open-source), [end-to-end encrypted](https://en.wikipedia.org/wiki/End-to-end_encryption) secret manager that enables teams to easily manage and sync their environment variables.
src="https://www.youtube.com/embed/JS3OKYU2078"
width="100%"
height="400"
></iframe>
Infisical is an [open-source](https://opensource.com/resources/what-open-source), end-to-end encrypted (E2EE) secret manager that enables teams to easily manage and sync their environment variables. Start syncing environment variables with [Infisical Cloud](https://app.infisical.com) or learn how to [host Infisical](/self-hosting/overview) yourself.
It stops [secret sprawl](https://www.gitguardian.com/glossary/secret-sprawl-definition) by providing a single source-of-truth for environment variables. It offers a dashboard for teams to manage environment variables and a platform-agnostic CLI to inject them into apps and infrastructure. <CardGroup cols={2}>
<Card
Some problems we solve: title="Quickstart"
href="/getting-started/quickstart"
- Leaking .env files to version control. icon="timer"
- Debugging missing environment variables. color="#ea5a0c"
- Sending environment variables over email. >
Tour Infisical in a few minutes.
Infisical uses [end-to-end encryption](https://en.wikipedia.org/wiki/End-to-end_encryption) to ensure that only designated team members can read their environment variables; unless intended for specific integrations, environment variables are always encrypted before being sent to the server. </Card>
<Card href="/cli/overview" title="CLI" icon="square-terminal" color="#16a34a">
Install the CLI to inject secrets into apps and infra.
</Card>
<Card
href="/self-hosting/overview"
title="Self-hosting"
icon="server"
color="#0285c7"
>
Learn how to configure and deploy Infisical.
</Card>
<Card
href="/integrations/heroku"
title="Integrations"
icon="plug"
color="#dc2626"
>
Explore integrations for Docker, AWS, Heroku, etc.
</Card>
</CardGroup>
+43
View File
@@ -0,0 +1,43 @@
---
title: "Quickstart"
---
This example demonstrates how to store and inject environment variables from [Infisical Cloud](https://app.infisical.com) into your application.
Note that the Infisical CLI is platform-agnostic and can inject environment variables across many tech stacks and frameworks.
## Set up Infisical Cloud
1. Login or create an accout at `app.infisical.com`.
2. Create a new project.
3. Populate your environment variables as in the image below.
![project quickstart](../images/project-quickstart.png)
## Set up the CLI
1. Follow the instructions to [install the CLI](/cli/overview).
2. Initialize Infisical for your project.
```bash
# move to your project
cd /path/to/project
# initialize infisical
infisical init
```
## Start your app with environment variables injected
```bash
# inject environment variables into app
infisical run -- [your application start command]
```
<Info>
Check out our [integrations](/integrations/overview) for injecting environment
variables into frameworks and platforms like Docker.
</Info>
Your app should be running with the environment variables injected.
@@ -1,11 +0,0 @@
---
title: "Statement"
---
As a secrets manager, we are deeply committed to enforcing the privacy and security of all users and data on the platform but acknowledge that it is virtually impossible to guarantee perfect security; unfortunately, even the most secure systems have vulnerabilities.
As part of our commitment, we do our best to maintain platform privacy and security, notify users if anything goes wrong, and rectify adverse situations immediately if anything happens. As Infisical grows, we will be adding more opt-in security measures to ensure better data protection and maintain trust within the growing community. With that, let’s make the most simple and secure secrets management system out there!
Best,
Infisical Team
Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

+15 -11
View File
@@ -3,23 +3,26 @@ title: "Docker Compose"
--- ---
### Step 1: Add CLI to your Dockerfile ### Step 1: Add CLI to your Dockerfile
Follow steps 1 through 3 on our [guide to configure Infisical CLI](/docker) in your Dockerfile.
Follow steps 1 through 3 on our [guide to configure Infisical CLI](../getting-started/cli/installation) in your Dockerfile.
### Step 2: Generate Infisical Token ### Step 2: Generate Infisical Token
In order for Infisical CLI to authenticate and retrieve your project's secrets without exposing your login credentials, you must generate a Infisical Token. In order for Infisical CLI to authenticate and retrieve your project's secrets without exposing your login credentials, you must generate a Infisical Token.
To learn how, visit [Infisical Token](../getting-started/cli/infisical-token). Once you have generated the token, keep it handy. To learn how, visit [Infisical Token](../getting-started/cli/infisical-token). Once you have generated the token, keep it handy.
<Info> <Info>
If you have multiple services and they do not use the same secrets, you will have to generate a Infisical Token for each service. If you have multiple services and they do not use the same secrets, you will
have to generate a Infisical Token for each service.
</Info> </Info>
### Step 3: Tell Docker Compose your Infisical Token ### Step 3: Tell Docker Compose your Infisical Token
For each service you want to inject secrets into, set an environment variable called `INFISICAL_TOKEN` equal to a helpful identifier variable. For each service you want to inject secrets into, set an environment variable called `INFISICAL_TOKEN` equal to a helpful identifier variable.
This will ensure that you can set Infisical Tokens for multiple services. This will ensure that you can set Infisical Tokens for multiple services.
For the example below, we have set `INFISICAL_TOKEN_FOR_WEB` and `INFISICAL_TOKEN_FOR_API` as the `INFISICAL_TOKEN` for the corresponding service. For the example below, we have set `INFISICAL_TOKEN_FOR_WEB` and `INFISICAL_TOKEN_FOR_API` as the `INFISICAL_TOKEN` for the corresponding service.
```yaml ```yaml
# Example Docker Compose file # Example Docker Compose file
services: services:
@@ -36,13 +39,14 @@ services:
container_name: auledge-backend container_name: auledge-backend
environment: environment:
- INFISICAL_TOKEN=${INFISICAL_TOKEN_FOR_API} - INFISICAL_TOKEN=${INFISICAL_TOKEN_FOR_API}
``` ```
### 4: Export shell variables
Next, set the shell variables you defined in your compose file. This can be done manually or via your CI/CD environment. Once done, it will be used to populate the corresponding `INFISICAL_TOKEN`
in your Docker Compose file.
``` bash ### 4: Export shell variables
Next, set the shell variables you defined in your compose file. This can be done manually or via your CI/CD environment. Once done, it will be used to populate the corresponding `INFISICAL_TOKEN`
in your Docker Compose file.
```bash
#Example #Example
# Token refers to the token we generated in step 2 for this service # Token refers to the token we generated in step 2 for this service
@@ -51,6 +55,6 @@ export INFISICAL_TOKEN_FOR_WEB=<token>
# Token refers to the token we generated in step 2 for this service # Token refers to the token we generated in step 2 for this service
export INFISICAL_TOKEN_FOR_API=<token> export INFISICAL_TOKEN_FOR_API=<token>
# Then run your compose file in the same terminal. # Then run your compose file in the same terminal.
docker-compose ... docker-compose ...
``` ```
+2 -4
View File
@@ -2,8 +2,6 @@
title: "Docker" title: "Docker"
--- ---
Prerequisite: [Infisical Token and How to Generate One](../../getting-started/dashboard/token).
## Step 1: Add CLI to your Dockerfile ## Step 1: Add CLI to your Dockerfile
<Tabs> <Tabs>
@@ -33,7 +31,7 @@ Prerequisite: [Infisical Token and How to Generate One](../../getting-started/da
## Step 2: Generate Infisical Token ## Step 2: Generate Infisical Token
In order for the CLI to authenticate and retrieve your project's secrets without requiring your login credentials, you must [generate an Infisical Token](../../getting-started/dashboard/token); keep it handy. [Generate an Infisical Token](../../getting-started/dashboard/token) and keep it handy.
## Step 3: Set start command of your container ## Step 3: Set start command of your container
@@ -56,7 +54,7 @@ Required options:
The CLI looks out for an environment variable called the `INFISICAL_TOKEN` which you can set depending on where you run the CLI. If `INFISICAL_TOKEN` is detected by the CLI, it will authenticate and retrieve the environment variables which the token is authorized for. The CLI looks out for an environment variable called the `INFISICAL_TOKEN` which you can set depending on where you run the CLI. If `INFISICAL_TOKEN` is detected by the CLI, it will authenticate and retrieve the environment variables which the token is authorized for.
```bash ```bash
docker run --env INFISICAL_TOKEN=<the-token-you-got-from-step-2>... docker run --env INFISICAL_TOKEN=[the-token-you-got-from-step-2]...
``` ```
Note: `INFISICAL_TOKEN` is the token you generated in step 2. Note: `INFISICAL_TOKEN` is the token you generated in step 2.
+8 -6
View File
@@ -5,20 +5,22 @@ description: "With this integration, you can automatically sync your secrets to
## Instructions ## Instructions
### Step 1: Open the integrations csonsole ### Step 1: Open the integrations console
Open the Infisical Dashboard. Choose the project in which you want to set up the intergation. Go to the integrations tab in the left sidebar. Open the Infisical Dashboard. Choose the project in which you want to set up the intergation. Go to the integrations tab in the left sidebar.
### Step 2: Authenticate with Heroku ### Step 2: Authenticate with Heroku
Click on Heroku in the list of available integrations. Log in if asked by Heroku and provide the necessary permissions to Infisical. You will afterwards be redirected back to the integrations page. Click on "Heroku" tile. Log in if required and provide the necessary permissions to Infisical. You will afterwards be redirected back to the integrations page.
Note: during an integration with Heroku, for security reasons, it is impossible to maintain end-to-end encryption. In theory, this lets Infisical decrypt yor environment variables. In practice, we can assure you that this will never be done, and it allows us to protect your secrets from bad actors online. The core Infisical service will always stay end-to-end encrypted. With any questions, reach out [email protected]. Note: during an integration with Heroku, for security reasons, it is impossible to maintain end-to-end encryption. In theory, this lets Infisical decrypt yor environment variables. In practice, we can assure you that this will never be done, and it allows us to protect your secrets from bad actors online. With any questions, reach out [email protected].
### Step 3: Start integration ### Step 3: Start integration
Once the integration is set up, choose a Heroku App that you want to sync the secrets to, and the Infisical project environment that you would to sync the secrets from. Click on the "Start Integration" button. Choose a Heroku App that you want to sync the secrets to, and the Infisical project environment that you want to sync the secrets from. Start the integration.
### Step 4: You're good to go! The integration should now show status 'In Sync'. Every time you edit secrets, they will be automatically pushed to Heroku.
The integration should now show status 'In Sync'. Every time you edit the secrets, they will be automatically pushed to Heroku. If you want to update anything in your integration, you will have to delete the current one and create a new one. <Info>
If you need to update your integration, you will have to delete the current one and create a new one.
</Info>
+24
View File
@@ -0,0 +1,24 @@
---
title: "Overview"
---
Integrations allow environment variables to be synced across your entire infrastructure from local development to CI/CD and production.
Missing an integration? Throw in a [request](https://github.com/Infisical/infisical/issues).
| Integration | Status |
| ---------------------------------------------- | ----------- |
| [Docker](/integrations/docker) | Available |
| [Docker-Compose](/integrations/docker-compose) | Available |
| [Heroku](/integrations/heroku) | Available |
| Kubernetes | Coming soon |
| Vercel | Coming soon |
| AWS | Coming soon |
| GCP | Coming soon |
| Azure | Coming soon |
| DigitalOcean | Coming soon |
| GitLab | Coming soon |
| CircleCI | Coming soon |
| TravisCI | Coming soon |
| GitHub Actions | Coming soon |
| Jenkins | Coming soon |
+54 -42
View File
@@ -35,17 +35,22 @@
], ],
"navigation": [ "navigation": [
{ {
"group": "Platform", "group": " ",
"pages": [ "pages": [
"getting-started/introduction", {
"getting-started/features", "group": "Overview",
"pages": [
"getting-started/introduction",
"getting-started/quickstart",
"getting-started/features"
]
},
{ {
"group": "Security", "group": "Security",
"pages": [ "pages": [
"getting-started/security/overview", "security/overview",
"getting-started/security/data-model", "security/data-model",
"getting-started/security/mechanics", "security/mechanics"
"getting-started/security/statement"
] ]
}, },
{ {
@@ -56,49 +61,56 @@
"getting-started/dashboard/project", "getting-started/dashboard/project",
"getting-started/dashboard/integrations", "getting-started/dashboard/integrations",
"getting-started/dashboard/token" "getting-started/dashboard/token"
] ]
}, },
{ {
"group": "Command Line", "group": "CLI",
"pages": [ "pages": [
"getting-started/cli/installation", "cli/overview",
"getting-started/cli/cli-guide", "cli/usage",
"getting-started/cli/token", {
"getting-started/cli/reference" "group": "Reference",
"pages": [
"cli/reference/commands",
"cli/reference/login",
"cli/reference/init",
"cli/reference/run"
]
}
] ]
}
]
},
{
"group": "Integrations",
"pages": [
"integrations/heroku",
"integrations/docker",
"integrations/docker-compose"
]
},
{
"group": "Self-hosting",
"pages": [
"self-hosting/overview",
{
"group": "Deployments",
"pages": ["self-hosting/deployments/linux"]
}, },
{ {
"group": "Configuration", "group": "Integrations",
"pages": ["self-hosting/configuration/envars"] "pages": [
"integrations/overview",
"integrations/heroku",
"integrations/docker",
"integrations/docker-compose"
]
},
{
"group": "Self-hosting",
"pages": [
"self-hosting/overview",
{
"group": "Deployments",
"pages": ["self-hosting/deployments/linux"]
},
{
"group": "Configuration",
"pages": ["self-hosting/configuration/envars"]
}
]
},
{
"group": "Contributing",
"pages": [
"contributing/overview",
"contributing/code-of-conduct",
"contributing/developing"
]
} }
] ]
},
{
"group": "Contributing",
"pages": [
"contributing/overview",
"contributing/code-of-conduct",
"contributing/developing",
"contributing/architecture"
]
} }
], ],
"backgroundImage": "/images/background.png" "backgroundImage": "/images/background.png"
@@ -18,3 +18,13 @@ In subsequent sections, we refer:
- To users uploading their secrets to Infisical as “senders” and those receiving secrets as “receivers". For instance, if Bob and Alice are both enrolled in a project and Bob adds new secrets to the project to be pulled by Alice, then Bob is considered to be the sender and Alice the receiver. - To users uploading their secrets to Infisical as “senders” and those receiving secrets as “receivers". For instance, if Bob and Alice are both enrolled in a project and Bob adds new secrets to the project to be pulled by Alice, then Bob is considered to be the sender and Alice the receiver.
- To any activity involving uploading or modifying secrets to Infisical as "pushing" and fetching secrets from Infisical as "pulling." - To any activity involving uploading or modifying secrets to Infisical as "pushing" and fetching secrets from Infisical as "pulling."
## Statement
As a secrets manager, we are deeply committed to enforcing the privacy and security of all users and data on the platform but acknowledge that it is virtually impossible to guarantee perfect security; unfortunately, even the most secure systems have vulnerabilities.
As part of our commitment, we do our best to maintain platform privacy and security, notify users if anything goes wrong, and rectify adverse situations immediately if anything happens. As Infisical grows, we will be adding more opt-in security measures to ensure better data protection and maintain trust within the growing community. With that, let’s make the most simple and secure secrets management system out there!
Best,
Infisical Team
+4
View File
@@ -17,6 +17,10 @@ Infisical Cloud also comes with some extra features unavailabe in the self-hoste
Infisical can be deployed on a Linux VM with docker-compose. We're rolling out more specific deployment options for DigitalOcean, AWS, GCP, and Azure soon. Infisical can be deployed on a Linux VM with docker-compose. We're rolling out more specific deployment options for DigitalOcean, AWS, GCP, and Azure soon.
Options:
- [Linux VM](/self-hosting/deployments/linux)
## Telemetry ## Telemetry
Infisical collects telemetry data about general usage. Infisical collects telemetry data about general usage.
+46
View File
@@ -0,0 +1,46 @@
{
"extends": "next/core-web-vitals",
"plugins": [
"simple-import-sort"
],
"rules": {
"react-hooks/exhaustive-deps": "off",
"simple-import-sort/exports": "warn",
"simple-import-sort/imports": [
"warn",
{
"groups": [
// Node.js builtins. You could also generate this regex if you use a `.js` config.
// For example: `^(${require("module").builtinModules.join("|")})(/|$)`
// Note that if you use the `node:` prefix for Node.js builtins,
// you can avoid this complexity: You can simply use "^node:".
[
"^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)"
],
// Packages `react` related packages
[
"^react",
"^next",
"^@?\\w"
],
// Internal packages.
[
"^~(/.*|$)"
],
// Relative imports
[
"^\\.\\.(?!/?$)",
"^\\.\\./?$",
"^\\./(?=.*/)(?!/?$)",
"^\\.(?!/?$)",
"^\\./?$"
],
// Style imports.
[
"^.+\\.?(css|scss)$"
]
]
}
]
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
{ {
"tabWidth": 4, "tabWidth": 2,
"useTabs": true "useTabs": false
} }
+71 -69
View File
@@ -1,82 +1,84 @@
import { useState, useEffect } from "react"; import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import checkAuth from "../pages/api/auth/CheckAuth";
import Image from "next/image"; import Image from "next/image";
import { publicPaths } from "../const"; import { useRouter } from "next/router";
import { publicPaths } from "~/const";
import checkAuth from "~/pages/api/auth/CheckAuth";
// #TODO: finish spinner only when the data loads fully // #TODO: finish spinner only when the data loads fully
// #TODO: Redirect somewhere if the page does not exist // #TODO: Redirect somewhere if the page does not exist
export default function RouteGuard({ children }) { export default function RouteGuard({ children }) {
const router = useRouter(); const router = useRouter();
const [authorized, setAuthorized] = useState(false); const [authorized, setAuthorized] = useState(false);
useEffect(async () => { useEffect(() => {
// on initial load - run auth check // on initial load - run auth check
await authCheck(router.asPath); (async () => {
await authCheck(router.asPath);
})();
// on route change start - hide page content by setting authorized to false // on route change start - hide page content by setting authorized to false
// #TODO: add the loading page when not yet authorized. // #TODO: add the loading page when not yet authorized.
const hideContent = () => setAuthorized(false); const hideContent = () => setAuthorized(false);
// const onError = () => setAuthorized(true) // const onError = () => setAuthorized(true)
router.events.on("routeChangeStart", hideContent); router.events.on("routeChangeStart", hideContent);
// router.events.on("routeChangeError", onError); // router.events.on("routeChangeError", onError);
// on route change complete - run auth check // on route change complete - run auth check
router.events.on("routeChangeComplete", authCheck); router.events.on("routeChangeComplete", authCheck);
// unsubscribe from events in useEffect return function // unsubscribe from events in useEffect return function
return () => { return () => {
router.events.off("routeChangeStart", hideContent); router.events.off("routeChangeStart", hideContent);
router.events.off("routeChangeComplete", authCheck); router.events.off("routeChangeComplete", authCheck);
// router.events.off("routeChangeError", onError); // router.events.off("routeChangeError", onError);
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps /**
}, []); * redirect to login page if accessing a private page and not logged in
* @param {*} url - the url of the page we are trying to go to
*/
async function authCheck(url) {
// Make sure that we don't redirect when the user is on the following pages.
const path = "/" + url.split("?")[0].split("/")[1];
/** // Check if the user is authenticated
* redirect to login page if accessing a private page and not logged in const response = await checkAuth();
* @param {*} url - the url of the page we are trying to go to // #TODO: figure our why sometimes it doesn't output a response
*/ if (!publicPaths.includes(path)) {
async function authCheck(url) { try {
// Make sure that we don't redirect when the user is on the following pages. if (response.status !== 200) {
const path = "/" + url.split("?")[0].split("/")[1]; router.push("/login");
console.log("Unauthorized to access.");
setAuthorized(false);
} else {
setAuthorized(true);
console.log("Authorized to access.");
}
} catch (error) {
console.log(
"Error (probably the authCheck route is stuck again...):",
error
);
}
}
}
// Check if the user is authenticated if (authorized) {
const response = await checkAuth(); return children;
// #TODO: figure our why sometimes it doesn't output a response } else {
if (!publicPaths.includes(path)) { return (
try { <div className="w-screen h-screen bg-bunker-800 flex items-center justify-center">
if (response.status !== 200) { <Image
router.push("/login"); src="/images/loading/loading.gif"
console.log("Unauthorized to access."); height={70}
setAuthorized(false); width={120}
} else { alt="google logo"
setAuthorized(true); ></Image>
console.log("Authorized to access."); </div>
} );
} catch (error) { }
console.log( }
"Error (probably the authCheck route is stuck again...):",
error
);
}
}
}
if (authorized) {
return children;
} else {
return (
<div className="w-screen h-screen bg-bunker-800 flex items-center justify-center">
<Image
src="/images/loading/loading.gif"
height={70}
width={120}
alt="google logo"
></Image>
</div>
);
}
}
-63
View File
@@ -1,63 +0,0 @@
/**
* @fileoverview Provides easy encryption/decryption methods using AES 256 GCM.
*/
"use strict";
const crypto = require("crypto");
const ALGORITHM = "aes-256-gcm";
const BLOCK_SIZE_BYTES = 16; // 128 bit
/**
* Provides easy encryption/decryption methods using AES 256 GCM.
*/
class Aes256Gcm {
/**
* No need to run the constructor. The class only has static methods.
*/
constructor() {}
/**
* Encrypts text with AES 256 GCM.
* @param {string} text - Cleartext to encode.
* @param {string} secret - Shared secret key, must be 32 bytes.
* @returns {object}
*/
static encrypt(text, secret) {
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
let ciphertext = cipher.update(text, "utf8", "base64");
ciphertext += cipher.final("base64");
return {
ciphertext,
iv: iv.toString("base64"),
tag: cipher.getAuthTag().toString("base64"),
};
}
/**
* Decrypts AES 256 CGM encrypted text.
* @param {string} ciphertext - Base64-encoded ciphertext.
* @param {string} iv - The base64-encoded initialization vector.
* @param {string} tag - The base64-encoded authentication tag generated by getAuthTag().
* @param {string} secret - Shared secret key, must be 32 bytes.
* @returns {string}
*/
static decrypt(ciphertext, iv, tag, secret) {
const decipher = crypto.createDecipheriv(
ALGORITHM,
secret,
Buffer.from(iv, "base64")
);
decipher.setAuthTag(Buffer.from(tag, "base64"));
let cleartext = decipher.update(ciphertext, "base64", "utf8");
cleartext += decipher.final("utf8");
return cleartext;
}
}
module.exports = Aes256Gcm;
+15 -9
View File
@@ -1,14 +1,20 @@
import posthog from "posthog-js"; import posthog from "posthog-js";
import { ENV, POSTHOG_API_KEY, POSTHOG_HOST, TELEMETRY_ENABLED } from "../utilities/config";
import {
ENV,
POSTHOG_API_KEY,
POSTHOG_HOST,
TELEMETRY_ENABLED,
} from "../utilities/config";
export const initPostHog = () => { export const initPostHog = () => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
if (ENV == "production" && TELEMETRY_ENABLED) { if (ENV == "production" && TELEMETRY_ENABLED) {
posthog.init(POSTHOG_API_KEY, { posthog.init(POSTHOG_API_KEY, {
api_host: POSTHOG_HOST, api_host: POSTHOG_HOST,
}); });
} }
} }
return posthog; return posthog;
}; };
+12 -14
View File
@@ -1,19 +1,17 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons"; import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export default function Error({ text }) { export default function Error({ text }) {
return ( return (
<div className="relative bg-red-500 opacity-100 border flex flex-row justify-center m-auto items-center w-fit rounded-full mb-4"> <div className="relative bg-red-500 opacity-100 border flex flex-row justify-center m-auto items-center w-fit rounded-full mb-4">
<FontAwesomeIcon <FontAwesomeIcon
icon={faExclamationTriangle} icon={faExclamationTriangle}
className="text-white mt-1.5 mb-2 mx-2" className="text-white mt-1.5 mb-2 mx-2"
/> />
{text && ( {text && (
<p className="relative top-0 text-white mr-2 text-sm py-1"> <p className="relative top-0 text-white mr-2 text-sm py-1">{text}</p>
{text} )}
</p> </div>
)} );
</div>
);
} }
+101 -102
View File
@@ -1,48 +1,49 @@
import React from "react"; import React from "react";
import Error from "./Error";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faCircle,
faCircleExclamation,
faE,
faEye,
faEyeSlash,
} from "@fortawesome/free-solid-svg-icons";
import guidGenerator from "../utilities/randomId";
import { useState } from "react"; import { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import {
faCircle,
faCircleExclamation,
faE,
faEye,
faEyeSlash,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import guidGenerator from "../utilities/randomId";
import Error from "./Error";
const InputField = (props) => { const InputField = (props) => {
const [passwordVisible, setPasswordVisible] = useState(false); const [passwordVisible, setPasswordVisible] = useState(false);
const router = useRouter(); const router = useRouter();
if (props.static === true) { if (props.static === true) {
return ( return (
<div className="flex flex-col my-2 md:my-4 justify-center w-full max-w-md"> <div className="flex flex-col my-2 md:my-4 justify-center w-full max-w-md">
<p className="text-sm font-semibold text-gray-400 mb-0.5"> <p className="text-sm font-semibold text-gray-400 mb-0.5">
{props.label} {props.label}
</p> </p>
{props.text && ( {props.text && (
<p className="text-xs text-gray-400 mb-2">{props.text}</p> <p className="text-xs text-gray-400 mb-2">{props.text}</p>
)} )}
<input <input
onChange={(e) => props.onChangeHandler(e.target.value)} onChange={(e) => props.onChangeHandler(e.target.value)}
type={props.type} type={props.type}
placeholder={props.placeholder} placeholder={props.placeholder}
value={props.value} value={props.value}
required={props.isRequired} required={props.isRequired}
className="bg-bunker-800 text-gray-400 border border-gray-600 rounded-md text-md p-2 w-full min-w-16 outline-none" className="bg-bunker-800 text-gray-400 border border-gray-600 rounded-md text-md p-2 w-full min-w-16 outline-none"
name={props.name} name={props.name}
readOnly readOnly
/> />
</div> </div>
); );
} else { } else {
return ( return (
<div className="flex-col w-full"> <div className="flex-col w-full">
<div className="flex flex-row text-mineshaft-300 items-center mb-0.5"> <div className="flex flex-row text-mineshaft-300 items-center mb-0.5">
<p className="text-sm font-semibold mr-1">{props.label}</p> <p className="text-sm font-semibold mr-1">{props.label}</p>
{/* {props.label == "Password" && router.asPath != "/login" && ( {/* {props.label == "Password" && router.asPath != "/login" && (
<div className="mb-0.5 relative inline-block text-gray-400 underline hover:text-primary duration-200"> <div className="mb-0.5 relative inline-block text-gray-400 underline hover:text-primary duration-200">
<FontAwesomeIcon <FontAwesomeIcon
icon={faCircleExclamation} icon={faCircleExclamation}
@@ -58,73 +59,71 @@ const InputField = (props) => {
</span> </span>
</div> </div>
)} */} )} */}
</div> </div>
<div <div
className={`group relative flex flex-col justify-center w-full max-w-2xl border ${ className={`group relative flex flex-col justify-center w-full max-w-2xl border ${
props.error ? "border-red" : "border-mineshaft-500" props.error ? "border-red" : "border-mineshaft-500"
} rounded-md`} } rounded-md`}
> >
<input <input
onChange={(e) => props.onChangeHandler(e.target.value)} onChange={(e) => props.onChangeHandler(e.target.value)}
type={passwordVisible == false ? props.type : "text"} type={passwordVisible == false ? props.type : "text"}
placeholder={props.placeholder} placeholder={props.placeholder}
value={props.value} value={props.value}
required={props.isRequired} required={props.isRequired}
className={`${ className={`${
props.blurred props.blurred
? "text-bunker-800 group-hover:text-gray-400 focus:text-gray-400 active:text-gray-400" ? "text-bunker-800 group-hover:text-gray-400 focus:text-gray-400 active:text-gray-400"
: "" : ""
} ${ } ${
props.error props.error ? "focus:ring-red/50" : "focus:ring-primary/50"
? "focus:ring-red/50" } relative peer bg-bunker-800 rounded-md text-gray-400 text-md p-2 w-full min-w-16 outline-none focus:ring-4 duration-200`}
: "focus:ring-primary/50" name={props.name}
} relative peer bg-bunker-800 rounded-md text-gray-400 text-md p-2 w-full min-w-16 outline-none focus:ring-4 duration-200`} spellCheck="false"
name={props.name} />
spellCheck="false" {props.label?.includes("Password") && (
/> <button
{props.label?.includes("Password") && ( onClick={() => {
<button setPasswordVisible(!passwordVisible);
onClick={() => { }}
setPasswordVisible(!passwordVisible); className="absolute self-end mr-3 text-gray-400 cursor-pointer"
}} >
className="absolute self-end mr-3 text-gray-400 cursor-pointer" {passwordVisible ? (
> <FontAwesomeIcon icon={faEyeSlash} />
{passwordVisible ? ( ) : (
<FontAwesomeIcon icon={faEyeSlash} /> <FontAwesomeIcon icon={faEye} />
) : ( )}
<FontAwesomeIcon icon={faEye} /> </button>
)} )}
</button> {props.blurred && (
)} <div className="peer group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible absolute h-10 w-fit max-w-xl rounded-md flex items-center text-gray-400/50 text-clip overflow-hidden">
{props.blurred && ( <p className="ml-2"></p>
<div className="peer group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible absolute h-10 w-fit max-w-xl rounded-md flex items-center text-gray-400/50 text-clip overflow-hidden"> {props.value
<p className="ml-2"></p> .split("")
{props.value .slice(0, 54)
.split("") .map(() => (
.slice(0, 54) <FontAwesomeIcon
.map(() => ( key={guidGenerator()}
<FontAwesomeIcon className="text-xxs mx-0.5"
key={guidGenerator()} icon={faCircle}
className="text-xxs mx-0.5" />
icon={faCircle} ))}
/> </div>
))} )}
</div> {/* {props.error && (
)}
{/* {props.error && (
<div className="absolute z-20 flex items-end justify-end mt-4 mr-1.5 self-end"> <div className="absolute z-20 flex items-end justify-end mt-4 mr-1.5 self-end">
<Error /> <Error />
</div> </div>
)} */} )} */}
</div> </div>
{props.error && ( {props.error && (
<p className="text-red text-xs mt-0.5 mx-0 mb-2 max-w-xs"> <p className="text-red text-xs mt-0.5 mx-0 mb-2 max-w-xs">
{props.errorText} {props.errorText}
</p> </p>
)} )}
</div> </div>
); );
} }
}; };
export default React.memo(InputField); export default React.memo(InputField);
+98 -95
View File
@@ -1,9 +1,13 @@
import React from "react"; import React from "react";
import { Listbox, Transition } from "@headlessui/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheck, faPlus, faAngleDown } from "@fortawesome/free-solid-svg-icons";
import { Fragment } from "react"; import { Fragment } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import {
faAngleDown,
faCheck,
faPlus,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Listbox, Transition } from "@headlessui/react";
/** /**
* This is the component that we use for drop down lists. * This is the component that we use for drop down lists.
@@ -11,98 +15,97 @@ import { useRouter } from "next/router";
* @returns * @returns
*/ */
export default function ListBox({ export default function ListBox({
selected, selected,
onChange, onChange,
data, data,
text, text,
buttonAction, buttonAction,
width, width,
workspaceMapping = [], workspaceMapping = [],
}) { }) {
const router = useRouter(); const router = useRouter();
return ( return (
<Listbox value={selected} onChange={onChange}> <Listbox value={selected} onChange={onChange}>
<div className="relative"> <div className="relative">
<Listbox.Button <Listbox.Button
className={`text-gray-400 relative ${ className={`text-gray-400 relative ${
width == "full" ? "w-full" : "w-52" width == "full" ? "w-full" : "w-52"
} cursor-default rounded-md bg-white/[0.07] hover:bg-white/[0.11] duration-200 py-2.5 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm`} } cursor-default rounded-md bg-white/[0.07] hover:bg-white/[0.11] duration-200 py-2.5 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm`}
> >
<div className="flex flex-row"> <div className="flex flex-row">
{text} {text}
<span className="ml-1 cursor-pointer block truncate font-semibold text-gray-300"> <span className="ml-1 cursor-pointer block truncate font-semibold text-gray-300">
{" "} {" "}
{selected} {selected}
</span> </span>
</div> </div>
{data && ( {data && (
<div className="cursor-pointer pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> <div className="cursor-pointer pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<FontAwesomeIcon icon={faAngleDown} className="text-md mr-1.5"/> <FontAwesomeIcon icon={faAngleDown} className="text-md mr-1.5" />
</div> </div>
)} )}
</Listbox.Button> </Listbox.Button>
{data && ( {data && (
<Transition <Transition
as={Fragment} as={Fragment}
leave="transition ease-in duration-100" leave="transition ease-in duration-100"
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<Listbox.Options className="border border-mineshaft-700 z-50 p-2 absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"> <Listbox.Options className="border border-mineshaft-700 z-50 p-2 absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{data.map((person, personIdx) => ( {data.map((person, personIdx) => (
<Listbox.Option <Listbox.Option
key={personIdx} key={personIdx}
className={({ active, selected }) => className={({ active, selected }) =>
`my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md ${ `my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md ${
selected selected ? "bg-white/10 text-gray-400 font-bold" : ""
? "bg-white/10 text-gray-400 font-bold" } ${
: "" active & !selected
} ${ ? "bg-white/5 text-mineshaft-200 cursor-pointer"
active & !selected : "text-gray-400"
? "bg-white/5 text-mineshaft-200 cursor-pointer" } `
: "text-gray-400" }
} ` value={person}
} >
value={person} {({ selected }) => (
> <>
{({ selected }) => ( <span
<> className={`block truncate text-primary${
<span selected ? "font-medium" : "font-normal"
className={`block truncate text-primary${ }`}
selected >
? "font-medium" {person}
: "font-normal" </span>
}`} {selected ? (
> <span className="text-primary rounded-lg absolute inset-y-0 left-0 flex items-center pl-3">
{person} <FontAwesomeIcon
</span> icon={faCheck}
{selected ? ( className="text-md ml-1"
<span className="text-primary rounded-lg absolute inset-y-0 left-0 flex items-center pl-3"> />
<FontAwesomeIcon icon={faCheck} className="text-md ml-1"/> </span>
</span> ) : null}
) : null} </>
</> )}
)} </Listbox.Option>
</Listbox.Option> ))}
))} {buttonAction && (
{buttonAction && ( <button
<button onClick={buttonAction}
onClick={buttonAction} className="cursor-pointer w-full"
className="cursor-pointer w-full" >
> <div className="my-0.5 relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-lime-300 duration-200 hover:text-black hover:font-semibold mt-2">
<div className="my-0.5 relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-lime-300 duration-200 hover:text-black hover:font-semibold mt-2"> <span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4">
<span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4"> <FontAwesomeIcon icon={faPlus} className="text-lg" />
<FontAwesomeIcon icon={faPlus} className="text-lg"/> </span>
</span> Add Project
Add Project </div>
</div> </button>
</button> )}
)} </Listbox.Options>
</Listbox.Options> </Transition>
</Transition> )}
)} </div>
</div> </Listbox>
</Listbox> );
);
} }
-111
View File
@@ -1,111 +0,0 @@
import React from "react";
import Link from "next/link";
import Image from "next/image";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
var classNames = require("classnames");
export default function Button({
text,
onButtonPressed,
link,
loading,
color,
size,
icon,
active = true,
iconDisabled,
textDisabled
}) {
let styleButton = classNames(
"group m-auto md:m-0 inline-block rounded-md duration-200",
// Setting background colors and hover modes
color == "mineshaft" && active && "bg-mineshaft-700 hover:bg-primary",
color == "mineshaft" && !active && "bg-mineshaft",
(color == "primary" || !color) && active && "bg-primary hover:opacity-80",
(color == "primary" || !color) && !active && "bg-primary",
color == "red" && "bg-red",
// Changing the opacity when active vs when not
active ? "opacity-100 cursor-pointer" : "opacity-40",
// Setting the button sizes
size == "md" && "h-10 w-full px-2 md:px-4",
size == "lg" && "h-12 w-full px-2 md:px-8",
!size && "md:py-1 px-3 md:px-8",
size == "icon-md" && "h-10 w-10 flex items-center justify-center",
size == "icon-sm" && "h-9 w-9 flex items-center justify-center",
);
let styleMainDiv = classNames(
"relative font-medium flex items-center",
// Setting the text color for the text and icon
color == "mineshaft" && "text-gray-400",
color != "mineshaft" && color != "red" && "text-black",
color == "red" && "text-gray-200",
active && color != "red" ? "group-hover:text-black" : "",
size == "icon" && "flex items-center justify-center",
);
let textStyle = classNames(
"relative duration-200",
// Show the loading sign if the loading indicator is on
loading == true ? "opacity-0" : "opacity-100",
size == "md" && "text-sm",
size == "lg" && "text-lg"
);
const button = (
<button
disabled={!active}
onClick={onButtonPressed}
className={styleButton}
>
<div className={styleMainDiv}>
<div
className={`${
loading == true ? "opacity-100" : "opacity-0"
} absolute flex items-center px-2 duration-200`}
>
<Image
src="/images/loading/loadingblack.gif"
height={25}
width={42}
alt="loading animation"
className={`rounded-xl`}
></Image>
</div>
{icon &&
<FontAwesomeIcon
icon={icon}
className={`flex my-auto font-extrabold ${size == "icon-sm" ? "text-sm" : "text-md"} ${(text || textDisabled) && "mr-2"}`}
/>
}
{iconDisabled &&
<FontAwesomeIcon
icon={iconDisabled}
className={`flex my-auto font-extrabold ${size == "icon-sm" ? "text-sm" : "text-md"} ${(text || textDisabled) && "mr-2"}`}
/>
}
{(text || textDisabled) &&
<p
className={textStyle}
>
{active ? text : textDisabled}
</p>
}
</div>
</button>
);
if (link) {
return <Link href={link}>{button}</Link>;
}
return button;
}
@@ -0,0 +1,111 @@
import React from "react";
import Image from "next/image";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
var classNames = require("classnames");
type ButtonProps = {
text: string;
onButtonPressed: () => void;
loading: boolean;
color: string;
size: string;
icon: IconProp;
active: boolean;
iconDisabled: string;
textDisabled: string;
}
export default function Button (props: ButtonProps): JSX.Element {
// Check if the button show always be 'active' - then true;
// or if it should switch between 'active' and 'disabled' - then give the status
const activityStatus = props.active || (props.text != "" && props.textDisabled == undefined)
let styleButton = classNames(
"group m-auto md:m-0 inline-block rounded-md duration-200",
// Setting background colors and hover modes
props.color == "mineshaft" && activityStatus && "bg-mineshaft-700 hover:bg-primary",
props.color == "mineshaft" && !activityStatus && "bg-mineshaft",
(props.color == "primary" || !props.color) && activityStatus && "bg-primary hover:opacity-80",
(props.color == "primary" || !props.color) && !activityStatus && "bg-primary",
props.color == "red" && "bg-red",
// Changing the opacity when active vs when not
activityStatus ? "opacity-100 cursor-pointer" : "opacity-40",
// Setting the button sizes
props.size == "md" && "h-10 w-full px-2 md:px-4",
props.size == "lg" && "h-12 w-full px-2 md:px-8",
!props.size && "md:py-1 px-3 md:px-8",
props.size == "icon-md" && "h-10 w-10 flex items-center justify-center",
props.size == "icon-sm" && "h-9 w-9 flex items-center justify-center"
);
let styleMainDiv = classNames(
"relative font-medium flex items-center",
// Setting the text color for the text and icon
props.color == "mineshaft" && "text-gray-400",
props.color != "mineshaft" && props.color != "red" && "text-black",
props.color == "red" && "text-gray-200",
activityStatus && props.color != "red" ? "group-hover:text-black" : "",
props.size == "icon" && "flex items-center justify-center"
);
let textStyle = classNames(
"relative duration-200",
// Show the loading sign if the loading indicator is on
props.loading == true ? "opacity-0" : "opacity-100",
props.size == "md" && "text-sm",
props.size == "lg" && "text-lg"
);
const button = (
<button
disabled={!activityStatus}
onClick={props.onButtonPressed}
className={styleButton}
>
<div className={styleMainDiv}>
<div
className={`${
props.loading == true ? "opacity-100" : "opacity-0"
} absolute flex items-center px-2 duration-200`}
>
<Image
src="/images/loading/loadingblack.gif"
height={25}
width={42}
alt="loading animation"
className={`rounded-xl`}
></Image>
</div>
{props.icon && (
<FontAwesomeIcon
icon={props.icon as IconProp}
className={`flex my-auto font-extrabold ${
props.size == "icon-sm" ? "text-sm" : "text-md"
} ${(props.text || props.textDisabled) && "mr-2"}`}
/>
)}
{props.iconDisabled && (
<FontAwesomeIcon
icon={props.iconDisabled as IconProp}
className={`flex my-auto font-extrabold ${
props.size == "icon-sm" ? "text-sm" : "text-md"
} ${(props.text || props.textDisabled) && "mr-2"}`}
/>
)}
{(props.text || props.textDisabled) && (
<p className={textStyle}>{activityStatus ? props.text : props.textDisabled}</p>
)}
</div>
</button>
);
return button;
}
@@ -1,95 +1,98 @@
import { Dialog, Transition } from "@headlessui/react";
import { Fragment, useState } from "react"; import { Fragment, useState } from "react";
import InputField from "../InputField"; import { Dialog, Transition } from "@headlessui/react";
import addIncidentContact from "../../../pages/api/organization/addIncidentContact";
import addIncidentContact from "~/pages/api/organization/addIncidentContact";
import Button from "../buttons/Button"; import Button from "../buttons/Button";
import InputField from "../InputField";
const AddIncidentContactDialog = ({ const AddIncidentContactDialog = ({
isOpen, isOpen,
closeModal, closeModal,
workspaceId, workspaceId,
incidentContacts, incidentContacts,
setIncidentContacts, setIncidentContacts,
}) => { }) => {
let [incidentContactEmail, setIncidentContactEmail] = useState(""); let [incidentContactEmail, setIncidentContactEmail] = useState("");
const submit = () => { const submit = () => {
setIncidentContacts( setIncidentContacts(
incidentContacts?.length > 0 incidentContacts?.length > 0
? incidentContacts.concat([incidentContactEmail]) ? incidentContacts.concat([incidentContactEmail])
: [incidentContactEmail] : [incidentContactEmail]
); );
addIncidentContact(localStorage.getItem("orgData.id"), incidentContactEmail); addIncidentContact(
closeModal(); localStorage.getItem("orgData.id"),
}; incidentContactEmail
return ( );
<div> closeModal();
<Transition appear show={isOpen} as={Fragment}> };
<Dialog as="div" className="relative z-10" onClose={closeModal}> return (
<Transition.Child <div>
as={Fragment} <Transition appear show={isOpen} as={Fragment}>
enter="ease-out duration-300" <Dialog as="div" className="relative z-10" onClose={closeModal}>
enterFrom="opacity-0" <Transition.Child
enterTo="opacity-100" as={Fragment}
leave="ease-in duration-200" enter="ease-out duration-300"
leaveFrom="opacity-100" enterFrom="opacity-0"
leaveTo="opacity-0" enterTo="opacity-100"
> leave="ease-in duration-200"
<div className="fixed inset-0 bg-black bg-opacity-70" /> leaveFrom="opacity-100"
</Transition.Child> leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-70" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto"> <div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center"> <div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
enterFrom="opacity-0 scale-95" enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100" enterTo="opacity-100 scale-100"
leave="ease-in duration-200" leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all"> <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title <Dialog.Title
as="h3" as="h3"
className="text-lg font-medium leading-6 text-gray-400" className="text-lg font-medium leading-6 text-gray-400"
> >
Add an Incident Contact Add an Incident Contact
</Dialog.Title> </Dialog.Title>
<div className="mt-2 mb-2"> <div className="mt-2 mb-2">
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
This contact will be notified in the This contact will be notified in the unlikely event of a
unlikely event of a severe incident. severe incident.
</p> </p>
</div> </div>
<div className="max-h-28"> <div className="max-h-28">
<InputField <InputField
label="Email" label="Email"
onChangeHandler={ onChangeHandler={setIncidentContactEmail}
setIncidentContactEmail type="varName"
} value={incidentContactEmail}
type="varName" placeholder=""
value={incidentContactEmail} isRequired
placeholder="" />
isRequired </div>
/> <div className="mt-6 max-w-max">
</div> <Button
<div className="mt-6 max-w-max"> onButtonPressed={submit}
<Button color="mineshaft"
onButtonPressed={submit} text="Add Incident Contact"
color="mineshaft" size="md"
text="Add Incident Contact" />
size="md" </div>
/> </Dialog.Panel>
</div> </Transition.Child>
</Dialog.Panel> </div>
</Transition.Child> </div>
</div> </Dialog>
</div> </Transition>
</Dialog> </div>
</Transition> );
</div>
);
}; };
export default AddIncidentContactDialog; export default AddIncidentContactDialog;
@@ -1,171 +1,150 @@
import { Dialog, Transition } from "@headlessui/react";
import { Fragment, useState } from "react"; import { Fragment, useState } from "react";
import ListBox from "../Listbox";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Button from "../buttons/Button";
import useTranslation from "next-translate/useTranslation";
import Trans from "next-translate/Trans"; import Trans from "next-translate/Trans";
import useTranslation from "next-translate/useTranslation";
import { Dialog, Transition } from "@headlessui/react";
import Button from "../buttons/Button";
import ListBox from "../Listbox";
const AddProjectMemberDialog = ({ const AddProjectMemberDialog = ({
isOpen, isOpen,
closeModal, closeModal,
submitModal, submitModal,
data, data,
email, email,
workspaceId, workspaceId,
setEmail, setEmail,
}) => { }) => {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="z-50"> <div className="z-50">
<Transition appear show={isOpen} as={Fragment}> <Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative" onClose={closeModal}> <Dialog as="div" className="relative" onClose={closeModal}>
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
enterFrom="opacity-0" enterFrom="opacity-0"
enterTo="opacity-100" enterTo="opacity-100"
leave="ease-in duration-200" leave="ease-in duration-200"
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-black bg-opacity-70" /> <div className="fixed inset-0 bg-black bg-opacity-70" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 overflow-y-auto"> <div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center"> <div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
enterFrom="opacity-0 scale-95" enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100" enterTo="opacity-100 scale-100"
leave="ease-in duration-200" leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all"> <Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
{data?.length > 0 ? ( {data?.length > 0 ? (
<Dialog.Title <Dialog.Title
as="h3" as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50" className="text-lg font-medium leading-6 text-gray-400 z-50"
> >
{t( {t("settings:add-member-dialog.add-member-to-project")}
"settings:add-member-dialog.add-member-to-project" </Dialog.Title>
)} ) : (
</Dialog.Title> <Dialog.Title
) : ( as="h3"
<Dialog.Title className="text-lg font-medium leading-6 text-gray-400 z-50"
as="h3" >
className="text-lg font-medium leading-6 text-gray-400 z-50" {t("settings:add-member-dialog.already-all-invited")}
> </Dialog.Title>
{t( )}
"settings:add-member-dialog.already-all-invited" <div className="mt-2 mb-4">
)} {data?.length > 0 ? (
</Dialog.Title> <div className="flex flex-col">
)} <p className="text-sm text-gray-500">
<div className="mt-2 mb-4"> {t("settings:add-member-dialog.user-will-email")}
{data?.length > 0 ? ( </p>
<div className="flex flex-col"> <div className="">
<p className="text-sm text-gray-500"> <Trans
{t( i18nKey="settings:add-member-dialog.looking-add"
"settings:add-member-dialog.user-will-email" components={[
)} // eslint-disable-next-line react/jsx-key
</p> <button
<div className=""> type="button"
<Trans className="inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
i18nKey="settings:add-member-dialog.looking-add" onClick={() =>
components={[ router.push(
<button "/settings/org/" + router.query.id
type="button" )
className="inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2" }
onClick={() => />,
router.push( // eslint-disable-next-line react/jsx-key
"/settings/org/" + <button
router type="button"
.query className="ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
.id onClick={() =>
) router.push(
} "/settings/org/" +
/>, router.query.id +
<button "?invite"
type="button" )
className="ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2" }
onClick={() => >
router.push( click here.
"/settings/org/" + </button>,
router ]}
.query />
.id + </div>
"?invite" </div>
) ) : (
} <p className="text-sm text-gray-500">
> {t("settings:add-member-dialog.add-user-org-first")}
click here. </p>
</button>, )}
]} </div>
/> <div className="max-h-28">
</div> {data?.length > 0 && (
</div> <ListBox
) : ( selected={email ? email : data[0]}
<p className="text-sm text-gray-500"> onChange={setEmail}
{t( data={data}
"settings:add-member-dialog.add-user-org-first" width="full"
)} />
</p> )}
)} </div>
</div> <div className="max-w-max">
<div className="max-h-28"> {data?.length > 0 ? (
{data?.length > 0 && ( <div className="mt-6 flex flex-col justify-start w-max">
<ListBox <Button
selected={ onButtonPressed={submitModal}
email ? email : data[0] color="mineshaft"
} text={t("settings:add-member-dialog.add-member")}
onChange={setEmail} size="md"
data={data} />
width="full" </div>
/> ) : (
)} <Button
</div> onButtonPressed={() =>
<div className="max-w-max"> router.push("/settings/org/" + router.query.id)
{data?.length > 0 ? ( }
<div className="mt-6 flex flex-col justify-start w-max"> color="mineshaft"
<Button text={t("settings:add-member-dialog.add-user-to-org")}
onButtonPressed={ size="md"
submitModal />
} )}
color="mineshaft" </div>
text={t( </Dialog.Panel>
"settings:add-member-dialog.add-member" </Transition.Child>
)} </div>
size="md" </div>
/> </Dialog>
</div> </Transition>
) : ( </div>
<Button );
onButtonPressed={() =>
router.push(
"/settings/org/" +
router.query.id
)
}
color="mineshaft"
text={t(
"settings:add-member-dialog.add-user-to-org"
)}
size="md"
/>
)}
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</div>
);
}; };
export default AddProjectMemberDialog; export default AddProjectMemberDialog;
@@ -1,231 +1,261 @@
import { Dialog, Transition } from "@headlessui/react";
import { Fragment, useState } from "react"; import { Fragment, useState } from "react";
import ListBox from "../Listbox";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Button from "../buttons/Button";
import InputField from "../InputField";
import getLatestFileKey from "../../../pages/api/workspace/getLatestFileKey";
import { decryptAssymmetric, encryptAssymmetric } from "../../utilities/crypto";
import addServiceToken from "../../../pages/api/serviceToken/addServiceToken";
import nacl from "tweetnacl";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Dialog, Transition } from "@headlessui/react";
import nacl from "tweetnacl";
import addServiceToken from "~/pages/api/serviceToken/addServiceToken";
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
import {
decryptAssymmetric,
encryptAssymmetric,
} from "../../utilities/cryptography/crypto";
import Button from "../buttons/Button";
import InputField from "../InputField";
import ListBox from "../Listbox";
const envMapping = { const envMapping = {
Development: "dev", Development: "dev",
Staging: "staging", Staging: "staging",
Production: "prod", Production: "prod",
Testing: "test", Testing: "test",
}; };
const expiryMapping = { const expiryMapping = {
"1 day": 86400, "1 day": 86400,
"7 days": 604800, "7 days": 604800,
"1 month": 2592000, "1 month": 2592000,
}; };
const AddServiceTokenDialog = ({ const AddServiceTokenDialog = ({
isOpen, isOpen,
closeModal, closeModal,
workspaceId, workspaceId,
workspaceName workspaceName,
}) => { }) => {
const router = useRouter(); const router = useRouter();
const [serviceToken, setServiceToken] = useState(""); const [serviceToken, setServiceToken] = useState("");
const [serviceTokenName, setServiceTokenName] = useState(""); const [serviceTokenName, setServiceTokenName] = useState("");
const [serviceTokenEnv, setServiceTokenEnv] = useState("Development"); const [serviceTokenEnv, setServiceTokenEnv] = useState("Development");
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day"); const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day");
const [serviceTokenCopied, setServiceTokenCopied] = useState(false); const [serviceTokenCopied, setServiceTokenCopied] = useState(false);
const generateServiceToken = async () => { const generateServiceToken = async () => {
const latestFileKey = await getLatestFileKey(workspaceId); const latestFileKey = await getLatestFileKey(workspaceId);
const key = decryptAssymmetric({ const key = decryptAssymmetric({
ciphertext: latestFileKey.latestKey.encryptedKey, ciphertext: latestFileKey.latestKey.encryptedKey,
nonce: latestFileKey.latestKey.nonce, nonce: latestFileKey.latestKey.nonce,
publicKey: latestFileKey.latestKey.sender.publicKey, publicKey: latestFileKey.latestKey.sender.publicKey,
privateKey: localStorage.getItem("PRIVATE_KEY") privateKey: localStorage.getItem("PRIVATE_KEY"),
}); });
// generate new public/private key pair // generate new public/private key pair
const pair = nacl.box.keyPair(); const pair = nacl.box.keyPair();
const publicKey = nacl.util.encodeBase64(pair.publicKey); const publicKey = nacl.util.encodeBase64(pair.publicKey);
const privateKey = nacl.util.encodeBase64(pair.secretKey); const privateKey = nacl.util.encodeBase64(pair.secretKey);
// encrypt workspace key under newly-generated public key
const { ciphertext: encryptedKey, nonce } = encryptAssymmetric({
plaintext: key,
publicKey,
privateKey
});
let newServiceToken = await addServiceToken({ // encrypt workspace key under newly-generated public key
name: serviceTokenName, const { ciphertext: encryptedKey, nonce } = encryptAssymmetric({
workspaceId, plaintext: key,
environment: envMapping[serviceTokenEnv], publicKey,
expiresIn: expiryMapping[serviceTokenExpiresIn], privateKey,
publicKey, });
encryptedKey,
nonce
})
const serviceToken = newServiceToken + ',' + privateKey;
setServiceToken(serviceToken);
}
function copyToClipboard() { let newServiceToken = await addServiceToken({
// Get the text field name: serviceTokenName,
var copyText = document.getElementById("serviceToken"); workspaceId,
environment: envMapping[serviceTokenEnv],
// Select the text field expiresIn: expiryMapping[serviceTokenExpiresIn],
copyText.select(); publicKey,
copyText.setSelectionRange(0, 99999); // For mobile devices encryptedKey,
nonce,
// Copy the text inside the text field });
navigator.clipboard.writeText(copyText.value);
setServiceTokenCopied(true);
setTimeout(() => setServiceTokenCopied(false), 2000);
// Alert the copied text
// alert("Copied the text: " + copyText.value);
}
const closeAddServiceTokenModal = () => { const serviceToken = newServiceToken + "," + privateKey;
closeModal(); setServiceToken(serviceToken);
setServiceTokenName(""); };
setServiceToken("");
}
return ( function copyToClipboard() {
<div className="z-50"> // Get the text field
<Transition appear show={isOpen} as={Fragment}> var copyText = document.getElementById("serviceToken");
<Dialog as="div" className="relative" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-bunker-700 bg-opacity-80" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto"> // Select the text field
<div className="flex min-h-full items-center justify-center p-4 text-center"> copyText.select();
<Transition.Child copyText.setSelectionRange(0, 99999); // For mobile devices
as={Fragment}
enter="ease-out duration-300" // Copy the text inside the text field
enterFrom="opacity-0 scale-95" navigator.clipboard.writeText(copyText.value);
enterTo="opacity-100 scale-100"
leave="ease-in duration-200" setServiceTokenCopied(true);
leaveFrom="opacity-100 scale-100" setTimeout(() => setServiceTokenCopied(false), 2000);
leaveTo="opacity-0 scale-95" // Alert the copied text
> // alert("Copied the text: " + copyText.value);
{serviceToken == "" }
? <Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title const closeAddServiceTokenModal = () => {
as="h3" closeModal();
className="text-lg font-medium leading-6 text-gray-400 z-50" setServiceTokenName("");
> setServiceToken("");
Add a service token for {workspaceName} };
</Dialog.Title>
<div className="mt-2 mb-4"> return (
<div className="flex flex-col"> <div className="z-50">
<p className="text-sm text-gray-500"> <Transition appear show={isOpen} as={Fragment}>
Specify the name, environment, and expiry period. When a token is generated, you will only be able to see it once before it disappears. Make sure to save it somewhere. <Dialog as="div" className="relative" onClose={closeModal}>
</p> <Transition.Child
</div> as={Fragment}
</div> enter="ease-out duration-300"
<div className="max-h-28 mb-2"> enterFrom="opacity-0"
<InputField enterTo="opacity-100"
label="Service Token Name" leave="ease-in duration-200"
onChangeHandler={ leaveFrom="opacity-100"
setServiceTokenName leaveTo="opacity-0"
} >
type="varName" <div className="fixed inset-0 bg-bunker-700 bg-opacity-80" />
value={serviceTokenName} </Transition.Child>
placeholder=""
isRequired <div className="fixed inset-0 overflow-y-auto">
/> <div className="flex min-h-full items-center justify-center p-4 text-center">
</div> <Transition.Child
<div className="max-h-28 mb-2"> as={Fragment}
<ListBox enter="ease-out duration-300"
selected={serviceTokenEnv} enterFrom="opacity-0 scale-95"
onChange={setServiceTokenEnv} enterTo="opacity-100 scale-100"
data={["Development", "Staging", "Production", "Testing"]} leave="ease-in duration-200"
width="full" leaveFrom="opacity-100 scale-100"
text="Environment: " leaveTo="opacity-0 scale-95"
/> >
</div> {serviceToken == "" ? (
<div className="max-h-28"> <Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<ListBox <Dialog.Title
selected={serviceTokenExpiresIn} as="h3"
onChange={setServiceTokenExpiresIn} className="text-lg font-medium leading-6 text-gray-400 z-50"
data={["1 day", "7 days", "1 month"]} >
width="full" Add a service token for {workspaceName}
text="Expires in: " </Dialog.Title>
/> <div className="mt-2 mb-4">
</div> <div className="flex flex-col">
<div className="max-w-max"> <p className="text-sm text-gray-500">
<div className="mt-6 flex flex-col justify-start w-max"> Specify the name, environment, and expiry period. When
<Button a token is generated, you will only be able to see it
onButtonPressed={() => generateServiceToken()} once before it disappears. Make sure to save it
color="mineshaft" somewhere.
text="Add Service Token" </p>
textDisabled="Add Service Token" </div>
size="md" </div>
active={serviceTokenName == "" ? false : true} <div className="max-h-28 mb-2">
/> <InputField
</div> label="Service Token Name"
</div> onChangeHandler={setServiceTokenName}
</Dialog.Panel> type="varName"
: <Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all"> value={serviceTokenName}
<Dialog.Title placeholder=""
as="h3" isRequired
className="text-lg font-medium leading-6 text-gray-400 z-50" />
> </div>
Copy your service token <div className="max-h-28 mb-2">
</Dialog.Title> <ListBox
<div className="mt-2 mb-4"> selected={serviceTokenEnv}
<div className="flex flex-col"> onChange={setServiceTokenEnv}
<p className="text-sm text-gray-500"> data={[
Once you close this popup, you will never see your service token again "Development",
</p> "Staging",
</div> "Production",
</div> "Testing",
<div className="w-full"> ]}
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-36"> width="full"
<input type="text" value={serviceToken} id="serviceToken" className="invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none"></input> text="Environment: "
<div className="bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none">{serviceToken}</div> />
<div className="group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200"> </div>
<button onClick={copyToClipboard} className="h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200"> <div className="max-h-28">
{serviceTokenCopied ? <FontAwesomeIcon icon={faCheck} className="pr-0.5"/> : <FontAwesomeIcon icon={faCopy} />} <ListBox
</button> selected={serviceTokenExpiresIn}
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm"> onChange={setServiceTokenExpiresIn}
Click to Copy data={["1 day", "7 days", "1 month"]}
</span> width="full"
</div> text="Expires in: "
</div> />
</div> </div>
<div className="mt-6 flex flex-col justify-start w-max"> <div className="max-w-max">
<Button <div className="mt-6 flex flex-col justify-start w-max">
onButtonPressed={() => closeAddServiceTokenModal()} <Button
color="mineshaft" onButtonPressed={() => generateServiceToken()}
text="Close" color="mineshaft"
size="md" text="Add Service Token"
/> textDisabled="Add Service Token"
</div> size="md"
</Dialog.Panel> active={serviceTokenName == "" ? false : true}
} />
</Transition.Child> </div>
</div> </div>
</div> </Dialog.Panel>
</Dialog> ) : (
</Transition> <Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
</div> <Dialog.Title
); as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50"
>
Copy your service token
</Dialog.Title>
<div className="mt-2 mb-4">
<div className="flex flex-col">
<p className="text-sm text-gray-500">
Once you close this popup, you will never see your
service token again
</p>
</div>
</div>
<div className="w-full">
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-36">
<input
type="text"
value={serviceToken}
id="serviceToken"
className="invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none"
></input>
<div className="bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none">
{serviceToken}
</div>
<div className="group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200">
<button
onClick={copyToClipboard}
className="h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200"
>
{serviceTokenCopied ? (
<FontAwesomeIcon
icon={faCheck}
className="pr-0.5"
/>
) : (
<FontAwesomeIcon icon={faCopy} />
)}
</button>
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm">
Click to Copy
</span>
</div>
</div>
</div>
<div className="mt-6 flex flex-col justify-start w-max">
<Button
onButtonPressed={() => closeAddServiceTokenModal()}
color="mineshaft"
text="Close"
size="md"
/>
</div>
</Dialog.Panel>
)}
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</div>
);
}; };
export default AddServiceTokenDialog; export default AddServiceTokenDialog;
+105 -108
View File
@@ -1,112 +1,109 @@
import { Dialog, Transition } from "@headlessui/react";
import { Fragment } from "react"; import { Fragment } from "react";
import InputField from "../InputField";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Button from "../buttons/Button"; import { Dialog, Transition } from "@headlessui/react";
import { STRIPE_PRODUCT_STARTER } from "../../utilities/config"; import { STRIPE_PRODUCT_STARTER } from "../../utilities/config";
import Button from "../buttons/Button";
import InputField from "../InputField";
const AddUserDialog = ({ const AddUserDialog = ({
isOpen, isOpen,
closeModal, closeModal,
submitModal, submitModal,
email, email,
workspaceId, workspaceId,
setEmail, setEmail,
currentPlan, currentPlan,
orgName orgName,
}) => { }) => {
const submit = () => { const submit = () => {
submitModal(email); submitModal(email);
}; };
const router = useRouter(); const router = useRouter();
return ( return (
<div className="z-50"> <div className="z-50">
<Transition appear show={isOpen} as={Fragment}> <Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative" onClose={closeModal}> <Dialog as="div" className="relative" onClose={closeModal}>
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
enterFrom="opacity-0" enterFrom="opacity-0"
enterTo="opacity-100" enterTo="opacity-100"
leave="ease-in duration-200" leave="ease-in duration-200"
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-black bg-opacity-70" /> <div className="fixed inset-0 bg-black bg-opacity-70" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 overflow-y-auto"> <div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center"> <div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
enterFrom="opacity-0 scale-95" enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100" enterTo="opacity-100 scale-100"
leave="ease-in duration-200" leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className="w-full max-w-lg transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all"> <Dialog.Panel className="w-full max-w-lg transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title <Dialog.Title
as="h3" as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50" className="text-lg font-medium leading-6 text-gray-400 z-50"
> >
Invite others to {orgName} Invite others to {orgName}
</Dialog.Title> </Dialog.Title>
<div className="mt-2 mb-4"> <div className="mt-2 mb-4">
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
An invite is specific to an email address An invite is specific to an email address and expires
and expires after 1 day. For security reasons, after 1 day. For security reasons, you will need to
you will need to separately add members to projects. separately add members to projects.
</p> </p>
</div> </div>
<div className="max-h-28"> <div className="max-h-28">
<InputField <InputField
label="Email" label="Email"
onChangeHandler={setEmail} onChangeHandler={setEmail}
type="varName" type="varName"
value={email} value={email}
placeholder="" placeholder=""
isRequired isRequired
/> />
</div> </div>
{currentPlan == STRIPE_PRODUCT_STARTER && <div className="flex flex-row"> {currentPlan == STRIPE_PRODUCT_STARTER && (
<button <div className="flex flex-row">
type="button" <button
className="inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2" type="button"
onClick={() => className="inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
router.push( onClick={() =>
"/settings/billing/" + router.push("/settings/billing/" + router.query.id)
router.query.id }
) >
} You can add up to 5 members on a Free tier.
> </button>
You can add up to 5 members on a Free tier. <button
</button> type="button"
<button className="ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
type="button" onClick={() =>
className="ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2" router.push("/settings/billing/" + router.query.id)
onClick={() => }
router.push( >
"/settings/billing/" + Upgrade now.
router.query.id </button>
) </div>
} )}
> <div className="mt-4 max-w-max">
Upgrade now. <Button
</button> onButtonPressed={submit}
</div>} color="mineshaft"
<div className="mt-4 max-w-max"> text="Invite"
<Button size="md"
onButtonPressed={submit} />
color="mineshaft" </div>
text="Invite" </Dialog.Panel>
size="md" {/* <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
/>
</div>
</Dialog.Panel>
{/* <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title <Dialog.Title
as="h3" as="h3"
className="text-xl font-medium leading-6 text-gray-300 z-50" className="text-xl font-medium leading-6 text-gray-300 z-50"
@@ -138,13 +135,13 @@ const AddUserDialog = ({
</button> </button>
</div> </div>
</Dialog.Panel> */} </Dialog.Panel> */}
</Transition.Child> </Transition.Child>
</div> </div>
</div> </div>
</Dialog> </Dialog>
</Transition> </Transition>
</div> </div>
); );
}; };
export default AddUserDialog; export default AddUserDialog;
@@ -1,9 +1,10 @@
import { Dialog, Transition } from "@headlessui/react";
import { Fragment, useState } from "react"; import { Fragment, useState } from "react";
import InputField from "../InputField";
import Image from "next/image"; import Image from "next/image";
import { Checkbox } from "../table/Checkbox"; import { Dialog, Transition } from "@headlessui/react";
import Button from "../buttons/Button"; import Button from "../buttons/Button";
import InputField from "../InputField";
import { Checkbox } from "../table/Checkbox";
/** /**
* The dialog modal for when the user wants to create a new workspace * The dialog modal for when the user wants to create a new workspace
@@ -11,94 +12,93 @@ import Button from "../buttons/Button";
* @returns * @returns
*/ */
const AddWorkspaceDialog = ({ const AddWorkspaceDialog = ({
isOpen, isOpen,
closeModal, closeModal,
submitModal, submitModal,
workspaceName, workspaceName,
setWorkspaceName, setWorkspaceName,
error, error,
loading, loading,
}) => { }) => {
const [addAllUsers, setAddAllUsers] = useState(true); const [addAllUsers, setAddAllUsers] = useState(true);
const submit = () => { const submit = () => {
submitModal(workspaceName, addAllUsers); submitModal(workspaceName, addAllUsers);
}; };
return ( return (
<div> <div>
<Transition appear show={isOpen} as={Fragment}> <Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={closeModal}> <Dialog as="div" className="relative z-20" onClose={closeModal}>
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
enterFrom="opacity-0" enterFrom="opacity-0"
enterTo="opacity-100" enterTo="opacity-100"
leave="ease-in duration-200" leave="ease-in duration-200"
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-black bg-opacity-70" /> <div className="fixed inset-0 bg-black bg-opacity-70" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 overflow-y-auto z-50"> <div className="fixed inset-0 overflow-y-auto z-50">
<div className="flex min-h-full items-center justify-center p-4 text-center"> <div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
enterFrom="opacity-0 scale-95" enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100" enterTo="opacity-100 scale-100"
leave="ease-in duration-200" leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all"> <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title <Dialog.Title
as="h3" as="h3"
className="text-lg font-medium leading-6 text-gray-400" className="text-lg font-medium leading-6 text-gray-400"
> >
Create a new project Create a new project
</Dialog.Title> </Dialog.Title>
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
This project will contain your This project will contain your environmental variables.
environmental variables. </p>
</p> </div>
</div> <div className="max-h-28 mt-4">
<div className="max-h-28 mt-4"> <InputField
<InputField label="Project Name"
label="Project Name" onChangeHandler={setWorkspaceName}
onChangeHandler={setWorkspaceName} type="varName"
type="varName" value={workspaceName}
value={workspaceName} placeholder=""
placeholder="" isRequired
isRequired error={error.length > 0}
error={error.length > 0} errorText={error}
errorText={error} />
/> </div>
</div> <div className="mt-4 ml-1">
<div className="mt-4 ml-1"> <Checkbox
<Checkbox addAllUsers={addAllUsers}
addAllUsers={addAllUsers} setAddAllUsers={setAddAllUsers}
setAddAllUsers={setAddAllUsers} />
/> </div>
</div> <div className="mt-4 max-w-min">
<div className="mt-4 max-w-min"> <Button
<Button onButtonPressed={submit}
onButtonPressed={submit} loading={loading}
loading={loading} color="mineshaft"
color="mineshaft" text="Create"
text="Create" size="md"
size="md" />
/> </div>
</div> </Dialog.Panel>
</Dialog.Panel> </Transition.Child>
</Transition.Child> </div>
</div> </div>
</div> </Dialog>
</Dialog> </Transition>
</Transition> </div>
</div> );
);
}; };
export default AddWorkspaceDialog; export default AddWorkspaceDialog;
@@ -1,77 +1,83 @@
import { Dialog, Transition } from '@headlessui/react' import { Fragment, useState } from "react";
import { Fragment, useState } from 'react' import { Dialog, Transition } from "@headlessui/react";
import InputField from '../InputField';
import InputField from "../InputField";
// #TODO: USE THIS. Currently it's not. Kinda complicated to set up because of state. // #TODO: USE THIS. Currently it's not. Kinda complicated to set up because of state.
const DeleteUserDialog = ({
isOpen,
closeModal,
submitModal,
userIdToBeDeleted,
}) => {
const submit = () => {
submitModal(userIdToBeDeleted);
};
return (
<div>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
const DeleteUserDialog = ({isOpen, closeModal, submitModal, userIdToBeDeleted}) => { <div className="fixed inset-0 overflow-y-auto">
const submit = () => { <div className="flex min-h-full items-center justify-center p-4 text-center">
submitModal(userIdToBeDeleted); <Transition.Child
}
return (
<div>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={closeModal}>
<Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
enterFrom="opacity-0" enterFrom="opacity-0 scale-95"
enterTo="opacity-100" enterTo="opacity-100 scale-100"
leave="ease-in duration-200" leave="ease-in duration-200"
leaveFrom="opacity-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0" leaveTo="opacity-0 scale-95"
> >
<div className="fixed inset-0 bg-black bg-opacity-25" /> <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
</Transition.Child> <Dialog.Title
as="h3"
<div className="fixed inset-0 overflow-y-auto"> className="text-lg font-medium leading-6 text-gray-400"
<div className="flex min-h-full items-center justify-center p-4 text-center"> >
<Transition.Child Are you sure you want to remove this user from the
as={Fragment} workspace?
enter="ease-out duration-300" </Dialog.Title>
enterFrom="opacity-0 scale-95" <div className="mt-2">
enterTo="opacity-100 scale-100" <p className="text-sm text-gray-500">
leave="ease-in duration-200" This action is irrevertible.
leaveFrom="opacity-100 scale-100" </p>
leaveTo="opacity-0 scale-95" </div>
> <div className="mt-6">
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all"> <button
<Dialog.Title type="button"
as="h3" className="inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-alizarin hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
className="text-lg font-medium leading-6 text-gray-400" onClick={submit}
> >
Are you sure you want to remove this user from the workspace? Delete
</Dialog.Title> </button>
<div className="mt-2"> <button
<p className="text-sm text-gray-500"> type="button"
This action is irrevertible. className="ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:border-white hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
</p> onClick={submit}
</div> >
<div className="mt-6"> Cancel
<button </button>
type="button" </div>
className="inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-alizarin hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2" </Dialog.Panel>
onClick={submit} </Transition.Child>
>
Delete
</button>
<button
type="button"
className="ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:border-white hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={submit}
>
Cancel
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div> </div>
</Dialog> </div>
</Transition> </Dialog>
</div> </Transition>
); </div>
);
}; };
export default DeleteUserDialog; export default DeleteUserDialog;
+282 -316
View File
@@ -1,339 +1,305 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import useTranslation from "next-translate/useTranslation";
import {
faGear,
faHouse,
faLink,
faMobile,
faUser,
} from "@fortawesome/free-solid-svg-icons";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import getOrganizations from "~/pages/api/organization/getOrgs";
import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProjects";
import getOrganizationUsers from "~/pages/api/organization/GetOrgUsers";
import addUserToWorkspace from "~/pages/api/workspace/addUserToWorkspace";
import createWorkspace from "~/pages/api/workspace/createWorkspace";
import getWorkspaces from "~/pages/api/workspace/getWorkspaces";
import NavBarDashboard from "../navigation/NavBarDashboard"; import NavBarDashboard from "../navigation/NavBarDashboard";
import Listbox from "./Listbox";
import getWorkspaces from "../../pages/api/workspace/getWorkspaces";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
faHouse, decryptAssymmetric,
faUser, encryptAssymmetric,
faGear, } from "../utilities/cryptography/crypto";
faMobile,
faLink,
} from "@fortawesome/free-solid-svg-icons";
import AddWorkspaceDialog from "./dialog/AddWorkspaceDialog";
import createWorkspace from "../../pages/api/workspace/createWorkspace";
import getOrganizationUserProjects from "../../pages/api/organization/GetOrgUserProjects";
import getOrganizationUsers from "../../pages/api/organization/GetOrgUsers";
import addUserToWorkspace from "../../pages/api/workspace/addUserToWorkspace";
import getOrganizations from "../../pages/api/organization/getOrgs";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { decryptAssymmetric, encryptAssymmetric } from "../utilities/crypto";
import Button from "./buttons/Button"; import Button from "./buttons/Button";
import AddWorkspaceDialog from "./dialog/AddWorkspaceDialog";
import useTranslation from "next-translate/useTranslation"; import Listbox from "./Listbox";
export default function Layout({ children }) { export default function Layout({ children }) {
const router = useRouter(); const router = useRouter();
const [workspaceList, setWorkspaceList] = useState([]); const [workspaceList, setWorkspaceList] = useState([]);
const [workspaceMapping, setWorkspaceMapping] = useState([{ 1: 2 }]); const [workspaceMapping, setWorkspaceMapping] = useState([{ 1: 2 }]);
const [workspaceSelected, setWorkspaceSelected] = useState("∞"); const [workspaceSelected, setWorkspaceSelected] = useState("∞");
let [newWorkspaceName, setNewWorkspaceName] = useState(""); let [newWorkspaceName, setNewWorkspaceName] = useState("");
let [isOpen, setIsOpen] = useState(false); let [isOpen, setIsOpen] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(false); const [error, setError] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
function closeModal() { function closeModal() {
setIsOpen(false); setIsOpen(false);
} }
// TODO: what to do about the fact that 2ids can have the same name // TODO: what to do about the fact that 2ids can have the same name
/** /**
* When a user creates a new workspace, redirect them to the page of the new workspace. * When a user creates a new workspace, redirect them to the page of the new workspace.
* @param {*} workspaceName * @param {*} workspaceName
*/ */
async function submitModal(workspaceName, addAllUsers) { async function submitModal(workspaceName, addAllUsers) {
setLoading(true); setLoading(true);
setTimeout(() => setLoading(false), 1500); setTimeout(() => setLoading(false), 1500);
const workspaces = await getWorkspaces(); const workspaces = await getWorkspaces();
const currentWorkspaces = workspaces.map((workspace) => workspace.name); const currentWorkspaces = workspaces.map((workspace) => workspace.name);
if (!currentWorkspaces.includes(workspaceName)) { if (!currentWorkspaces.includes(workspaceName)) {
const newWorkspace = await createWorkspace( const newWorkspace = await createWorkspace(
workspaceName, workspaceName,
localStorage.getItem("orgData.id") localStorage.getItem("orgData.id")
); );
let newWorkspaceId; let newWorkspaceId;
try { try {
newWorkspaceId = newWorkspace._id; newWorkspaceId = newWorkspace._id;
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
if (addAllUsers) { if (addAllUsers) {
let orgUsers = await getOrganizationUsers({ let orgUsers = await getOrganizationUsers({
orgId: localStorage.getItem("orgData.id"), orgId: localStorage.getItem("orgData.id"),
}); });
orgUsers.map(async (user) => { orgUsers.map(async (user) => {
if (user.status == "accepted") { if (user.status == "accepted") {
let result = await addUserToWorkspace( let result = await addUserToWorkspace(
user.user.email, user.user.email,
newWorkspaceId newWorkspaceId
); );
if (result?.invitee && result?.latestKey) { if (result?.invitee && result?.latestKey) {
const PRIVATE_KEY = const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
localStorage.getItem("PRIVATE_KEY");
// assymmetrically decrypt symmetric key with local private key // assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({ const key = decryptAssymmetric({
ciphertext: result.latestKey.encryptedKey, ciphertext: result.latestKey.encryptedKey,
nonce: result.latestKey.nonce, nonce: result.latestKey.nonce,
publicKey: result.latestKey.sender.publicKey, publicKey: result.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY, privateKey: PRIVATE_KEY,
}); });
const { ciphertext, nonce } = encryptAssymmetric({ const { ciphertext, nonce } = encryptAssymmetric({
plaintext: key, plaintext: key,
publicKey: result.invitee.publicKey, publicKey: result.invitee.publicKey,
privateKey: PRIVATE_KEY, privateKey: PRIVATE_KEY,
}); });
uploadKeys( uploadKeys(newWorkspaceId, result.invitee._id, ciphertext, nonce);
newWorkspaceId, }
result.invitee._id, }
ciphertext, });
nonce }
); router.push("/dashboard/" + newWorkspaceId + "?Development");
} setIsOpen(false);
} setNewWorkspaceName("");
}); } else {
} setError(t("error_project-already-exists"));
router.push("/dashboard/" + newWorkspaceId + "?Development"); setLoading(false);
setIsOpen(false); }
setNewWorkspaceName(""); }
} else {
setError(t("error_project-already-exists"));
setLoading(false);
}
}
function openModal() { function openModal() {
setIsOpen(true); setIsOpen(true);
} }
const menuItems = [ const menuItems = [
{ {
href: href:
"/dashboard/" + "/dashboard/" + workspaceMapping[workspaceSelected] + "?Development",
workspaceMapping[workspaceSelected] + title: t("nav:menu.secrets"),
"?Development", emoji: <FontAwesomeIcon icon={faHouse} />,
title: t("nav:menu.secrets"), },
emoji: <FontAwesomeIcon icon={faHouse} />, {
}, href: "/users/" + workspaceMapping[workspaceSelected],
{ title: t("nav:menu.members"),
href: "/users/" + workspaceMapping[workspaceSelected], emoji: <FontAwesomeIcon icon={faUser} />,
title: t("nav:menu.members"), },
emoji: <FontAwesomeIcon icon={faUser} />, {
}, href: "/integrations/" + workspaceMapping[workspaceSelected],
{ title: t("nav:menu.integrations"),
href: "/integrations/" + workspaceMapping[workspaceSelected], emoji: <FontAwesomeIcon icon={faLink} />,
title: t("nav:menu.integrations"), },
emoji: <FontAwesomeIcon icon={faLink} />, {
}, href: "/settings/project/" + workspaceMapping[workspaceSelected],
{ title: t("nav:menu.project-settings"),
href: "/settings/project/" + workspaceMapping[workspaceSelected], emoji: <FontAwesomeIcon icon={faGear} />,
title: t("nav:menu.project-settings"), },
emoji: <FontAwesomeIcon icon={faGear} />, ];
},
];
useEffect(async () => { useEffect(async () => {
// Put a user in a workspace if they're not in one yet // Put a user in a workspace if they're not in one yet
if ( if (
localStorage.getItem("orgData.id") == null || localStorage.getItem("orgData.id") == null ||
localStorage.getItem("orgData.id") == "" localStorage.getItem("orgData.id") == ""
) { ) {
const userOrgs = await getOrganizations(); const userOrgs = await getOrganizations();
localStorage.setItem("orgData.id", userOrgs[0]._id); localStorage.setItem("orgData.id", userOrgs[0]._id);
} }
let orgUserProjects = await getOrganizationUserProjects({ let orgUserProjects = await getOrganizationUserProjects({
orgId: localStorage.getItem("orgData.id"), orgId: localStorage.getItem("orgData.id"),
}); });
let userWorkspaces = orgUserProjects; let userWorkspaces = orgUserProjects;
if ( if (
userWorkspaces.length == 0 && userWorkspaces.length == 0 &&
router.asPath != "/noprojects" && router.asPath != "/noprojects" &&
!router.asPath.includes("settings") !router.asPath.includes("settings")
) { ) {
router.push("/noprojects"); router.push("/noprojects");
} else if (router.asPath != "/noprojects") { } else if (router.asPath != "/noprojects") {
const intendedWorkspaceId = router.asPath const intendedWorkspaceId = router.asPath
.split("/") .split("/")
[router.asPath.split("/").length - 1].split("?")[0]; [router.asPath.split("/").length - 1].split("?")[0];
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs // If a user is not a member of a workspace they are trying to access, just push them to one of theirs
if ( if (
intendedWorkspaceId != "heroku" && intendedWorkspaceId != "heroku" &&
!userWorkspaces !userWorkspaces
.map((workspace) => workspace._id) .map((workspace) => workspace._id)
.includes(intendedWorkspaceId) .includes(intendedWorkspaceId)
) { ) {
router.push( router.push("/dashboard/" + userWorkspaces[0]._id + "?Development");
"/dashboard/" + userWorkspaces[0]._id + "?Development" } else {
); setWorkspaceList(userWorkspaces.map((workspace) => workspace.name));
} else { setWorkspaceMapping(
setWorkspaceList( Object.fromEntries(
userWorkspaces.map((workspace) => workspace.name) userWorkspaces.map((workspace) => [workspace.name, workspace._id])
); )
setWorkspaceMapping( );
Object.fromEntries( setWorkspaceSelected(
userWorkspaces.map((workspace) => [ Object.fromEntries(
workspace.name, userWorkspaces.map((workspace) => [workspace._id, workspace.name])
workspace._id, )[
]) router.asPath
) .split("/")
); [router.asPath.split("/").length - 1].split("?")[0]
setWorkspaceSelected( ]
Object.fromEntries( );
userWorkspaces.map((workspace) => [ }
workspace._id, }
workspace.name, }, []);
])
)[
router.asPath
.split("/")
[router.asPath.split("/").length - 1].split("?")[0]
]
);
}
}
}, []);
useEffect(() => { useEffect(() => {
try { try {
if ( if (
workspaceMapping[workspaceSelected] && workspaceMapping[workspaceSelected] &&
workspaceMapping[workspaceSelected] !== workspaceMapping[workspaceSelected] !==
router.asPath router.asPath
.split("/") .split("/")
[router.asPath.split("/").length - 1].split("?")[0] [router.asPath.split("/").length - 1].split("?")[0]
) { ) {
router.push( router.push(
"/dashboard/" + "/dashboard/" + workspaceMapping[workspaceSelected] + "?Development"
workspaceMapping[workspaceSelected] + );
"?Development" localStorage.setItem(
); "projectData.id",
localStorage.setItem( workspaceMapping[workspaceSelected]
"projectData.id", );
workspaceMapping[workspaceSelected] }
); } catch (error) {
} console.log(error);
} catch (error) { }
console.log(error); }, [workspaceSelected]);
}
}, [workspaceSelected]);
return ( return (
<> <>
<div className="fixed w-full hidden md:block flex flex-col h-screen"> <div className="fixed w-full hidden md:block flex flex-col h-screen">
<NavBarDashboard /> <NavBarDashboard />
<div className="flex flex-col md:flex-row flex-1"> <div className="flex flex-col md:flex-row flex-1">
<aside className="bg-bunker-600 border-r border-mineshaft-500 w-full md:w-60 h-screen"> <aside className="bg-bunker-600 border-r border-mineshaft-500 w-full md:w-60 h-screen">
<nav> <nav>
<div className="py-6"></div> <div className="py-6"></div>
<div className="flex justify-center w-full mt-7 mb-8 bg-bunker-600 w-full h-full flex flex-col items-center px-4"> <div className="flex justify-center w-full mt-7 mb-8 bg-bunker-600 w-full h-full flex flex-col items-center px-4">
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide"> <div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
{t("nav:menu.project")} {t("nav:menu.project")}
</div> </div>
{workspaceList.length > 0 ? ( {workspaceList.length > 0 ? (
<Listbox <Listbox
selected={workspaceSelected} selected={workspaceSelected}
onChange={setWorkspaceSelected} onChange={setWorkspaceSelected}
data={workspaceList} data={workspaceList}
buttonAction={openModal} buttonAction={openModal}
text="" text=""
workspaceMapping={workspaceMapping} workspaceMapping={workspaceMapping}
/> />
) : ( ) : (
<Button <Button
text="Add Project" text="Add Project"
onButtonPressed={openModal} onButtonPressed={openModal}
color="mineshaft" color="mineshaft"
size="md" size="md"
icon={faPlus} icon={faPlus}
/> />
)} )}
</div> </div>
<ul> <ul>
{workspaceList.length > 0 && {workspaceList.length > 0 &&
menuItems.map(({ href, title, emoji }) => ( menuItems.map(({ href, title, emoji }) => (
<li className="mt-1.5 mx-2" key={title}> <li className="mt-1.5 mx-2" key={title}>
{router.asPath.split("/")[1] === {router.asPath.split("/")[1] === href.split("/")[1] &&
href.split("/")[1] && (["project", "billing", "org", "personal"].includes(
([ router.asPath.split("/")[2]
"project", )
"billing", ? router.asPath.split("/")[2] === href.split("/")[2]
"org", : true) ? (
"personal", <div
].includes( className={`flex p-2 text-white text-sm rounded cursor-pointer bg-mineshaft-50/10`}
router.asPath.split("/")[2] >
) <div className="bg-primary w-1 rounded-xl mr-1"></div>
? router.asPath.split( <p className="ml-2 mr-4">{emoji}</p>
"/" {title}
)[2] === href.split("/")[2] </div>
: true) ? ( ) : router.asPath == "/noprojects" ? (
<div <div className={`flex p-2 text-white text-sm rounded`}>
className={`flex p-2 text-white text-sm rounded cursor-pointer bg-mineshaft-50/10`} <p className="ml-2 mr-4">{emoji}</p>
> {title}
<div className="bg-primary w-1 rounded-xl mr-1"></div> </div>
<p className="ml-2 mr-4"> ) : (
{emoji} <Link href={href}>
</p> <div
{title} className={`flex p-2 text-white text-sm rounded cursor-pointer hover:bg-mineshaft-50/5`}
</div> >
) : router.asPath == <p className="ml-2 mr-4">{emoji}</p>
"/noprojects" ? ( {title}
<div </div>
className={`flex p-2 text-white text-sm rounded`} </Link>
> )}
<p className="ml-2 mr-4"> </li>
{emoji} ))}
</p> </ul>
{title} </nav>
</div> </aside>
) : ( <AddWorkspaceDialog
<Link href={href}> isOpen={isOpen}
<div closeModal={closeModal}
className={`flex p-2 text-white text-sm rounded cursor-pointer hover:bg-mineshaft-50/5`} submitModal={submitModal}
> workspaceName={newWorkspaceName}
<p className="ml-2 mr-4"> setWorkspaceName={setNewWorkspaceName}
{emoji} error={error}
</p> loading={loading}
{title} />
</div> <main className="flex-1 bg-bunker-800">{children}</main>
</Link> </div>
)} </div>
</li> <div className="block md:hidden bg-bunker-800 w-screen h-screen flex flex-col justify-center items-center">
))} <FontAwesomeIcon
</ul> icon={faMobile}
</nav> className="text-gray-300 text-7xl mb-8"
</aside> />
<AddWorkspaceDialog <p className="text-gray-200 px-6 text-center text-lg max-w-sm">
isOpen={isOpen} {` ${t("common:no-mobile")} `}
closeModal={closeModal} </p>
submitModal={submitModal} </div>
workspaceName={newWorkspaceName} </>
setWorkspaceName={setNewWorkspaceName} );
error={error}
loading={loading}
/>
<main className="flex-1 bg-bunker-800">{children}</main>
</div>
</div>
<div className="block md:hidden bg-bunker-800 w-screen h-screen flex flex-col justify-center items-center">
<FontAwesomeIcon
icon={faMobile}
className="text-gray-300 text-7xl mb-8"
/>
<p className="text-gray-200 px-6 text-center text-lg max-w-sm">
{` ${t("need-login")} `}
</p>
</div>
</>
);
} }
@@ -1,41 +1,51 @@
import React from "react"; import React from "react";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faXmark,
} from "@fortawesome/free-solid-svg-icons";
export default function BottonRightPopup({ export default function BottonRightPopup({
buttonText, buttonText,
buttonLink, buttonLink,
titleText, titleText,
emoji, emoji,
textLine1, textLine1,
textLine2, textLine2,
setCheckDocsPopUpVisible setCheckDocsPopUpVisible,
}) { }) {
return ( return (
<div class="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-xl absolute bottom-0 right-0 mr-6 mb-6" role="alert"> <div
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6"> class="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-xl absolute bottom-0 right-0 mr-6 mb-6"
<div className="font-bold text-xl mr-2 mt-0.5 flex flex-row"> role="alert"
<div>{titleText}</div> >
<div class="ml-2.5">{emoji}</div> <div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">
</div> <div className="font-bold text-xl mr-2 mt-0.5 flex flex-row">
<button className="mt-1" onClick={() => setCheckDocsPopUpVisible(false)}> <div>{titleText}</div>
<FontAwesomeIcon icon={faXmark} className="text-gray-400 text-2xl hover:text-red duration-200 cursor-pointer" /> <div class="ml-2.5">{emoji}</div>
</button> </div>
</div> <button
<div class="block sm:inline px-6 mt-4 mb-0.5 text-gray-300">{textLine1}</div> className="mt-1"
<div class="block sm:inline mb-4 px-6">{textLine2}</div> onClick={() => setCheckDocsPopUpVisible(false)}
<div className="flex flex-row px-6 w-full"> >
<a <FontAwesomeIcon
class="font-bold p-2 bg-white/10 rounded-md w-full hover:bg-primary duration-200 hover:text-black flex justify-center" icon={faXmark}
href={buttonLink} className="text-gray-400 text-2xl hover:text-red duration-200 cursor-pointer"
target="_blank" />
rel="noopener" </button>
> </div>
{buttonText} <div class="block sm:inline px-6 mt-4 mb-0.5 text-gray-300">
</a> {textLine1}
</div> </div>
</div> <div class="block sm:inline mb-4 px-6">{textLine2}</div>
); <div className="flex flex-row px-6 w-full">
{/*eslint-disable-next-line react/jsx-no-target-blank */}
<a
class="font-bold p-2 bg-white/10 rounded-md w-full hover:bg-primary duration-200 hover:text-black flex justify-center"
href={buttonLink}
target="_blank"
rel="noopener"
>
{buttonText}
</a>
</div>
</div>
);
} }
+23 -23
View File
@@ -1,28 +1,28 @@
import React from "react"; import React from "react";
export const Checkbox = ({ addAllUsers, setAddAllUsers }) => { export const Checkbox = ({ addAllUsers, setAddAllUsers }) => {
return ( return (
<> <>
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
{addAllUsers == true ? ( {addAllUsers == true ? (
<input <input
type="checkbox" type="checkbox"
className="accent-primary h-4 w-4" className="accent-primary h-4 w-4"
checked checked
readOnly readOnly
onClick={() => setAddAllUsers(!addAllUsers)} onClick={() => setAddAllUsers(!addAllUsers)}
/> />
) : ( ) : (
<div <div
className="h-4 w-4 bg-bunker border border-gray-600 rounded-sm" className="h-4 w-4 bg-bunker border border-gray-600 rounded-sm"
onClick={() => setAddAllUsers(!addAllUsers)} onClick={() => setAddAllUsers(!addAllUsers)}
></div> ></div>
)} )}
<label className="ml-2 text-gray-500 text-sm"> <label className="ml-2 text-gray-500 text-sm">
Add all members of my organization to this project. Add all members of my organization to this project.
</label> </label>
</div> </div>
</> </>
); );
}; };
@@ -1,17 +1,18 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { faX } from "@fortawesome/free-solid-svg-icons"; import { faX } from "@fortawesome/free-solid-svg-icons";
import Button from "../buttons/Button";
import guidGenerator from "../../utilities/randomId"; import guidGenerator from "../../utilities/randomId";
import Button from "../buttons/Button";
const roles = ["admin", "user"]; const roles = ["admin", "user"];
const reverseEnvMapping = { const reverseEnvMapping = {
"dev": "Development", dev: "Development",
"staging": "Staging", staging: "Staging",
"prod": "Production", prod: "Production",
"test": "Testing" test: "Testing",
} };
/** /**
* This is the component that we utilize for the user table - in future, can reuse it for some other purposes too. * This is the component that we utilize for the user table - in future, can reuse it for some other purposes too.
@@ -19,63 +20,66 @@ const reverseEnvMapping = {
* @param {*} props * @param {*} props
* @returns * @returns
*/ */
const ServiceTokenTable = ({ const ServiceTokenTable = ({ data, workspaceName }) => {
data, const router = useRouter();
workspaceName
}) => {
const router = useRouter();
return ( return (
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1"> <div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
<div className="absolute rounded-t-md w-full h-12 bg-white/5"></div> <div className="absolute rounded-t-md w-full h-12 bg-white/5"></div>
<table className="w-full my-1"> <table className="w-full my-1">
<thead className="text-bunker-300"> <thead className="text-bunker-300">
<tr> <tr>
<th className="text-left pl-6 pt-2.5 pb-2">Token name</th> <th className="text-left pl-6 pt-2.5 pb-2">Token name</th>
<th className="text-left pl-6 pt-2.5 pb-2">Project</th> <th className="text-left pl-6 pt-2.5 pb-2">Project</th>
<th className="text-left pl-6 pt-2.5 pb-2">Environment</th> <th className="text-left pl-6 pt-2.5 pb-2">Environment</th>
<th className="text-left pl-6 pt-2.5 pb-2">Valid until</th> <th className="text-left pl-6 pt-2.5 pb-2">Valid until</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data?.length > 0 {data?.length > 0 ? (
? data data.map((row, index) => {
.map((row, index) => { return (
return ( <tr
<tr key={guidGenerator()} className="bg-bunker-800 hover:bg-bunker-800/5 duration-100"> key={guidGenerator()}
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> className="bg-bunker-800 hover:bg-bunker-800/5 duration-100"
{row.name} >
</td> <td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> {row.name}
{workspaceName} </td>
</td> <td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> {workspaceName}
{reverseEnvMapping[row.environment]} </td>
</td> <td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> {reverseEnvMapping[row.environment]}
{new Date(row.expiresAt).toUTCString()} </td>
</td> <td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
<td className="py-2 border-mineshaft-700 border-t"> {new Date(row.expiresAt).toUTCString()}
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center"> </td>
<Button <td className="py-2 border-mineshaft-700 border-t">
onButtonPressed={() => {}} <div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
color="red" <Button
size="icon-sm" onButtonPressed={() => {}}
icon={faX} color="red"
/> size="icon-sm"
</div> icon={faX}
</td> />
</tr> </div>
); </td>
}) </tr>
: <tr> );
<td colSpan="4" className="text-center pt-7 pb-4 text-bunker-400">No service tokens yet</td> })
</tr>} ) : (
</tbody> <tr>
</table> <td colSpan="4" className="text-center pt-7 pb-4 text-bunker-400">
</div> No service tokens yet
); </td>
</tr>
)}
</tbody>
</table>
</div>
);
}; };
export default ServiceTokenTable; export default ServiceTokenTable;
+209 -235
View File
@@ -1,19 +1,21 @@
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Listbox from "../Listbox";
import uploadKeys from "../../../pages/api/workspace/uploadKeys";
import getLatestFileKey from "../../../pages/api/workspace/getLatestFileKey";
import deleteUserFromWorkspace from "../../../pages/api/workspace/deleteUserFromWorkspace";
import changeUserRoleInWorkspace from "../../../pages/api/workspace/changeUserRoleInWorkspace";
import deleteUserFromOrganization from "../../../pages/api/organization/deleteUserFromOrganization";
import { faX } from "@fortawesome/free-solid-svg-icons"; import { faX } from "@fortawesome/free-solid-svg-icons";
import Button from "../buttons/Button";
import deleteUserFromOrganization from "~/pages/api/organization/deleteUserFromOrganization";
import changeUserRoleInWorkspace from "~/pages/api/workspace/changeUserRoleInWorkspace";
import deleteUserFromWorkspace from "~/pages/api/workspace/deleteUserFromWorkspace";
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
import uploadKeys from "~/pages/api/workspace/uploadKeys";
import guidGenerator from "../../utilities/randomId"; import guidGenerator from "../../utilities/randomId";
import Button from "../buttons/Button";
import Listbox from "../Listbox";
const { const {
decryptAssymmetric, decryptAssymmetric,
encryptAssymmetric, encryptAssymmetric,
} = require("../../utilities/crypto"); } = require("../../utilities/cryptography/crypto");
const nacl = require("tweetnacl"); const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util"); nacl.util = require("tweetnacl-util");
@@ -26,242 +28,214 @@ const roles = ["admin", "user"];
* @returns * @returns
*/ */
const UserTable = ({ const UserTable = ({
userData, userData,
changeData, changeData,
myUser, myUser,
filter, filter,
resendInvite, resendInvite,
isOrg, isOrg,
onClick, onClick,
deleteUser, deleteUser,
setUserIdToBeDeleted, setUserIdToBeDeleted,
}) => { }) => {
const [roleSelected, setRoleSelected] = useState( const [roleSelected, setRoleSelected] = useState(
Array(userData?.length).fill(userData.map((user) => user.role)) Array(userData?.length).fill(userData.map((user) => user.role))
); );
const router = useRouter(); const router = useRouter();
const [myRole, setMyRole] = useState("member"); const [myRole, setMyRole] = useState("member");
// Delete the row in the table (e.g. a user) // Delete the row in the table (e.g. a user)
// #TODO: Add a pop-up that warns you that the user is going to be deleted. // #TODO: Add a pop-up that warns you that the user is going to be deleted.
const handleDelete = (membershipId, index, e) => { const handleDelete = (membershipId, index, e) => {
// setUserIdToBeDeleted(userId); // setUserIdToBeDeleted(userId);
// onClick(); // onClick();
if (isOrg) { if (isOrg) {
deleteUserFromOrganization(membershipId); deleteUserFromOrganization(membershipId);
} else { } else {
deleteUserFromWorkspace(membershipId); deleteUserFromWorkspace(membershipId);
} }
changeData(userData.filter((v, i) => i !== index)); changeData(userData.filter((v, i) => i !== index));
setRoleSelected([ setRoleSelected([
...roleSelected.slice(0, index), ...roleSelected.slice(0, index),
...roleSelected.slice(index + 1, userData?.length), ...roleSelected.slice(index + 1, userData?.length),
]); ]);
}; };
// Update the rold of a certain user // Update the rold of a certain user
const handleRoleUpdate = (index, e) => { const handleRoleUpdate = (index, e) => {
changeUserRoleInWorkspace(userData[index].membershipId, e); changeUserRoleInWorkspace(userData[index].membershipId, e);
changeData([ changeData([
...userData.slice(0, index), ...userData.slice(0, index),
...[ ...[
{ {
key: userData[index].key, key: userData[index].key,
firstName: userData[index].firstName, firstName: userData[index].firstName,
lastName: userData[index].lastName, lastName: userData[index].lastName,
email: userData[index].email, email: userData[index].email,
role: e, role: e,
status: userData[index].status, status: userData[index].status,
userId: userData[index].userId, userId: userData[index].userId,
membershipId: userData[index].membershipId, membershipId: userData[index].membershipId,
publicKey: userData[index].publicKey, publicKey: userData[index].publicKey,
}, },
], ],
...userData.slice(index + 1, userData?.length), ...userData.slice(index + 1, userData?.length),
]); ]);
}; };
useEffect(async () => { useEffect(() => {
setMyRole(userData.filter((user) => user.email == myUser)[0]?.role); setMyRole(userData.filter((user) => user.email == myUser)[0]?.role);
}, [userData, myUser]); }, [userData, myUser]);
const grantAccess = async (id, publicKey) => { const grantAccess = async (id, publicKey) => {
let result = await getLatestFileKey(router.query.id); let result = await getLatestFileKey(router.query.id);
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
// assymmetrically decrypt symmetric key with local private key // assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({ const key = decryptAssymmetric({
ciphertext: result.latestKey.encryptedKey, ciphertext: result.latestKey.encryptedKey,
nonce: result.latestKey.nonce, nonce: result.latestKey.nonce,
publicKey: result.latestKey.sender.publicKey, publicKey: result.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY, privateKey: PRIVATE_KEY,
}); });
const { ciphertext, nonce } = encryptAssymmetric({ const { ciphertext, nonce } = encryptAssymmetric({
plaintext: key, plaintext: key,
publicKey: publicKey, publicKey: publicKey,
privateKey: PRIVATE_KEY, privateKey: PRIVATE_KEY,
}); });
uploadKeys(router.query.id, id, ciphertext, nonce); uploadKeys(router.query.id, id, ciphertext, nonce);
router.reload(); router.reload();
}; };
const deleteMembershipAndResendInvite = (email, membershipId) => { const deleteMembershipAndResendInvite = (email, membershipId) => {
// deleteUserFromWorkspace(membershipId); // deleteUserFromWorkspace(membershipId);
resendInvite(email); resendInvite(email);
}; };
return ( return (
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1"> <div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
<div className="absolute rounded-t-md w-full h-14 bg-white/5"></div> <div className="absolute rounded-t-md w-full h-14 bg-white/5"></div>
<table className="w-full my-1"> <table className="w-full my-1">
<thead className="text-gray-400"> <thead className="text-gray-400">
<tr> <tr>
<th className="text-left pl-6 py-3.5">First Name</th> <th className="text-left pl-6 py-3.5">First Name</th>
<th className="text-left pl-6 py-3.5">Last Name</th> <th className="text-left pl-6 py-3.5">Last Name</th>
<th className="text-left pl-6 py-3.5">Email</th> <th className="text-left pl-6 py-3.5">Email</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{userData?.filter( {userData?.filter(
(user) => (user) =>
user.firstName?.toLowerCase().includes(filter) || user.firstName?.toLowerCase().includes(filter) ||
user.lastName?.toLowerCase().includes(filter) || user.lastName?.toLowerCase().includes(filter) ||
user.email?.toLowerCase().includes(filter) user.email?.toLowerCase().includes(filter)
).length > 0 && ).length > 0 &&
userData userData
?.filter( ?.filter(
(user) => (user) =>
user.firstName user.firstName?.toLowerCase().includes(filter) ||
?.toLowerCase() user.lastName?.toLowerCase().includes(filter) ||
.includes(filter) || user.email?.toLowerCase().includes(filter)
user.lastName )
?.toLowerCase() .map((row, index) => {
.includes(filter) || return (
user.email?.toLowerCase().includes(filter) <tr
) key={guidGenerator()}
.map((row, index) => { className="bg-bunker-800 hover:bg-bunker-800/5"
return ( >
<tr key={guidGenerator()} className="bg-bunker-800 hover:bg-bunker-800/5"> <td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> {row.firstName}
{row.firstName} </td>
</td> <td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> {row.lastName}
{row.lastName} </td>
</td> <td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> {row.email}
{row.email} </td>
</td> <td className="flex flex-row justify-end pr-8 py-2 border-t border-0.5 border-mineshaft-700">
<td className="flex flex-row justify-end pr-8 py-2 border-t border-0.5 border-mineshaft-700"> <div className="flex justify-end mr-6 w-3/4 mx-2 w-full h-full flex flex-row items-center">
<div className="flex justify-end mr-6 w-3/4 mx-2 w-full h-full flex flex-row items-center"> {row.status == "granted" &&
{row.status == "granted" && ((myRole == "admin" && row.role != "owner") ||
((myRole == "admin" && myRole == "owner") &&
row.role != "owner") || myUser !== row.email ? (
myRole == "owner") && <Listbox
myUser !== row.email ? ( selected={row.role}
<Listbox onChange={(e) => handleRoleUpdate(index, e)}
selected={row.role} data={
onChange={(e) => myRole == "owner"
handleRoleUpdate( ? ["owner", "admin", "member"]
index, : ["admin", "member"]
e }
) text="Role: "
} membershipId={row.membershipId}
data={ />
myRole == "owner" ) : (
? [ row.status != "invited" &&
"owner", row.status != "verified" && (
"admin", <Listbox
"member", selected={row.role}
] text="Role: "
: [ membershipId={row.membershipId}
"admin", />
"member", )
] )}
} {(row.status == "invited" ||
text="Role: " row.status == "verified") && (
membershipId={ <div className="w-full pl-9">
row.membershipId <Button
} onButtonPressed={() =>
/> deleteMembershipAndResendInvite(
) : ( row.email,
row.status != "invited" && row.membershipId
row.status != )
"verified" && ( }
<Listbox color="mineshaft"
selected={row.role} text="Resend Invite"
text="Role: " size="md"
membershipId={ />
row.membershipId </div>
} )}
/> {row.status == "completed" && myUser !== row.email && (
) <div className="border border-mineshaft-700 rounded-md bg-white/5 hover:bg-primary text-white hover:text-black duration-200">
)} <Button
{(row.status == "invited" || onButtonPressed={() =>
row.status == grantAccess(row.userId, row.publicKey)
"verified") && ( }
<div className="w-full pl-12"> color="mineshaft"
<Button text="Grant Access"
onButtonPressed={() => size="md"
deleteMembershipAndResendInvite( />
row.email, </div>
row.membershipId )}
) </div>
} {myUser !== row.email &&
color="mineshaft" // row.role != "admin" &&
text="Resend Invite" myRole != "member" ? (
size="md" <div className="opacity-50 hover:opacity-100 flex items-center">
/> <Button
</div> onButtonPressed={(e) =>
)} handleDelete(row.membershipId, index, e)
{row.status == "completed" && }
myUser !== row.email && ( color="red"
<div className="border border-mineshaft-700 rounded-md bg-white/5 hover:bg-primary text-white hover:text-black duration-200"> size="icon-sm"
<Button icon={faX}
onButtonPressed={() => />
grantAccess( </div>
row.userId, ) : (
row.publicKey <div className="w-9 h-9"></div>
) )}
} </td>
color="mineshaft" </tr>
text="Grant Access" );
size="md" })}
/> </tbody>
</div> </table>
)} </div>
</div> );
{myUser !== row.email &&
// row.role != "admin" &&
myRole != "member" ? (
<div className="opacity-50 hover:opacity-100 flex items-center">
<Button
onButtonPressed={(e) =>
handleDelete(
row.membershipId,
index,
e
)
}
color="red"
size="icon-sm"
icon={faX}
/>
</div>
) : (
<div className="w-9 h-9"></div>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}; };
export default UserTable; export default UserTable;
-72
View File
@@ -1,72 +0,0 @@
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheck, faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { faCcMastercard, faCcVisa } from "@fortawesome/free-brands-svg-icons";
import { faCircle } from "@fortawesome/free-solid-svg-icons";
export default function Card({ card, changeSelectedCard, selected }) {
function creditCardBrandIcon(cc) {
if (cc == "visa") {
return faCcVisa;
} else if ((cc = "mastercard")) {
return faCcMastercard;
} else return faQuestionCircle;
}
return (
<button onClick={() => changeSelectedCard(card.id)}>
<div
className={`flex flex-col p-3 items-start justify-between mr-2 w-52 h-28 bg-primary/5 rounded-lg duration-200 ${
card.id == selected
? "border-primary text-primary"
: "border-gray-500 text-gray-300"
} hover:border-primary border-2 hover:text-primary cursor-pointer`}
>
<div className="flex flex-row items-center justify-between w-full">
<FontAwesomeIcon
className="text-3xl mr-4"
icon={creditCardBrandIcon(card.card.brand)}
/>
{card.id == selected && (
<FontAwesomeIcon
className="text-xl ml-2 mr-2"
icon={faCheck}
/>
)}
</div>
<div className="flex flex-row items-center justify-between w-full">
<div className="flex flex-row items-center">
<p className="tracking-tighter mr-1 mt-0.5 flex">
{"****".split("").map(() => (
<FontAwesomeIcon
className="text-xxxs mr-0.5"
icon={faCircle}
/>
))}
</p>
<p className="tracking-tighter mr-1 mt-0.5 flex">
{"****".split("").map(() => (
<FontAwesomeIcon
className="text-xxxs mr-0.5"
icon={faCircle}
/>
))}
</p>
<p className="tracking-tighter mr-1 mt-0.5 flex">
{"****".split("").map(() => (
<FontAwesomeIcon
className="text-xxxs mr-0.5"
icon={faCircle}
/>
))}
</p>
<p className="text-xs">{card.card.last4}</p>
</div>
<p className="text-xs">
{card.card.exp_month + "/" + card.card.exp_year}
</p>
</div>
</div>
</button>
);
}
+79 -77
View File
@@ -1,82 +1,84 @@
import React from "react"; import React from "react";
import StripeRedirect from "../../pages/api/organization/StripeRedirect";
import StripeRedirect from "~/pages/api/organization/StripeRedirect";
export default function Plan({ plan }) { export default function Plan({ plan }) {
return ( return (
<div <div
className={`relative flex flex-col justify-between border border-2 min-w-fit w-96 rounded-lg h-68 mr-4 bg-mineshaft-800 ${ className={`relative flex flex-col justify-between border border-2 min-w-fit w-96 rounded-lg h-68 mr-4 bg-mineshaft-800 ${
(plan.name != "Starter") & (plan.current == true) (plan.name != "Starter") & (plan.current == true)
? "border-primary" ? "border-primary"
: "border-chicago-700" : "border-chicago-700"
} }
`} `}
> >
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex flex-row justify-between items-center relative z-10"> <div className="flex flex-row justify-between items-center relative z-10">
<p <p className={`px-6 py-4 text-3xl font-semibold text-gray-400`}>
className={`px-6 py-4 text-3xl font-semibold text-gray-400`} {plan.name}
> </p>
{plan.name} </div>
</p> <div className="flex flwx-row items-end justify-start mb-4">
</div> <p className="pl-6 text-3xl font-semibold text-primary">
<div className="flex flwx-row items-end justify-start mb-4"> {plan.price}
<p className="pl-6 text-3xl font-semibold text-primary"> </p>
{plan.price} <p className="pl-3 mb-1 text-lg text-gray-400">
</p> {plan.priceExplanation}
<p className="pl-3 mb-1 text-lg text-gray-400"> </p>
{plan.priceExplanation} </div>
</p> <p className="relative z-10 max-w-fit px-6 text-base text-gray-400">
</div> {plan.text}
<p className="relative z-10 max-w-fit px-6 text-base text-gray-400"> </p>
{plan.text} <p className="relative z-10 max-w-fit px-6 text-base text-gray-400">
</p> {plan.subtext}
<p className="relative z-10 max-w-fit px-6 text-base text-gray-400"> </p>
{plan.subtext} </div>
</p> <div className="flex flex-row items-center">
</div> {plan.current == false ? (
<div className="flex flex-row items-center"> <>
{plan.current == false ? ( {plan.buttonTextMain == "Schedule a Demo" ? (
<> <a href="/scheduledemo" target='_blank rel="noopener"'>
{plan.buttonTextMain == "Schedule a Demo" ? ( <div className="relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-gray-600 hover:text-black hover:border-primary text-gray-400 font-semibold hover:bg-primary bg-bunker duration-200 cursor-pointer rounded-md flex w-max">
<a {plan.buttonTextMain}
href="/scheduledemo" </div>
target='_blank rel="noopener"' </a>
> ) : (
<div className="relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-gray-600 hover:text-black hover:border-primary text-gray-400 font-semibold hover:bg-primary bg-bunker duration-200 cursor-pointer rounded-md flex w-max"> <div
{plan.buttonTextMain} className={`relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-gray-600 text-gray-400 font-semibold ${
</div> plan.buttonTextMain == "Downgrade"
</a> ? "hover:bg-red hover:text-white hover:border-red"
) : ( : "hover:bg-primary hover:text-black hover:border-primary"
<div } bg-bunker duration-200 cursor-pointer rounded-md flex w-max`}
className={`relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-gray-600 text-gray-400 font-semibold ${ >
plan.buttonTextMain == "Downgrade" <button
? "hover:bg-red hover:text-white hover:border-red" onClick={() =>
: "hover:bg-primary hover:text-black hover:border-primary" StripeRedirect({
} bg-bunker duration-200 cursor-pointer rounded-md flex w-max`} orgId: localStorage.getItem("orgData.id"),
> })
<button onClick={() => StripeRedirect({orgId: localStorage.getItem("orgData.id")})}>{plan.buttonTextMain}</button> }
</div> >
)} {plan.buttonTextMain}
<a href="/pricing" target='_blank rel="noopener"'> </button>
<div className="relative z-10 text-gray-400 font-semibold hover:text-primary duration-200 cursor-pointer mb-0.5"> </div>
{plan.buttonTextSecondary} )}
</div> <a href="/pricing" target='_blank rel="noopener"'>
</a> <div className="relative z-10 text-gray-400 font-semibold hover:text-primary duration-200 cursor-pointer mb-0.5">
</> {plan.buttonTextSecondary}
) : ( </div>
<div </a>
className={`h-8 w-full rounded-b-md flex justify-center items-center z-10 ${ </>
(plan.name != "Starter") & (plan.current == true) ) : (
? "bg-primary" <div
: "bg-chicago-700" className={`h-8 w-full rounded-b-md flex justify-center items-center z-10 ${
}`} (plan.name != "Starter") & (plan.current == true)
> ? "bg-primary"
<p className="text-xs text-black font-semibold"> : "bg-chicago-700"
CURRENT PLAN }`}
</p> >
</div> <p className="text-xs text-black font-semibold">CURRENT PLAN</p>
)} </div>
</div> )}
</div> </div>
); </div>
);
} }
@@ -1,111 +1,131 @@
import React from "react"; import React, { Fragment } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircle } from "@fortawesome/free-solid-svg-icons"; import { faCircle } from "@fortawesome/free-solid-svg-icons";
import guidGenerator from "../utilities/randomId"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import guidGenerator from "../utilities/randomId";
/** /**
* This function splits the input of a dashboard field into the parts that are inside and outside of ${...} * This function splits the input of a dashboard field into the parts that are inside and outside of ${...}
* @param {string} text - the value of the input in the Dashboard Input Field * @param {string} text - the value of the input in the Dashboard Input Field
* @returns * @returns
*/ */
const findReferences = (text) => { const findReferences = (text) => {
var splitText = text.split('${'); var splitText = text.split("${");
let textArray = [splitText[0]]; let textArray = [splitText[0]];
for (var i = 1; i < splitText.length; i++) { for (var i = 1; i < splitText.length; i++) {
let insideBrackets = "${" + splitText[i].split('}')[0] let insideBrackets = "${" + splitText[i].split("}")[0];
if (splitText[i].includes('}')) { if (splitText[i].includes("}")) {
insideBrackets += "}" insideBrackets += "}";
} }
textArray.push(insideBrackets) textArray.push(insideBrackets);
textArray.push(splitText[i].split('}')[1]) textArray.push(splitText[i].split("}")[1]);
} }
return textArray; return textArray;
} };
/** /**
* This component renders the input fields on the dashboard * This component renders the input fields on the dashboard
* @param {object} obj - the order number of a keyPair * @param {object} obj - the order number of a keyPair
* @param {number} obj.index - the order number of a keyPair * @param {number} obj.index - the order number of a keyPair
* @param {function} obj.onChangeHandler - what happens when the input is modified * @param {function} obj.onChangeHandler - what happens when the input is modified
* @param {string} obj.type - whether the input field is for a Key Name or for a Key Value * @param {string} obj.type - whether the input field is for a Key Name or for a Key Value
* @param {string} obj.value - value of the InputField * @param {string} obj.value - value of the InputField
* @param {boolean} obj.blurred - whether the input field should be blurred (behind the gray dots) or not; this can be turned on/off in the dashboard * @param {boolean} obj.blurred - whether the input field should be blurred (behind the gray dots) or not; this can be turned on/off in the dashboard
* @returns * @returns
*/ */
const DashboardInputField = ({index, onChangeHandler, type, value, blurred}) => { const DashboardInputField = ({
if (type === "varName") { index,
return ( onChangeHandler,
<div className="flex-col w-full"> type,
<div value,
className={`group relative flex flex-col justify-center w-full max-w-2xl border border-mineshaft-500 rounded-md`} blurred,
> }) => {
<input if (type === "varName") {
onChange={(e) => onChangeHandler(e.target.value, index)} return (
type={type} <div className="flex-col w-full">
value={value} <div
className="asolute z-10 peer font-mono ph-no-capture bg-bunker-800 rounded-md caret-white text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-4 focus:ring-primary/50 duration-200" className={`group relative flex flex-col justify-center w-full max-w-2xl border border-mineshaft-500 rounded-md`}
spellCheck="false" >
/> <input
</div> onChange={(e) => onChangeHandler(e.target.value, index)}
</div> type={type}
); value={value}
} else if (type === "value") { className="asolute z-10 peer font-mono ph-no-capture bg-bunker-800 rounded-md caret-white text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-4 focus:ring-primary/50 duration-200"
return ( spellCheck="false"
<div className="flex-col w-full"> />
<div </div>
className={`group relative flex flex-col justify-center w-full max-w-2xl border border-mineshaft-500 rounded-md`} </div>
> );
<input } else if (type === "value") {
onChange={(e) => onChangeHandler(e.target.value, index)} return (
type={type} <div className="flex-col w-full">
value={value} <div
className={`${ className={`group relative flex flex-col justify-center w-full max-w-2xl border border-mineshaft-500 rounded-md`}
blurred >
? "text-transparent group-hover:text-transparent focus:text-transparent active:text-transparent" <input
: "" onChange={(e) => onChangeHandler(e.target.value, index)}
} asolute z-10 peer font-mono ph-no-capture bg-transparent rounded-md caret-white text-transparent text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-4 focus:ring-primary/50 duration-200`} type={type}
spellCheck="false" value={value}
/> className={`${
<div blurred
className={`${ ? "text-transparent group-hover:text-transparent focus:text-transparent active:text-transparent"
blurred : ""
? "text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-400 peer-active:text-gray-400" } asolute z-10 peer font-mono ph-no-capture bg-transparent rounded-md caret-white text-transparent text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-4 focus:ring-primary/50 duration-200`}
: "" spellCheck="false"
} flex flex-row font-mono absolute z-0 ph-no-capture bg-bunker-800 rounded-md text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-4 focus:ring-primary/50 duration-100`} />
> <div
{findReferences(value).map((text, id) => className={`${
id % 2 == 0 || text.length <= 2 blurred
? <span className="ph-no-capture">{text}</span> ? "text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-400 peer-active:text-gray-400"
: <span className="ph-no-capture text-yellow"> : ""
{text.slice(0, 2)} } flex flex-row font-mono absolute z-0 ph-no-capture bg-bunker-800 h-9 rounded-md text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-4 focus:ring-primary/50 duration-100`}
<span className="ph-no-capture text-yellow-200/80"> >
{text.slice(2, text.length - 1)} {findReferences(value).map((texts, id) => {
</span> if (id % 2 == 0 || texts.length <= 2) {
{text.slice(text.length - 1, text.length) == "}" return (
? <span className="ph-no-capture text-yellow">{text.slice(text.length - 1, text.length)} </span> <span className="ph-no-capture" key={id}>
: <span className="ph-no-capture text-yellow-400">{text.slice(text.length - 1, text.length)} </span>} {texts}
</span>)} </span>
</div> );
{blurred && ( }
<div className="z-20 peer pr-2 bg-bunker-800 group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible absolute h-9 w-fit max-w-xl rounded-md flex items-center text-gray-400/50 text-clip overflow-hidden"> return (
<p className="ml-2"></p> <span className="ph-no-capture text-yellow" key={id}>
{value {texts.slice(0, 2)}
.split("") <span className="ph-no-capture text-yellow-200/80">
.slice(0,42) {texts.slice(2, texts.length - 1)}
.map(() => ( </span>
<FontAwesomeIcon {texts.slice(texts.length - 1, texts.length) == "}" ? (
key={guidGenerator()} <span className="ph-no-capture text-yellow">
className="text-xxs mx-0.5" {texts.slice(texts.length - 1, texts.length)}{" "}
icon={faCircle} </span>
/> ) : (
))} <span className="ph-no-capture text-yellow-400">
</div> {texts.slice(texts.length - 1, texts.length)}{" "}
)} </span>
</div> )}
</div> </span>
); );
} })}
</div>
{blurred && (
<div className="z-20 peer pr-2 bg-bunker-800 group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible absolute h-9 w-fit max-w-xl rounded-md flex items-center text-gray-400/50 text-clip overflow-hidden">
<p className="ml-2"></p>
{value
.split("")
.slice(0, 42)
.map(() => (
<FontAwesomeIcon
key={guidGenerator()}
className="text-xxs mx-0.5"
icon={faCircle}
/>
))}
</div>
)}
</div>
</div>
);
}
}; };
export default React.memo(DashboardInputField); export default React.memo(DashboardInputField);
+167 -171
View File
@@ -1,190 +1,186 @@
import { useState } from "react"; import { useState } from "react";
import Image from "next/image";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import useTranslation from "next-translate/useTranslation";
import { faUpload } from "@fortawesome/free-solid-svg-icons"; import { faUpload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Button from "../basic/buttons/Button";
import Error from "../basic/Error";
import parse from "../utilities/file";
import guidGenerator from "../utilities/randomId"; import guidGenerator from "../utilities/randomId";
import Error from "../basic/Error";
import Image from "next/image";
import parse from "../utilities/file";
import Button from "../basic/buttons/Button";
import useTranslation from "next-translate/useTranslation";
const DropZone = ({ const DropZone = ({
setData, setData,
setErrorDragAndDrop, setErrorDragAndDrop,
createNewFile, createNewFile,
errorDragAndDrop, errorDragAndDrop,
addPresetRow, addPresetRow,
setButtonReady, setButtonReady,
keysExist, keysExist,
numCurrentRows, numCurrentRows,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const handleDragEnter = (e) => { const handleDragEnter = (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
}; };
const handleDragLeave = (e) => { const handleDragLeave = (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
}; };
const handleDragOver = (e) => { const handleDragOver = (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// set dropEffect to copy i.e copy of the source item // set dropEffect to copy i.e copy of the source item
e.dataTransfer.dropEffect = "copy"; e.dataTransfer.dropEffect = "copy";
}; };
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// This function function immediately parses the file after it is dropped // This function function immediately parses the file after it is dropped
const handleDrop = async (e) => { const handleDrop = async (e) => {
setLoading(true); setLoading(true);
setTimeout(() => setLoading(false), 5000); setTimeout(() => setLoading(false), 5000);
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
e.dataTransfer.dropEffect = "copy"; e.dataTransfer.dropEffect = "copy";
var file = e.dataTransfer.files[0], var file = e.dataTransfer.files[0],
reader = new FileReader(); reader = new FileReader();
reader.onload = function (event) { reader.onload = function (event) {
const keyPairs = parse(event.target.result); const keyPairs = parse(event.target.result);
const newData = Object.keys(keyPairs).map((key, index) => [ const newData = Object.keys(keyPairs).map((key, index) => [
guidGenerator(), guidGenerator(),
numCurrentRows + index, numCurrentRows + index,
key, key,
keyPairs[key], keyPairs[key],
"shared", "shared",
]); ]);
setData(newData); setData(newData);
setButtonReady(true); setButtonReady(true);
}; };
// If something is wrong show an error // If something is wrong show an error
try { try {
reader.readAsText(file); reader.readAsText(file);
setLoading(false); setLoading(false);
} catch (error) { } catch (error) {
setErrorDragAndDrop(true); setErrorDragAndDrop(true);
setLoading(false); setLoading(false);
} }
}; };
// This function is used when the user manually selects a file from the in-browser dircetory (not drag and drop) // This function is used when the user manually selects a file from the in-browser dircetory (not drag and drop)
const handleFileSelect = (e) => { const handleFileSelect = (e) => {
setLoading(true); setLoading(true);
setTimeout(() => setLoading(false), 5000); setTimeout(() => setLoading(false), 5000);
var file = e.target.files[0], var file = e.target.files[0],
reader = new FileReader(); reader = new FileReader();
reader.onload = function (event) { reader.onload = function (event) {
const newData = event.target.result const newData = event.target.result
.split("\n") .split("\n")
.map((line, index) => [ .map((line, index) => [
guidGenerator(), guidGenerator(),
numCurrentRows + index, numCurrentRows + index,
line.split("=")[0], line.split("=")[0],
line.split("=").slice(1, line.split("=").length).join("="), line.split("=").slice(1, line.split("=").length).join("="),
"shared", "shared",
]); ]);
setData(newData); setData(newData);
setButtonReady(true); setButtonReady(true);
}; };
reader.readAsText(file); reader.readAsText(file);
}; };
return loading ? ( return loading ? (
<div className="flex items-center justify-center pt-16 mb-16"> <div className="flex items-center justify-center pt-16 mb-16">
<Image <Image
src="/images/loading/loading.gif" src="/images/loading/loading.gif"
height={70} height={70}
width={120} width={120}
alt="google logo" alt="google logo"
></Image> ></Image>
</div> </div>
) : keysExist ? ( ) : keysExist ? (
<div <div
className="opacity-60 hover:opacity-100 duration-200 relative bg-bunker outline max-w-[calc(100%-1rem)] w-full outline-dashed outline-gray-600 rounded-md outline-2 flex flex-col items-center justify-center mb-16 mx-auto mt-1 py-8 px-2" className="opacity-60 hover:opacity-100 duration-200 relative bg-bunker outline max-w-[calc(100%-1rem)] w-full outline-dashed outline-gray-600 rounded-md outline-2 flex flex-col items-center justify-center mb-16 mx-auto mt-1 py-8 px-2"
onDragEnter={(e) => handleDragEnter(e)} onDragEnter={(e) => handleDragEnter(e)}
onDragOver={(e) => handleDragOver(e)} onDragOver={(e) => handleDragOver(e)}
onDragLeave={(e) => handleDragLeave(e)} onDragLeave={(e) => handleDragLeave(e)}
onDrop={(e) => handleDrop(e)} onDrop={(e) => handleDrop(e)}
> >
<input <input
id="fileSelect" id="fileSelect"
type="file" type="file"
className="opacity-0 absolute w-full h-full" className="opacity-0 absolute w-full h-full"
accept=".txt,.env" accept=".txt,.env"
onChange={(e) => handleFileSelect(e)} onChange={(e) => handleFileSelect(e)}
/> />
{errorDragAndDrop ? ( {errorDragAndDrop ? (
<div className="my-3 max-w-xl opacity-80"></div> <div className="my-3 max-w-xl opacity-80"></div>
) : ( ) : (
<div className=""></div> <div className=""></div>
)} )}
<div className="flex flex-row"> <div className="flex flex-row">
<FontAwesomeIcon <FontAwesomeIcon
icon={faUpload} icon={faUpload}
className="text-gray-300 text-3xl mr-6" className="text-gray-300 text-3xl mr-6"
/> />
<p className="text-gray-300 mt-1"> <p className="text-gray-300 mt-1">{t("common:drop-zone-keys")}</p>
{t("common:drop-zone-keys")} </div>
</p> {errorDragAndDrop ? (
</div> <div className="mt-8 max-w-xl opacity-80">
{errorDragAndDrop ? ( <Error text="Something went wrong! Make sure you drag the file directly from the folder in which it is located (e.g., not VS code). Tip: click 'Reveal in Finder/Explorer'" />
<div className="mt-8 max-w-xl opacity-80"> </div>
<Error text="Something went wrong! Make sure you drag the file directly from the folder in which it is located (e.g., not VS code). Tip: click 'Reveal in Finder/Explorer'" /> ) : (
</div> <></>
) : ( )}
<></> </div>
)} ) : (
</div> <div
) : ( className="opacity-80 hover:opacity-100 duration-200 relative bg-bunker outline max-w-2xl w-full outline-dashed outline-gray-700 rounded-md outline-2 flex flex-col items-center justify-center pt-16 mb-16 px-4"
<div onDragEnter={(e) => handleDragEnter(e)}
className="opacity-80 hover:opacity-100 duration-200 relative bg-bunker outline max-w-2xl w-full outline-dashed outline-gray-700 rounded-md outline-2 flex flex-col items-center justify-center pt-16 mb-16 px-4" onDragOver={(e) => handleDragOver(e)}
onDragEnter={(e) => handleDragEnter(e)} onDragLeave={(e) => handleDragLeave(e)}
onDragOver={(e) => handleDragOver(e)} onDrop={(e) => handleDrop(e)}
onDragLeave={(e) => handleDragLeave(e)} >
onDrop={(e) => handleDrop(e)} <FontAwesomeIcon icon={faUpload} className="text-7xl mb-8" />
> <p className="">{t("common:drop-zone")}</p>
<FontAwesomeIcon icon={faUpload} className="text-7xl mb-8" /> <input
<p className="">{t("common:drop-zone")}</p> id="fileSelect"
<input type="file"
id="fileSelect" className="opacity-0 absolute w-full h-full"
type="file" accept=".txt,.env"
className="opacity-0 absolute w-full h-full" onChange={(e) => handleFileSelect(e)}
accept=".txt,.env" />
onChange={(e) => handleFileSelect(e)} <div className="flex flex-row w-full items-center justify-center mb-6 mt-5">
/> <div className="border-t border-gray-700 w-1/5"></div>
<div className="flex flex-row w-full items-center justify-center mb-6 mt-5"> <p className="text-gray-400 text-xs mx-4">OR</p>
<div className="border-t border-gray-700 w-1/5"></div> <div className="border-t border-gray-700 w-1/5"></div>
<p className="text-gray-400 text-xs mx-4">OR</p> </div>
<div className="border-t border-gray-700 w-1/5"></div> <div className="z-10 mb-6">
</div> <Button
<div className="z-10 mb-6"> color="mineshaft"
<Button text="Create a new .env file"
color="mineshaft" onButtonPressed={createNewFile}
text="Create a new .env file" size="md"
onButtonPressed={createNewFile} />
size="md" </div>
/> {errorDragAndDrop ? (
</div> <div className="opacity-80">
{errorDragAndDrop ? ( <Error text="Something went wrong! Make sure you drag the file directly from the folder in which it is located (e.g., not VS code). Tip: click 'Reveal in Finder/Explorer'" />
<div className="opacity-80"> </div>
<Error text="Something went wrong! Make sure you drag the file directly from the folder in which it is located (e.g., not VS code). Tip: click 'Reveal in Finder/Explorer'" /> ) : (
</div> <div className="py-3">
) : ( {/* <p className="text-xs text-gray-500"> If you are expecting to see a file here, contact your administrator for permission. </p> */}
<div className="py-3"> </div>
{/* <p className="text-xs text-gray-500"> If you are expecting to see a file here, contact your administrator for permission. </p> */} )}
</div> </div>
)} );
</div>
);
}; };
export default DropZone; export default DropZone;
+277 -309
View File
@@ -1,328 +1,296 @@
import React, { useState, Fragment, useEffect, useMemo } from "react"; /* eslint-disable react/jsx-key */
import { useRouter } from "next/router"; import React, { Fragment, useEffect, useMemo, useState } from "react";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/router";
import logout from "../../pages/api/auth/Logout"; import useTranslation from "next-translate/useTranslation";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons"; import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
import { import {
faBook, faAngleDown,
faGear, faBook,
faCoins, faCoins,
faRightFromBracket, faEnvelope,
faEnvelope, faGear,
faPlus, faPlus,
faAngleDown, faRightFromBracket,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { faSlack, faGithub } from "@fortawesome/free-brands-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Menu, Transition } from "@headlessui/react"; import { Menu, Transition } from "@headlessui/react";
import getUser from "../../pages/api/user/getUser";
import getOrganizations from "../../pages/api/organization/getOrgs";
import getOrganization from "../../pages/api/organization/GetOrg";
import guidGenerator from "../utilities/randomId";
import useTranslation from "next-translate/useTranslation";
import getOrganization from "../../pages/api/organization/GetOrg";
import getOrganizations from "../../pages/api/organization/getOrgs";
import getUser from "../../pages/api/user/getUser";
import guidGenerator from "../utilities/randomId";
/** /**
* @param {(key: string) => string} t * @param {(key: string) => string} t
*/ */
const supportOptions = (t) => [ const supportOptions = (t) => [
[ [
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faSlack} />, <FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faSlack} />,
t("nav:support.slack"), t("nav:support.slack"),
"https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA", "https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA",
], ],
[ [
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faBook} />, <FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faBook} />,
t("nav:support.docs"), t("nav:support.docs"),
"https://infisical.com/docs/getting-started/introduction", "https://infisical.com/docs/getting-started/introduction",
], ],
[ [
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faGithub} />, <FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faGithub} />,
t("nav:support.issue"), t("nav:support.issue"),
"https://github.com/Infisical/infisical-cli/issues", "https://github.com/Infisical/infisical-cli/issues",
], ],
[ [
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faEnvelope} />, <FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faEnvelope} />,
t("nav:support.email"), t("nav:support.email"),
"mailto:[email protected]", "mailto:[email protected]",
], ],
]; ];
export default function Navbar({ onButtonPressed }) { export default function Navbar({ onButtonPressed }) {
const router = useRouter(); const router = useRouter();
const [user, setUser] = useState({}); const [user, setUser] = useState({});
const [orgs, setOrgs] = useState([]); const [orgs, setOrgs] = useState([]);
const [currentOrg, setCurrentOrg] = useState([]); const [currentOrg, setCurrentOrg] = useState([]);
const { t } = useTranslation(""); const { t } = useTranslation("");
const supportOptionsList = useMemo(() => supportOptions(t), [t]); const supportOptionsList = useMemo(() => supportOptions(t), [t]);
useEffect(async () => { useEffect(async () => {
const userData = await getUser(); const userData = await getUser();
setUser(userData); setUser(userData);
const orgsData = await getOrganizations(); const orgsData = await getOrganizations();
setOrgs(orgsData); setOrgs(orgsData);
const currentOrg = await getOrganization({ const currentOrg = await getOrganization({
orgId: localStorage.getItem("orgData.id"), orgId: localStorage.getItem("orgData.id"),
}); });
setCurrentOrg(currentOrg); setCurrentOrg(currentOrg);
}, []); }, []);
const closeApp = async () => { const closeApp = async () => {
console.log("Logging out..."); console.log("Logging out...");
await logout(); await logout();
router.push("/"); router.push("/");
}; };
return ( return (
<div className="absolute flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-50"> <div className="absolute flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-50">
<div className="m-auto flex justify-start items-center mx-4"> <div className="m-auto flex justify-start items-center mx-4">
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
<div className="flex justify-center py-4"> <div className="flex justify-center py-4">
<Image <Image
src="/images/logotransparent.png" src="/images/logotransparent.png"
height={23} height={23}
width={57} width={57}
alt="logo" alt="logo"
/> />
</div> </div>
<a className="text-2xl text-white font-semibold mx-2"> <a className="text-2xl text-white font-semibold mx-2">Infisical</a>
Infisical </div>
</a> </div>
</div> <div className="relative flex justify-start items-center mx-2 z-40">
</div> <Menu as="div" className="relative inline-block text-left">
<div className="relative flex justify-start items-center mx-2 z-40"> <div className="mr-4">
<Menu as="div" className="relative inline-block text-left"> <Menu.Button className="inline-flex w-full justify-center rounded-md px-2 py-2 text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
<div className="mr-4"> <FontAwesomeIcon className="text-xl" icon={faCircleQuestion} />
<Menu.Button className="inline-flex w-full justify-center rounded-md px-2 py-2 text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"> </Menu.Button>
<FontAwesomeIcon </div>
className="text-xl" <Transition
icon={faCircleQuestion} as={Fragment}
/> enter="transition ease-out duration-100"
</Menu.Button> enterFrom="transform opacity-0 scale-95"
</div> enterTo="transform opacity-100 scale-100"
<Transition leave="transition ease-in duration-75"
as={Fragment} leaveFrom="transform opacity-100 scale-100"
enter="transition ease-out duration-100" leaveTo="transform opacity-0 scale-95"
enterFrom="transform opacity-0 scale-95" >
enterTo="transform opacity-100 scale-100" <Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-20 ring-opacity-5 focus:outline-none px-2 py-1.5">
leave="transition ease-in duration-75" {supportOptionsList.map(([icon, text, url]) => (
leaveFrom="transform opacity-100 scale-100" <a
leaveTo="transform opacity-0 scale-95" key={guidGenerator()}
> target="_blank"
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-20 ring-opacity-5 focus:outline-none px-2 py-1.5"> rel="noopener noreferrer"
{supportOptionsList.map(([icon, text, url]) => ( href={url}
<a className="font-normal text-gray-300 duration-200 rounded-md w-full flex items-center py-0.5"
key={guidGenerator()} >
target="_blank" <div className="relative flex justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full">
rel="noopener" {icon}
href={url} <div className="text-sm">{text}</div>
className="font-normal text-gray-300 duration-200 rounded-md w-full flex items-center py-0.5" </div>
> </a>
<div className="relative flex justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"> ))}
{icon} </Menu.Items>
<div className="text-sm">{text}</div> </Transition>
</div> </Menu>
</a> <Menu as="div" className="relative inline-block text-left mr-4">
))} <div>
</Menu.Items> <Menu.Button className="inline-flex w-full justify-center rounded-md pr-2 pl-2 py-2 text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
</Transition> {user?.firstName} {user?.lastName}
</Menu> <FontAwesomeIcon
<Menu as="div" className="relative inline-block text-left mr-4"> icon={faAngleDown}
<div> className="ml-2 mt-1 text-sm text-gray-300 hover:text-lime-100"
<Menu.Button className="inline-flex w-full justify-center rounded-md pr-2 pl-2 py-2 text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"> />
{user?.firstName} {user?.lastName} </Menu.Button>
<FontAwesomeIcon </div>
icon={faAngleDown} <Transition
className="ml-2 mt-1 text-sm text-gray-300 hover:text-lime-100" as={Fragment}
/> enter="transition ease-out duration-100"
</Menu.Button> enterFrom="transform opacity-0 scale-95"
</div> enterTo="transform opacity-100 scale-100"
<Transition leave="transition ease-in duration-75"
as={Fragment} leaveFrom="transform opacity-100 scale-100"
enter="transition ease-out duration-100" leaveTo="transform opacity-0 scale-95"
enterFrom="transform opacity-0 scale-95" >
enterTo="transform opacity-100 scale-100" <Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-20 ring-opacity-5 focus:outline-none">
leave="transition ease-in duration-75" <div className="px-1 py-1 ">
leaveFrom="transform opacity-100 scale-100" <div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
leaveTo="transform opacity-0 scale-95" {t("nav:user.signed-in-as")}
> </div>
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-20 ring-opacity-5 focus:outline-none"> <div
<div className="px-1 py-1 "> onClick={() =>
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide"> router.push("/settings/personal/" + router.query.id)
{t("nav:user.signed-in-as")} }
</div> className="flex flex-row items-center px-1 mx-1 my-1 hover:bg-white/5 cursor-pointer rounded-md"
<div >
onClick={() => <div className="bg-white/10 h-8 w-9 rounded-full flex items-center justify-center text-gray-300">
router.push( {user?.firstName?.charAt(0)}
"/settings/personal/" + </div>
router.query.id <div className="flex items-center justify-between w-full">
) <div>
} <p className="text-gray-300 px-2 pt-1 text-sm">
className="flex flex-row items-center px-1 mx-1 my-1 hover:bg-white/5 cursor-pointer rounded-md" {" "}
> {user?.firstName} {user?.lastName}
<div className="bg-white/10 h-8 w-9 rounded-full flex items-center justify-center text-gray-300"> </p>
{user?.firstName?.charAt(0)} <p className="text-gray-400 px-2 pb-1 text-xs">
</div> {" "}
<div className="flex items-center justify-between w-full"> {user?.email}
<div> </p>
<p className="text-gray-300 px-2 pt-1 text-sm"> </div>
{" "} <FontAwesomeIcon
{user?.firstName}{" "} icon={faGear}
{user?.lastName} className="text-lg text-gray-400 p-2 mr-1 rounded-md cursor-pointer hover:bg-white/10"
</p> />
<p className="text-gray-400 px-2 pb-1 text-xs"> </div>
{" "} </div>
{user?.email} </div>
</p> <div className="px-2 pt-2">
</div> <div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
<FontAwesomeIcon {t("nav:user.current-organization")}
icon={faGear} </div>
className="text-lg text-gray-400 p-2 mr-1 rounded-md cursor-pointer hover:bg-white/10" <div
/> onClick={() =>
</div> router.push("/settings/org/" + router.query.id)
</div> }
</div> className="flex flex-row items-center px-2 mt-2 py-1 hover:bg-white/5 cursor-pointer rounded-md"
<div className="px-2 pt-2"> >
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide"> <div className="bg-white/10 h-7 w-8 rounded-md flex items-center justify-center text-gray-300">
{t("nav:user.current-organization")} {currentOrg?.name?.charAt(0)}
</div> </div>
<div <div className="flex items-center justify-between w-full">
onClick={() => <p className="text-gray-300 px-2 text-sm">
router.push( {currentOrg?.name}
"/settings/org/" + router.query.id </p>
) <FontAwesomeIcon
} icon={faGear}
className="flex flex-row items-center px-2 mt-2 py-1 hover:bg-white/5 cursor-pointer rounded-md" className="text-lg text-gray-400 p-2 rounded-md cursor-pointer hover:bg-white/10"
> />
<div className="bg-white/10 h-7 w-8 rounded-md flex items-center justify-center text-gray-300"> </div>
{currentOrg?.name?.charAt(0)} </div>
</div> <button
<div className="flex items-center justify-between w-full"> // onClick={buttonAction}
<p className="text-gray-300 px-2 text-sm"> className="cursor-pointer w-full"
{currentOrg?.name} >
</p> <div
<FontAwesomeIcon onClick={() =>
icon={faGear} router.push("/settings/billing/" + router.query.id)
className="text-lg text-gray-400 p-2 rounded-md cursor-pointer hover:bg-white/10" }
/> className="mt-1 relative flex justify-start cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/5 duration-200 hover:text-gray-200"
</div> >
</div> <FontAwesomeIcon
<button className="text-lg pl-1.5 pr-3"
// onClick={buttonAction} icon={faCoins}
className="cursor-pointer w-full" />
> <div className="text-sm">{t("nav:user.usage-billing")}</div>
<div </div>
onClick={() => </button>
router.push( <button
"/settings/billing/" + // onClick={buttonAction}
router.query.id className="cursor-pointer w-full mb-2"
) >
} <div
className="mt-1 relative flex justify-start cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/5 duration-200 hover:text-gray-200" onClick={() =>
> router.push(
<FontAwesomeIcon "/settings/org/" + router.query.id + "?invite"
className="text-lg pl-1.5 pr-3" )
icon={faCoins} }
/> className="relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-primary/100 duration-200 hover:text-black hover:font-semibold mt-1"
<div className="text-sm"> >
{t("nav:user.usage-billing")} <span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4">
</div> <FontAwesomeIcon icon={faPlus} className="ml-1" />
</div> </span>
</button> <div className="text-sm ml-1">{t("nav:user.invite")}</div>
<button </div>
// onClick={buttonAction} </button>
className="cursor-pointer w-full mb-2" </div>
> {orgs?.length > 1 && (
<div <div className="px-1 pt-1">
onClick={() => <div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
router.push( {t("nav:user.other-organizations")}
"/settings/org/" + </div>
router.query.id + <div className="flex flex-col items-start px-1 mt-3 mb-2">
"?invite" {orgs
) .filter(
} (org) => org._id != localStorage.getItem("orgData.id")
className="relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-primary/100 duration-200 hover:text-black hover:font-semibold mt-1" )
> .map((org) => (
<span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4"> <div
<FontAwesomeIcon key={guidGenerator()}
icon={faPlus} onClick={() => {
className="ml-1" localStorage.setItem("orgData.id", org._id);
/> router.reload();
</span> }}
<div className="text-sm ml-1"> className="flex flex-row justify-start items-center hover:bg-white/5 w-full p-1.5 cursor-pointer rounded-md"
{t("nav:user.invite")} >
</div> <div className="bg-white/10 h-7 w-8 rounded-md flex items-center justify-center text-gray-300">
</div> {org.name.charAt(0)}
</button> </div>
</div> <div className="flex items-center justify-between w-full">
{orgs?.length > 1 && ( <p className="text-gray-300 px-2 text-sm">
<div className="px-1 pt-1"> {org.name}
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide"> </p>
{t("nav:user.other-organizations")} </div>
</div> </div>
<div className="flex flex-col items-start px-1 mt-3 mb-2"> ))}
{orgs </div>
.filter( </div>
(org) => )}
org._id != <div className="px-1 py-1">
localStorage.getItem( <Menu.Item>
"orgData.id" {({ active }) => (
) <button
) onClick={closeApp}
.map((org) => ( className={`${
<div active
key={guidGenerator()} ? "bg-red font-semibold text-white"
onClick={() => { : "text-gray-400"
localStorage.setItem( } group flex w-full items-center rounded-md px-2 py-2 text-sm`}
"orgData.id", >
org._id <div className="relative flex justify-start items-center cursor-pointer select-none">
); <FontAwesomeIcon
router.reload(); className="text-lg ml-1.5 mr-3"
}} icon={faRightFromBracket}
className="flex flex-row justify-start items-center hover:bg-white/5 w-full p-1.5 cursor-pointer rounded-md" />
> {t("common:logout")}
<div className="bg-white/10 h-7 w-8 rounded-md flex items-center justify-center text-gray-300"> </div>
{org.name.charAt(0)} </button>
</div> )}
<div className="flex items-center justify-between w-full"> </Menu.Item>
<p className="text-gray-300 px-2 text-sm"> </div>
{org.name} </Menu.Items>
</p> </Transition>
</div> </Menu>
</div> </div>
))} </div>
</div> );
</div>
)}
<div className="px-1 py-1">
<Menu.Item>
{({ active }) => (
<button
onClick={closeApp}
className={`${
active
? "bg-red font-semibold text-white"
: "text-gray-400"
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
>
<div className="relative flex justify-start items-center cursor-pointer select-none">
<FontAwesomeIcon
className="text-lg ml-1.5 mr-3"
icon={faRightFromBracket}
/>
{t("common:logout")}
</div>
</button>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
</div>
);
} }
+51 -41
View File
@@ -1,47 +1,57 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAngleRight, faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { faCcMastercard, faCcVisa } from "@fortawesome/free-brands-svg-icons";
import { faCircle } from "@fortawesome/free-solid-svg-icons";
import getOrganization from "../../pages/api/organization/GetOrg";
import getWorkspaceInfo from "../../pages/api/workspace/getWorkspaceInfo";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { faCcMastercard, faCcVisa } from "@fortawesome/free-brands-svg-icons";
import {
faAngleRight,
faQuestionCircle,
} from "@fortawesome/free-solid-svg-icons";
import { faCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export default function NavHeader({ pageName, isProjectRelated}) { import getOrganization from "~/pages/api/organization/GetOrg";
const [orgName, setOrgName] = useState(""); import getWorkspaceInfo from "~/pages/api/workspace/getWorkspaceInfo";
const [workspaceName, setWorkspaceName] = useState("");
const router = useRouter();
useEffect(async () => { export default function NavHeader({ pageName, isProjectRelated }) {
let org = await getOrganization({ const [orgName, setOrgName] = useState("");
orgId: localStorage.getItem("orgData.id"), const [workspaceName, setWorkspaceName] = useState("");
}); const router = useRouter();
setOrgName(org.name);
let workspace = await getWorkspaceInfo({
workspaceId: router.query.id,
});
setWorkspaceName(workspace.name);
}, []);
return ( useEffect(() => {
<div className="pt-20 ml-6 flex flex-row items-center"> (async () => {
<div className="bg-primary-900 h-6 w-6 rounded-md flex items-center justify-center text-mineshaft-100 mr-2"> let org = await getOrganization({
{orgName?.charAt(0)} orgId: localStorage.getItem("orgData.id"),
</div> });
<div className="text-primary text-sm font-semibold"> setOrgName(org.name);
{orgName} let workspace = await getWorkspaceInfo({
</div> workspaceId: router.query.id,
{isProjectRelated && <> });
<FontAwesomeIcon icon={faAngleRight} className="ml-3 text-sm text-gray-400 mr-3" /> setWorkspaceName(workspace.name);
<div className="font-semibold text-primary text-sm"> })();
{workspaceName} // eslint-disable-next-line react-hooks/exhaustive-deps
</div> }, []);
</>
} return (
<FontAwesomeIcon icon={faAngleRight} className="ml-3 text-sm text-gray-400 mr-3" /> <div className="pt-20 ml-6 flex flex-row items-center">
<div className="text-gray-400 text-sm"> <div className="bg-primary-900 h-6 w-6 rounded-md flex items-center justify-center text-mineshaft-100 mr-2">
{pageName} {orgName?.charAt(0)}
</div> </div>
</div> <div className="text-primary text-sm font-semibold">{orgName}</div>
); {isProjectRelated && (
<>
<FontAwesomeIcon
icon={faAngleRight}
className="ml-3 text-sm text-gray-400 mr-3"
/>
<div className="font-semibold text-primary text-sm">
{workspaceName}
</div>
</>
)}
<FontAwesomeIcon
icon={faAngleRight}
className="ml-3 text-sm text-gray-400 mr-3"
/>
<div className="text-gray-400 text-sm">{pageName}</div>
</div>
);
} }
+20 -22
View File
@@ -1,29 +1,27 @@
import token from "../../pages/api/auth/Token" import { PATH } from "~/const";
import { PATH } from '../../const'; import token from "~/pages/api/auth/Token";
export default class SecurityClient { export default class SecurityClient {
static authOrigins = [PATH] static authOrigins = [PATH];
static #token = ''; static #token = "";
contructor() { contructor() {}
static setToken(token) {
this.#token = token;
}
static async fetchCall(resource, options) {
let req = new Request(resource, options);
const destOrigin = new URL(req.url).origin;
if (this.#token == "") {
this.setToken(await token());
} }
static setToken(token) { if (this.#token && this.authOrigins.includes(destOrigin)) {
this.#token = token; req.headers.set("Authorization", "Bearer " + this.#token);
return fetch(req);
} }
}
static async fetchCall(resource, options) { }
let req = new Request(resource, options);
const destOrigin = new URL(req.url).origin;
if (this.#token == "") {
this.setToken(await token())
}
if (this.#token && this.authOrigins.includes(destOrigin)) {
req.headers.set('Authorization', "Bearer " + this.#token);
return fetch(req);
}
}
}
+150 -152
View File
@@ -1,12 +1,13 @@
import login1 from "../../pages/api/auth/Login1"; import Aes256Gcm from "~/components/utilities/cryptography/aes-256-gcm";
import login2 from "../../pages/api/auth/Login2"; import login1 from "~/pages/api/auth/Login1";
import Aes256Gcm from "../../components/aes-256-gcm"; import login2 from "~/pages/api/auth/Login2";
import pushKeys from "./pushKeys"; import getOrganizations from "~/pages/api/organization/getOrgs";
import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProjects";
import { initPostHog } from "../analytics/posthog"; import { initPostHog } from "../analytics/posthog";
import getOrganizations from "../../pages/api/organization/getOrgs"; import pushKeys from "./secrets/pushKeys";
import getOrganizationUserProjects from "../../pages/api/organization/GetOrgUserProjects";
import SecurityClient from "./SecurityClient";
import { ENV } from "./config"; import { ENV } from "./config";
import SecurityClient from "./SecurityClient";
const nacl = require("tweetnacl"); const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util"); nacl.util = require("tweetnacl-util");
@@ -23,161 +24,158 @@ const client = new jsrp.client();
* @returns * @returns
*/ */
const attemptLogin = async ( const attemptLogin = async (
email, email,
password, password,
setErrorLogin, setErrorLogin,
router, router,
isSignUp, isSignUp,
isLogin isLogin
) => { ) => {
try { try {
let userWorkspace, userOrg; let userWorkspace, userOrg;
client.init( client.init(
{ {
username: email, username: email,
password: password, password: password,
}, },
async () => { async () => {
const clientPublicKey = client.getPublicKey(); const clientPublicKey = client.getPublicKey();
let serverPublicKey, salt; let serverPublicKey, salt;
try { try {
const res = await login1(email, clientPublicKey); const res = await login1(email, clientPublicKey);
res = await res.json(); res = await res.json();
serverPublicKey = res.serverPublicKey; serverPublicKey = res.serverPublicKey;
salt = res.salt; salt = res.salt;
} catch (err) { } catch (err) {
setErrorLogin(true); setErrorLogin(true);
console.log("Wrong password", err); console.log("Wrong password", err);
} }
let response; let response;
try { try {
client.setSalt(salt); client.setSalt(salt);
client.setServerPublicKey(serverPublicKey); client.setServerPublicKey(serverPublicKey);
const clientProof = client.getProof(); // called M1 const clientProof = client.getProof(); // called M1
response = await login2(email, clientProof); response = await login2(email, clientProof);
} catch (err) { } catch (err) {
setErrorLogin(true); setErrorLogin(true);
console.log("Password verification failed"); console.log("Password verification failed");
} }
// if everything works, go the main dashboard page. // if everything works, go the main dashboard page.
try { try {
if (response.status == "200") { if (response.status == "200") {
response = await response.json(); response = await response.json();
SecurityClient.setToken(response["token"]); SecurityClient.setToken(response["token"]);
const publicKey = response["publicKey"]; const publicKey = response["publicKey"];
const encryptedPrivateKey = const encryptedPrivateKey = response["encryptedPrivateKey"];
response["encryptedPrivateKey"]; const iv = response["iv"];
const iv = response["iv"]; const tag = response["tag"];
const tag = response["tag"];
const PRIVATE_KEY = Aes256Gcm.decrypt( const PRIVATE_KEY = Aes256Gcm.decrypt(
encryptedPrivateKey, encryptedPrivateKey,
iv, iv,
tag, tag,
password.slice(0, 32).padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), "0") password
); .slice(0, 32)
.padStart(
32 +
(password.slice(0, 32).length - new Blob([password]).size),
"0"
)
);
try { try {
localStorage.setItem("publicKey", publicKey); localStorage.setItem("publicKey", publicKey);
localStorage.setItem( localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey);
"encryptedPrivateKey", localStorage.setItem("iv", iv);
encryptedPrivateKey localStorage.setItem("tag", tag);
); localStorage.setItem("PRIVATE_KEY", PRIVATE_KEY);
localStorage.setItem("iv", iv); } catch (err) {
localStorage.setItem("tag", tag); setErrorLogin(true);
localStorage.setItem("PRIVATE_KEY", PRIVATE_KEY); console.error(
} catch (err) { "Unable to send the tokens in local storage:" + err.message
setErrorLogin(true); );
console.error( }
"Unable to send the tokens in local storage:" + } else {
err.message setErrorLogin(true);
); }
}
} else {
setErrorLogin(true);
}
const userOrgs = await getOrganizations(); const userOrgs = await getOrganizations();
const userOrgsData = userOrgs.map(org => org._id); const userOrgsData = userOrgs.map((org) => org._id);
let orgToLogin;
if (userOrgsData.includes(localStorage.getItem("orgData.id"))) {
orgToLogin = localStorage.getItem("orgData.id");
} else {
orgToLogin = userOrgsData[0];
localStorage.setItem("orgData.id", orgToLogin);
}
let orgUserProjects = await getOrganizationUserProjects({ let orgToLogin;
orgId: orgToLogin, if (userOrgsData.includes(localStorage.getItem("orgData.id"))) {
}); orgToLogin = localStorage.getItem("orgData.id");
} else {
orgUserProjects = orgUserProjects?.map(project => project._id); orgToLogin = userOrgsData[0];
let projectToLogin; localStorage.setItem("orgData.id", orgToLogin);
if (orgUserProjects.includes(localStorage.getItem("projectData.id"))) { }
projectToLogin = localStorage.getItem("projectData.id");
} else {
try {
projectToLogin = orgUserProjects[0];
localStorage.setItem("projectData.id", projectToLogin);
} catch (error) {
console.log("ERROR: User likely has no projects. ", error)
}
}
// If user is logging in for the first time, add the example keys let orgUserProjects = await getOrganizationUserProjects({
if (isSignUp) { orgId: orgToLogin,
await pushKeys( });
{
DATABASE_URL: [
"mongodb+srv://this_is:[email protected]",
"personal",
],
TWILIO_AUTH_TOKEN: [
"hgSIwDAKvz8PJfkj6xkzYqzGmAP3HLuG",
"shared",
],
WEBSITE_URL: [
"http://localhost:3000",
"shared",
],
STRIPE_SECRET_KEY: [
"sk_test_7348oyho4hfq398HIUOH78",
"shared",
],
},
projectToLogin,
"Development"
);
}
try {
if (email) {
if (ENV == "production") {
const posthog = initPostHog();
posthog.identify(email);
posthog.capture("User Logged In");
}
}
} catch (error) {
console.log("posthog", error);
}
if (isLogin) { orgUserProjects = orgUserProjects?.map((project) => project._id);
router.push("/dashboard/"); let projectToLogin;
} if (
} catch (error) { orgUserProjects.includes(localStorage.getItem("projectData.id"))
setErrorLogin(true); ) {
console.log("Login response not available"); projectToLogin = localStorage.getItem("projectData.id");
} } else {
} try {
); projectToLogin = orgUserProjects[0];
} catch (error) { localStorage.setItem("projectData.id", projectToLogin);
console.log("Something went wrong during authentication"); } catch (error) {
} console.log("ERROR: User likely has no projects. ", error);
return true; }
}
// If user is logging in for the first time, add the example keys
if (isSignUp) {
await pushKeys(
{
DATABASE_URL: [
"mongodb+srv://this_is:[email protected]",
"personal",
],
TWILIO_AUTH_TOKEN: [
"hgSIwDAKvz8PJfkj6xkzYqzGmAP3HLuG",
"shared",
],
WEBSITE_URL: ["http://localhost:3000", "shared"],
STRIPE_SECRET_KEY: ["sk_test_7348oyho4hfq398HIUOH78", "shared"],
},
projectToLogin,
"Development"
);
}
try {
if (email) {
if (ENV == "production") {
const posthog = initPostHog();
posthog.identify(email);
posthog.capture("User Logged In");
}
}
} catch (error) {
console.log("posthog", error);
}
if (isLogin) {
router.push("/dashboard/");
}
} catch (error) {
setErrorLogin(true);
console.log("Login response not available");
}
}
);
} catch (error) {
console.log("Something went wrong during authentication");
}
return true;
}; };
export default attemptLogin; export default attemptLogin;
@@ -1,109 +0,0 @@
import Aes256Gcm from "../aes-256-gcm";
import SRP1 from "../../pages/api/auth/SRP1";
import changePassword2 from "../../pages/api/auth/ChangePassword2";
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
const jsrp = require("jsrp");
const clientOldPassword = new jsrp.client();
const clientNewPassword = new jsrp.client();
/**
* This function loggs in the user (whether it's right after signup, or a normal login)
* @param {*} email
* @param {*} password
* @param {*} setErrorLogin
* @param {*} router
* @param {*} isSignUp
* @returns
*/
const changePassword = async (
email,
currentPassword,
newPassword,
setCurrentPasswordError,
setPasswordChanged,
setCurrentPassword,
setNewPassword
) => {
try {
setPasswordChanged(false);
setCurrentPasswordError(false);
clientOldPassword.init(
{
username: email,
password: currentPassword,
},
async () => {
const clientPublicKey = clientOldPassword.getPublicKey();
let serverPublicKey, salt;
try {
const res = await SRP1({
clientPublicKey: clientPublicKey
});
serverPublicKey = res.serverPublicKey;
salt = res.salt;
} catch (err) {
setCurrentPasswordError(true);
console.log("Wrong current password", err, 1);
}
clientOldPassword.setSalt(salt);
clientOldPassword.setServerPublicKey(serverPublicKey);
const clientProof = clientOldPassword.getProof(); // called M1
clientNewPassword.init({
username: email,
password: newPassword
}, async () => {
clientNewPassword.createVerifier(async (err, result) => {
let { ciphertext, iv, tag } = Aes256Gcm.encrypt(
localStorage.getItem("PRIVATE_KEY"),
newPassword.slice(0, 32).padStart(32 + (newPassword.slice(0, 32).length - new Blob([newPassword]).size), "0")
);
if (ciphertext) {
localStorage.setItem(
"encryptedPrivateKey",
ciphertext
);
localStorage.setItem("iv", iv);
localStorage.setItem("tag", tag);
let res;
try {
res = await changePassword2({
encryptedPrivateKey: ciphertext,
iv,
tag,
salt: result.salt,
verifier: result.verifier,
clientProof
});
if (res.status == 400) {
setCurrentPasswordError(true);
} else if (res.status == 200) {
setPasswordChanged(true);
setCurrentPassword("");
setNewPassword("");
}
} catch (err) {
setCurrentPasswordError(true)
console.log(err);
}
}
});
});
}
);
} catch (error) {
console.log("Something went wrong during changing the password", slat, serverPublicKey);
}
return true;
};
export default changePassword;
@@ -7,59 +7,59 @@
* @returns * @returns
*/ */
const passwordCheck = ( const passwordCheck = (
password, password,
setPasswordErrorLength, setPasswordErrorLength,
setPasswordErrorNumber, setPasswordErrorNumber,
setPasswordErrorLowerCase, setPasswordErrorLowerCase,
currentErrorCheck currentErrorCheck
) => { ) => {
let errorCheck = currentErrorCheck; let errorCheck = currentErrorCheck;
if (!password || password.length < 14) { if (!password || password.length < 14) {
setPasswordErrorLength(true); setPasswordErrorLength(true);
errorCheck = true; errorCheck = true;
} else { } else {
setPasswordErrorLength(false); setPasswordErrorLength(false);
} }
if (!/\d/.test(password)) { if (!/\d/.test(password)) {
setPasswordErrorNumber(true); setPasswordErrorNumber(true);
errorCheck = true; errorCheck = true;
} else { } else {
setPasswordErrorNumber(false); setPasswordErrorNumber(false);
} }
if (!/[a-z]/.test(password)) { if (!/[a-z]/.test(password)) {
setPasswordErrorLowerCase(true); setPasswordErrorLowerCase(true);
errorCheck = true; errorCheck = true;
// } else if (/(.)(?:(?!\1).){1,2}/.test(password)) { // } else if (/(.)(?:(?!\1).){1,2}/.test(password)) {
// console.log(111) // console.log(111)
// setPasswordError(true); // setPasswordError(true);
// setPasswordErrorMessage("Password should not contain repeating characters."); // setPasswordErrorMessage("Password should not contain repeating characters.");
// errorCheck = true; // errorCheck = true;
// } else if (RegExp(`[${email}]`).test(password)) { // } else if (RegExp(`[${email}]`).test(password)) {
// console.log(222) // console.log(222)
// setPasswordError(true); // setPasswordError(true);
// setPasswordErrorMessage("Password should not contain your email."); // setPasswordErrorMessage("Password should not contain your email.");
// errorCheck = true; // errorCheck = true;
} else { } else {
setPasswordErrorLowerCase(false); setPasswordErrorLowerCase(false);
} }
// if (!/[A-Z]/.test(password)) { // if (!/[A-Z]/.test(password)) {
// setPasswordErrorUpperCase(true); // setPasswordErrorUpperCase(true);
// errorCheck = true; // errorCheck = true;
// } else { // } else {
// setPasswordErrorUpperCase(false); // setPasswordErrorUpperCase(false);
// } // }
// if (!/(?=.*[!@#$%^&*])/.test(password)) { // if (!/(?=.*[!@#$%^&*])/.test(password)) {
// setPasswordErrorSpecialChar(true); // setPasswordErrorSpecialChar(true);
// // "Please add at least 1 special character (*, !, #, %)." // // "Please add at least 1 special character (*, !, #, %)."
// errorCheck = true; // errorCheck = true;
// } else { // } else {
// setPasswordErrorSpecialChar(false); // setPasswordErrorSpecialChar(false);
// } // }
return errorCheck; return errorCheck;
}; };
export default passwordCheck; export default passwordCheck;
+11 -9
View File
@@ -1,15 +1,17 @@
const ENV = process.env.NEXT_PUBLIC_ENV! || 'development'; // investigate const ENV = process.env.NEXT_PUBLIC_ENV! || "development"; // investigate
const POSTHOG_API_KEY = process.env.NEXT_PUBLIC_POSTHOG_API_KEY!; const POSTHOG_API_KEY = process.env.NEXT_PUBLIC_POSTHOG_API_KEY!;
const POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST! || 'https://app.posthog.com'; const POSTHOG_HOST =
process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com";
const STRIPE_PRODUCT_PRO = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_PRO!; const STRIPE_PRODUCT_PRO = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_PRO!;
const STRIPE_PRODUCT_STARTER = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_STARTER!; const STRIPE_PRODUCT_STARTER = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_STARTER!;
const TELEMETRY_ENABLED = (process.env.NEXT_PUBLIC_TELEMETRY_ENABLED! !== 'false'); const TELEMETRY_ENABLED =
process.env.NEXT_PUBLIC_TELEMETRY_ENABLED! !== "false";
export { export {
ENV, ENV,
POSTHOG_API_KEY, POSTHOG_API_KEY,
POSTHOG_HOST, POSTHOG_HOST,
STRIPE_PRODUCT_PRO, STRIPE_PRODUCT_PRO,
STRIPE_PRODUCT_STARTER, STRIPE_PRODUCT_STARTER,
TELEMETRY_ENABLED TELEMETRY_ENABLED,
}; };
@@ -0,0 +1,63 @@
/**
* @fileoverview Provides easy encryption/decryption methods using AES 256 GCM.
*/
"use strict";
const crypto = require("crypto");
const ALGORITHM = "aes-256-gcm";
const BLOCK_SIZE_BYTES = 16; // 128 bit
/**
* Provides easy encryption/decryption methods using AES 256 GCM.
*/
class Aes256Gcm {
/**
* No need to run the constructor. The class only has static methods.
*/
constructor() {}
/**
* Encrypts text with AES 256 GCM.
* @param {string} text - Cleartext to encode.
* @param {string} secret - Shared secret key, must be 32 bytes.
* @returns {object}
*/
static encrypt(text, secret) {
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
let ciphertext = cipher.update(text, "utf8", "base64");
ciphertext += cipher.final("base64");
return {
ciphertext,
iv: iv.toString("base64"),
tag: cipher.getAuthTag().toString("base64"),
};
}
/**
* Decrypts AES 256 CGM encrypted text.
* @param {string} ciphertext - Base64-encoded ciphertext.
* @param {string} iv - The base64-encoded initialization vector.
* @param {string} tag - The base64-encoded authentication tag generated by getAuthTag().
* @param {string} secret - Shared secret key, must be 32 bytes.
* @returns {string}
*/
static decrypt(ciphertext, iv, tag, secret) {
const decipher = crypto.createDecipheriv(
ALGORITHM,
secret,
Buffer.from(iv, "base64")
);
decipher.setAuthTag(Buffer.from(tag, "base64"));
let cleartext = decipher.update(ciphertext, "base64", "utf8");
cleartext += decipher.final("utf8");
return cleartext;
}
}
module.exports = Aes256Gcm;
@@ -0,0 +1,120 @@
import changePassword2 from "~/pages/api/auth/ChangePassword2";
import SRP1 from "~/pages/api/auth/SRP1";
import Aes256Gcm from "./aes-256-gcm";
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
const jsrp = require("jsrp");
const clientOldPassword = new jsrp.client();
const clientNewPassword = new jsrp.client();
/**
* This function loggs in the user (whether it's right after signup, or a normal login)
* @param {*} email
* @param {*} password
* @param {*} setErrorLogin
* @param {*} router
* @param {*} isSignUp
* @returns
*/
const changePassword = async (
email,
currentPassword,
newPassword,
setCurrentPasswordError,
setPasswordChanged,
setCurrentPassword,
setNewPassword
) => {
try {
setPasswordChanged(false);
setCurrentPasswordError(false);
clientOldPassword.init(
{
username: email,
password: currentPassword,
},
async () => {
const clientPublicKey = clientOldPassword.getPublicKey();
let serverPublicKey, salt;
try {
const res = await SRP1({
clientPublicKey: clientPublicKey,
});
serverPublicKey = res.serverPublicKey;
salt = res.salt;
} catch (err) {
setCurrentPasswordError(true);
console.log("Wrong current password", err, 1);
}
clientOldPassword.setSalt(salt);
clientOldPassword.setServerPublicKey(serverPublicKey);
const clientProof = clientOldPassword.getProof(); // called M1
clientNewPassword.init(
{
username: email,
password: newPassword,
},
async () => {
clientNewPassword.createVerifier(async (err, result) => {
// The Blob part here is needed to account for symbols that count as 2+ bytes (e.g., é, å, ø)
let { ciphertext, iv, tag } = Aes256Gcm.encrypt(
localStorage.getItem("PRIVATE_KEY"),
newPassword
.slice(0, 32)
.padStart(
32 +
(newPassword.slice(0, 32).length -
new Blob([newPassword]).size),
"0"
)
);
if (ciphertext) {
localStorage.setItem("encryptedPrivateKey", ciphertext);
localStorage.setItem("iv", iv);
localStorage.setItem("tag", tag);
let res;
try {
res = await changePassword2({
encryptedPrivateKey: ciphertext,
iv,
tag,
salt: result.salt,
verifier: result.verifier,
clientProof,
});
if (res.status == 400) {
setCurrentPasswordError(true);
} else if (res.status == 200) {
setPasswordChanged(true);
setCurrentPassword("");
setNewPassword("");
}
} catch (err) {
setCurrentPasswordError(true);
console.log(err);
}
}
});
}
);
}
);
} catch (error) {
console.log(
"Something went wrong during changing the password",
slat,
serverPublicKey
);
}
return true;
};
export default changePassword;
@@ -1,6 +1,6 @@
const nacl = require("tweetnacl"); const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util"); nacl.util = require("tweetnacl-util");
const aes = require("../aes-256-gcm"); const aes = require("./aes-256-gcm");
/** /**
* Return assymmetrically encrypted [plaintext] using [publicKey] where * Return assymmetrically encrypted [plaintext] using [publicKey] where
@@ -14,18 +14,18 @@ const aes = require("../aes-256-gcm");
* @returns {String} nonce - base64-encoded nonce * @returns {String} nonce - base64-encoded nonce
*/ */
const encryptAssymmetric = ({ plaintext, publicKey, privateKey }) => { const encryptAssymmetric = ({ plaintext, publicKey, privateKey }) => {
const nonce = nacl.randomBytes(24); const nonce = nacl.randomBytes(24);
const ciphertext = nacl.box( const ciphertext = nacl.box(
nacl.util.decodeUTF8(plaintext), nacl.util.decodeUTF8(plaintext),
nonce, nonce,
nacl.util.decodeBase64(publicKey), nacl.util.decodeBase64(publicKey),
nacl.util.decodeBase64(privateKey) nacl.util.decodeBase64(privateKey)
); );
return { return {
ciphertext: nacl.util.encodeBase64(ciphertext), ciphertext: nacl.util.encodeBase64(ciphertext),
nonce: nacl.util.encodeBase64(nonce), nonce: nacl.util.encodeBase64(nonce),
}; };
}; };
/** /**
@@ -39,14 +39,14 @@ const encryptAssymmetric = ({ plaintext, publicKey, privateKey }) => {
* @param {String} plaintext - UTF8 plaintext * @param {String} plaintext - UTF8 plaintext
*/ */
const decryptAssymmetric = ({ ciphertext, nonce, publicKey, privateKey }) => { const decryptAssymmetric = ({ ciphertext, nonce, publicKey, privateKey }) => {
const plaintext = nacl.box.open( const plaintext = nacl.box.open(
nacl.util.decodeBase64(ciphertext), nacl.util.decodeBase64(ciphertext),
nacl.util.decodeBase64(nonce), nacl.util.decodeBase64(nonce),
nacl.util.decodeBase64(publicKey), nacl.util.decodeBase64(publicKey),
nacl.util.decodeBase64(privateKey) nacl.util.decodeBase64(privateKey)
); );
return nacl.util.encodeUTF8(plaintext); return nacl.util.encodeUTF8(plaintext);
}; };
/** /**
@@ -56,23 +56,23 @@ const decryptAssymmetric = ({ ciphertext, nonce, publicKey, privateKey }) => {
* @param {String} obj.key - 16-byte hex key * @param {String} obj.key - 16-byte hex key
*/ */
const encryptSymmetric = ({ plaintext, key }) => { const encryptSymmetric = ({ plaintext, key }) => {
let ciphertext, iv, tag; let ciphertext, iv, tag;
try { try {
const obj = aes.encrypt(plaintext, key); const obj = aes.encrypt(plaintext, key);
ciphertext = obj.ciphertext; ciphertext = obj.ciphertext;
iv = obj.iv; iv = obj.iv;
tag = obj.tag; tag = obj.tag;
} catch (err) { } catch (err) {
console.log("Failed to perform encryption"); console.log("Failed to perform encryption");
console.log(err); console.log(err);
process.exit(1); process.exit(1);
} }
return { return {
ciphertext, ciphertext,
iv, iv,
tag, tag,
}; };
}; };
/** /**
@@ -86,20 +86,20 @@ const encryptSymmetric = ({ plaintext, key }) => {
* *
*/ */
const decryptSymmetric = ({ ciphertext, iv, tag, key }) => { const decryptSymmetric = ({ ciphertext, iv, tag, key }) => {
let plaintext; let plaintext;
try { try {
plaintext = aes.decrypt(ciphertext, iv, tag, key); plaintext = aes.decrypt(ciphertext, iv, tag, key);
} catch (err) { } catch (err) {
console.log("Failed to perform decryption"); console.log("Failed to perform decryption");
process.exit(1); process.exit(1);
} }
return plaintext; return plaintext;
}; };
module.exports = { module.exports = {
encryptAssymmetric, encryptAssymmetric,
decryptAssymmetric, decryptAssymmetric,
encryptSymmetric, encryptSymmetric,
decryptSymmetric, decryptSymmetric,
}; };
@@ -0,0 +1,98 @@
import issueBackupPrivateKey from "~/pages/api/auth/IssueBackupPrivateKey";
import SRP1 from "~/pages/api/auth/SRP1";
import generateBackupPDF from "../generateBackupPDF";
import Aes256Gcm from "./aes-256-gcm";
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
const jsrp = require("jsrp");
const clientPassword = new jsrp.client();
const clientKey = new jsrp.client();
const crypto = require("crypto");
/**
* This function loggs in the user (whether it's right after signup, or a normal login)
* @param {*} email
* @param {*} password
* @param {*} setErrorLogin
* @param {*} router
* @param {*} isSignUp
* @returns
*/
const issueBackupKey = async ({
email,
password,
personalName,
setBackupKeyError,
setBackupKeyIssued,
}) => {
try {
setBackupKeyError(false);
setBackupKeyIssued(false);
clientPassword.init(
{
username: email,
password: password,
},
async () => {
const clientPublicKey = clientPassword.getPublicKey();
let serverPublicKey, salt;
try {
const res = await SRP1({
clientPublicKey: clientPublicKey,
});
serverPublicKey = res.serverPublicKey;
salt = res.salt;
} catch (err) {
setBackupKeyError(true);
console.log("Wrong current password", err, 1);
}
clientPassword.setSalt(salt);
clientPassword.setServerPublicKey(serverPublicKey);
const clientProof = clientPassword.getProof(); // called M1
const generatedKey = crypto.randomBytes(16).toString("hex");
clientKey.init(
{
username: email,
password: generatedKey,
},
async () => {
clientKey.createVerifier(async (err, result) => {
let { ciphertext, iv, tag } = Aes256Gcm.encrypt(
localStorage.getItem("PRIVATE_KEY"),
generatedKey
);
const res = await issueBackupPrivateKey({
encryptedPrivateKey: ciphertext,
iv,
tag,
salt: result.salt,
verifier: result.verifier,
clientProof,
});
if (res.status == 400) {
setBackupKeyError(true);
} else if (res.status == 200) {
generateBackupPDF(personalName, email, generatedKey);
setBackupKeyIssued(true);
}
});
}
);
}
);
} catch (error) {
setBackupKeyError(true);
console.log("Failed to issue a backup key");
}
return true;
};
export default issueBackupKey;
+26 -26
View File
@@ -1,5 +1,5 @@
const LINE = const LINE =
/(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm; /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm;
/** /**
* Return text that is the buffer parsed * Return text that is the buffer parsed
@@ -7,41 +7,41 @@ const LINE =
* @returns {String} text - text of buffer * @returns {String} text - text of buffer
*/ */
function parse(src) { function parse(src) {
const obj = {}; const obj = {};
// Convert buffer to string // Convert buffer to string
let lines = src.toString(); let lines = src.toString();
// Convert line breaks to same format // Convert line breaks to same format
lines = lines.replace(/\r\n?/gm, "\n"); lines = lines.replace(/\r\n?/gm, "\n");
let match; let match;
while ((match = LINE.exec(lines)) != null) { while ((match = LINE.exec(lines)) != null) {
const key = match[1]; const key = match[1];
// Default undefined or null to empty string // Default undefined or null to empty string
let value = match[2] || ""; let value = match[2] || "";
// Remove whitespace // Remove whitespace
value = value.trim(); value = value.trim();
// Check if double quoted // Check if double quoted
const maybeQuote = value[0]; const maybeQuote = value[0];
// Remove surrounding quotes // Remove surrounding quotes
value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2"); value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2");
// Expand newlines if double quoted // Expand newlines if double quoted
if (maybeQuote === '"') { if (maybeQuote === '"') {
value = value.replace(/\\n/g, "\n"); value = value.replace(/\\n/g, "\n");
value = value.replace(/\\r/g, "\r"); value = value.replace(/\\r/g, "\r");
} }
// Add to object // Add to object
obj[key] = value; obj[key] = value;
} }
return obj; return obj;
} }
export default parse; export default parse;
File diff suppressed because one or more lines are too long
@@ -1,106 +0,0 @@
import getSecrets from "../../pages/api/files/GetSecrets";
import guidGenerator from "./randomId";
const {
decryptAssymmetric,
decryptSymmetric,
} = require("../../components/utilities/crypto");
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
const envMapping = {
Development: "dev",
Staging: "staging",
Production: "prod",
Testing: "test",
};
const getSecretsForProject = async ({
env,
setFileState,
setIsKeyAvailable,
setData,
workspaceId
}) => {
try {
let file;
try {
file = await getSecrets(
workspaceId,
envMapping[env]
);
setFileState(file);
} catch (error) {
console.log("ERROR: Not able to access the latest file");
}
// This is called isKeyAvilable but what it really means is if a person is able to create new key pairs
setIsKeyAvailable(
!file.key ? (file.secrets.length == 0 ? true : false) : true
);
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
let tempFileState = [];
if (file.key) {
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: file.key.encryptedKey,
nonce: file.key.nonce,
publicKey: file.key.sender.publicKey,
privateKey: PRIVATE_KEY,
});
file.secrets.map((secretPair) => {
// decrypt .env file with symmetric key
const plainTextKey = decryptSymmetric({
ciphertext: secretPair.secretKey.ciphertext,
iv: secretPair.secretKey.iv,
tag: secretPair.secretKey.tag,
key,
});
const plainTextValue = decryptSymmetric({
ciphertext: secretPair.secretValue.ciphertext,
iv: secretPair.secretValue.iv,
tag: secretPair.secretValue.tag,
key,
});
tempFileState.push({
key: plainTextKey,
value: plainTextValue,
type: secretPair.type,
});
});
}
setFileState(tempFileState);
setData(
tempFileState.map((line, index) => [
guidGenerator(),
index,
line["key"],
line["value"],
line["type"],
])
// .sort((a, b) =>
// sortMethod == "alphabetical"
// ? a[2].localeCompare(b[2])
// : b[2].localeCompare(a[2])
// )
);
return tempFileState.map((line, index) => [
guidGenerator(),
index,
line["key"],
line["value"],
line["type"],
]);
} catch (error) {
console.log("Something went wrong during accessing or decripting secrets.");
}
return true;
};
export default getSecretsForProject;
@@ -1,96 +0,0 @@
import Aes256Gcm from "../aes-256-gcm";
import SRP1 from "../../pages/api/auth/SRP1";
import issueBackupPrivateKey from "../../pages/api/auth/IssueBackupPrivateKey";
import generateBackupPDF from "./generateBackupPDF";
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
const jsrp = require("jsrp");
const clientPassword = new jsrp.client();
const clientKey = new jsrp.client();
const crypto = require("crypto");
/**
* This function loggs in the user (whether it's right after signup, or a normal login)
* @param {*} email
* @param {*} password
* @param {*} setErrorLogin
* @param {*} router
* @param {*} isSignUp
* @returns
*/
const issueBackupKey = async ({
email,
password,
personalName,
setBackupKeyError,
setBackupKeyIssued
}) => {
try {
setBackupKeyError(false);
setBackupKeyIssued(false);
clientPassword.init(
{
username: email,
password: password,
},
async () => {
const clientPublicKey = clientPassword.getPublicKey();
let serverPublicKey, salt;
try {
const res = await SRP1({
clientPublicKey: clientPublicKey
});
serverPublicKey = res.serverPublicKey;
salt = res.salt;
} catch (err) {
setBackupKeyError(true);
console.log("Wrong current password", err, 1);
}
clientPassword.setSalt(salt);
clientPassword.setServerPublicKey(serverPublicKey);
const clientProof = clientPassword.getProof(); // called M1
const generatedKey = crypto.randomBytes(16).toString("hex");
clientKey.init({
username: email,
password: generatedKey
}, async () => {
clientKey.createVerifier(async (err, result) => {
let { ciphertext, iv, tag } = Aes256Gcm.encrypt(
localStorage.getItem("PRIVATE_KEY"),
generatedKey
);
const res = await issueBackupPrivateKey({
encryptedPrivateKey: ciphertext,
iv,
tag,
salt: result.salt,
verifier: result.verifier,
clientProof
});
if (res.status == 400) {
setBackupKeyError(true);
} else if (res.status == 200) {
generateBackupPDF(personalName, email, generatedKey);
setBackupKeyIssued(true);
}
});
})
}
);
} catch (error) {
setBackupKeyError(true);
console.log("Failed to issue a backup key");
}
return true;
};
export default issueBackupKey;
-109
View File
@@ -1,109 +0,0 @@
import getLatestFileKey from "../../pages/api/workspace/getLatestFileKey";
import getWorkspaceKeys from "../../pages/api/workspace/getWorkspaceKeys";
import uploadSecrets from "../../pages/api/files/UploadSecrets";
const crypto = require("crypto");
const {
decryptAssymmetric,
decryptSymmetric,
encryptSymmetric,
encryptAssymmetric,
} = require("../../components/utilities/crypto");
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
const envMapping = {
Development: "dev",
Staging: "staging",
Production: "prod",
Testing: "test",
};
const pushKeys = async (obj, workspaceId, env) => {
let sharedKey = await getLatestFileKey(workspaceId);
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
let randomBytes;
if (Object.keys(sharedKey).length > 0) {
// case: a (shared) key exists for the workspace
randomBytes = decryptAssymmetric({
ciphertext: sharedKey.latestKey.encryptedKey,
nonce: sharedKey.latestKey.nonce,
publicKey: sharedKey.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY,
});
} else {
// case: a (shared) key does not exist for the workspace
randomBytes = crypto.randomBytes(16).toString("hex");
}
const secrets = Object.keys(obj).map((key) => {
// encrypt key
const {
ciphertext: ciphertextKey,
iv: ivKey,
tag: tagKey,
} = encryptSymmetric({
plaintext: key,
key: randomBytes,
});
// encrypt value
const {
ciphertext: ciphertextValue,
iv: ivValue,
tag: tagValue,
} = encryptSymmetric({
plaintext: obj[key][0],
key: randomBytes,
});
const visibility = obj[key][1] != null ? obj[key][1] : "personal";
return {
ciphertextKey,
ivKey,
tagKey,
hashKey: crypto.createHash("sha256").update(key).digest("hex"),
ciphertextValue,
ivValue,
tagValue,
hashValue: crypto
.createHash("sha256")
.update(obj[key][0])
.digest("hex"),
type: visibility,
};
});
// obtain public keys of all receivers (i.e. members in workspace)
const publicKeys = await getWorkspaceKeys({
workspaceId,
});
// assymmetrically encrypt key with each receiver public keys
const keys = publicKeys.map((k) => {
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey: k.publicKey,
privateKey: PRIVATE_KEY,
});
return {
encryptedKey: ciphertext,
nonce,
userId: k.userId,
};
});
// send payload
await uploadSecrets({
workspaceId,
secrets,
keys,
environment: envMapping[env],
});
};
export default pushKeys;
@@ -1,77 +0,0 @@
import publicKeyInfical from "../../pages/api/auth/publicKeyInfisical"
import changeHerokuConfigVars from "../../pages/api/integrations/ChangeHerokuConfigVars";
const crypto = require("crypto");
const {
encryptSymmetric,
encryptAssymmetric,
} = require("./crypto");
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
const pushKeysIntegration = async ({ obj, integrationId }) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
let randomBytes = crypto.randomBytes(16).toString("hex");
const secrets = Object.keys(obj).map((key) => {
// encrypt key
const {
ciphertext: ciphertextKey,
iv: ivKey,
tag: tagKey,
} = encryptSymmetric({
plaintext: key,
key: randomBytes,
});
// encrypt value
const {
ciphertext: ciphertextValue,
iv: ivValue,
tag: tagValue,
} = encryptSymmetric({
plaintext: obj[key],
key: randomBytes,
});
const visibility = "shared";
return {
ciphertextKey,
ivKey,
tagKey,
hashKey: crypto.createHash("sha256").update(key).digest("hex"),
ciphertextValue,
ivValue,
tagValue,
hashValue: crypto
.createHash("sha256")
.update(obj[key])
.digest("hex"),
type: visibility,
};
});
// obtain public keys of all receivers (i.e. members in workspace)
let publicKeyInfisical = await publicKeyInfical();
publicKeyInfisical = (await publicKeyInfisical.json()).publicKey;
// assymmetrically encrypt key with each receiver public keys
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey: publicKeyInfisical,
privateKey: PRIVATE_KEY,
});
const key = {
encryptedKey: ciphertext,
nonce,
};
changeHerokuConfigVars({integrationId, key, secrets})
};
export default pushKeysIntegration;
+17 -17
View File
@@ -3,23 +3,23 @@
* @returns * @returns
*/ */
const guidGenerator = () => { const guidGenerator = () => {
var S4 = function () { var S4 = function () {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
}; };
return ( return (
S4() + S4() +
S4() + S4() +
"-" + "-" +
S4() + S4() +
"-" + "-" +
S4() + S4() +
"-" + "-" +
S4() + S4() +
"-" + "-" +
S4() + S4() +
S4() + S4() +
S4() S4()
); );
}; };
export default guidGenerator; export default guidGenerator;
@@ -0,0 +1,103 @@
import getSecrets from "~/pages/api/files/GetSecrets";
import guidGenerator from "../randomId";
const {
decryptAssymmetric,
decryptSymmetric,
} = require("../cryptography/crypto");
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
const envMapping = {
Development: "dev",
Staging: "staging",
Production: "prod",
Testing: "test",
};
const getSecretsForProject = async ({
env,
setFileState,
setIsKeyAvailable,
setData,
workspaceId,
}) => {
try {
let file;
try {
file = await getSecrets(workspaceId, envMapping[env]);
setFileState(file);
} catch (error) {
console.log("ERROR: Not able to access the latest file");
}
// This is called isKeyAvilable but what it really means is if a person is able to create new key pairs
setIsKeyAvailable(
!file.key ? (file.secrets.length == 0 ? true : false) : true
);
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
let tempFileState = [];
if (file.key) {
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: file.key.encryptedKey,
nonce: file.key.nonce,
publicKey: file.key.sender.publicKey,
privateKey: PRIVATE_KEY,
});
file.secrets.map((secretPair) => {
// decrypt .env file with symmetric key
const plainTextKey = decryptSymmetric({
ciphertext: secretPair.secretKey.ciphertext,
iv: secretPair.secretKey.iv,
tag: secretPair.secretKey.tag,
key,
});
const plainTextValue = decryptSymmetric({
ciphertext: secretPair.secretValue.ciphertext,
iv: secretPair.secretValue.iv,
tag: secretPair.secretValue.tag,
key,
});
tempFileState.push({
key: plainTextKey,
value: plainTextValue,
type: secretPair.type,
});
});
}
setFileState(tempFileState);
setData(
tempFileState.map((line, index) => [
guidGenerator(),
index,
line["key"],
line["value"],
line["type"],
])
// .sort((a, b) =>
// sortMethod == "alphabetical"
// ? a[2].localeCompare(b[2])
// : b[2].localeCompare(a[2])
// )
);
return tempFileState.map((line, index) => [
guidGenerator(),
index,
line["key"],
line["value"],
line["type"],
]);
} catch (error) {
console.log("Something went wrong during accessing or decripting secrets.");
}
return true;
};
export default getSecretsForProject;
@@ -0,0 +1,106 @@
import uploadSecrets from "~/pages/api/files/UploadSecrets";
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
import getWorkspaceKeys from "~/pages/api/workspace/getWorkspaceKeys";
const crypto = require("crypto");
const {
decryptAssymmetric,
decryptSymmetric,
encryptSymmetric,
encryptAssymmetric,
} = require("../cryptography/crypto");
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
const envMapping = {
Development: "dev",
Staging: "staging",
Production: "prod",
Testing: "test",
};
const pushKeys = async (obj, workspaceId, env) => {
let sharedKey = await getLatestFileKey(workspaceId);
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
let randomBytes;
if (Object.keys(sharedKey).length > 0) {
// case: a (shared) key exists for the workspace
randomBytes = decryptAssymmetric({
ciphertext: sharedKey.latestKey.encryptedKey,
nonce: sharedKey.latestKey.nonce,
publicKey: sharedKey.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY,
});
} else {
// case: a (shared) key does not exist for the workspace
randomBytes = crypto.randomBytes(16).toString("hex");
}
const secrets = Object.keys(obj).map((key) => {
// encrypt key
const {
ciphertext: ciphertextKey,
iv: ivKey,
tag: tagKey,
} = encryptSymmetric({
plaintext: key,
key: randomBytes,
});
// encrypt value
const {
ciphertext: ciphertextValue,
iv: ivValue,
tag: tagValue,
} = encryptSymmetric({
plaintext: obj[key][0],
key: randomBytes,
});
const visibility = obj[key][1] != null ? obj[key][1] : "personal";
return {
ciphertextKey,
ivKey,
tagKey,
hashKey: crypto.createHash("sha256").update(key).digest("hex"),
ciphertextValue,
ivValue,
tagValue,
hashValue: crypto.createHash("sha256").update(obj[key][0]).digest("hex"),
type: visibility,
};
});
// obtain public keys of all receivers (i.e. members in workspace)
const publicKeys = await getWorkspaceKeys({
workspaceId,
});
// assymmetrically encrypt key with each receiver public keys
const keys = publicKeys.map((k) => {
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey: k.publicKey,
privateKey: PRIVATE_KEY,
});
return {
encryptedKey: ciphertext,
nonce,
userId: k.userId,
};
});
// send payload
await uploadSecrets({
workspaceId,
secrets,
keys,
environment: envMapping[env],
});
};
export default pushKeys;
@@ -0,0 +1,74 @@
import publicKeyInfical from "~/pages/api/auth/publicKeyInfisical";
import changeHerokuConfigVars from "~/pages/api/integrations/ChangeHerokuConfigVars";
const crypto = require("crypto");
const {
encryptSymmetric,
encryptAssymmetric,
} = require("../cryptography/crypto");
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
const pushKeysIntegration = async ({ obj, integrationId }) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
let randomBytes = crypto.randomBytes(16).toString("hex");
const secrets = Object.keys(obj).map((key) => {
// encrypt key
const {
ciphertext: ciphertextKey,
iv: ivKey,
tag: tagKey,
} = encryptSymmetric({
plaintext: key,
key: randomBytes,
});
// encrypt value
const {
ciphertext: ciphertextValue,
iv: ivValue,
tag: tagValue,
} = encryptSymmetric({
plaintext: obj[key],
key: randomBytes,
});
const visibility = "shared";
return {
ciphertextKey,
ivKey,
tagKey,
hashKey: crypto.createHash("sha256").update(key).digest("hex"),
ciphertextValue,
ivValue,
tagValue,
hashValue: crypto.createHash("sha256").update(obj[key]).digest("hex"),
type: visibility,
};
});
// obtain public keys of all receivers (i.e. members in workspace)
let publicKeyInfisical = await publicKeyInfical();
publicKeyInfisical = (await publicKeyInfisical.json()).publicKey;
// assymmetrically encrypt key with each receiver public keys
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey: publicKeyInfisical,
privateKey: PRIVATE_KEY,
});
const key = {
encryptedKey: ciphertext,
nonce,
};
changeHerokuConfigVars({ integrationId, key, secrets });
};
export default pushKeysIntegration;
+16 -16
View File
@@ -1,20 +1,20 @@
export const PATH = process.env.NEXT_PUBLIC_WEBSITE_URL; export const PATH = process.env.NEXT_PUBLIC_WEBSITE_URL;
export const publicPaths = [ export const publicPaths = [
`/`, `/`,
// `/integrations`, // `/integrations`,
`/signupinvite`, `/signupinvite`,
`/pricing`, `/pricing`,
`/signup`, `/signup`,
`/login`, `/login`,
`/blog`, `/blog`,
`/docs`, `/docs`,
`/changelog`, `/changelog`,
`/security`, `/security`,
`/scheduledemo`, `/scheduledemo`,
`/blog/[slug]`, `/blog/[slug]`,
`/faq`, `/faq`,
`/privacy`, `/privacy`,
`/terms`, `/terms`,
`/subprocessors`, `/subprocessors`,
]; ];
+133 -133
View File
@@ -1,135 +1,135 @@
{ {
"common": { "common": {
"error_project-already-exists": "A project with this name already exists.", "error_project-already-exists": "A project with this name already exists.",
"need-login": " To use Infisical, please log in through a device with larger dimensions. ", "no-mobile": " To use Infisical, please log in through a device with larger dimensions. ",
"email": "Email", "email": "Email",
"password": "Password", "password": "Password",
"verify": "Verify", "verify": "Verify",
"first-name": "First Name", "first-name": "First Name",
"last-name": "Last Name", "last-name": "Last Name",
"logout": "Log Out", "logout": "Log Out",
"validate-required": "Please input your {{name}}", "validate-required": "Please input your {{name}}",
"password-validate": "Password should contain at least:", "password-validate": "Password should contain at least:",
"password-validate-length": "14 characters", "password-validate-length": "14 characters",
"password-validate-case": "1 lowercase character", "password-validate-case": "1 lowercase character",
"password-validate-number": "1 number", "password-validate-number": "1 number",
"maintenance-alert": "We are experiencing minor technical difficulties. We are working on solving it right now. Please come back in a few minutes.", "maintenance-alert": "We are experiencing minor technical difficulties. We are working on solving it right now. Please come back in a few minutes.",
"click-to-copy": "Click to copy", "click-to-copy": "Click to copy",
"project-id": "Project ID", "project-id": "Project ID",
"save-changes": "Save Changes", "save-changes": "Save Changes",
"saved": "Saved", "saved": "Saved",
"drop-zone": "Drag and drop your .env file here.", "drop-zone": "Drag and drop your .env file here.",
"drop-zone-keys": "Drag and drop your .env file here to add more keys.", "drop-zone-keys": "Drag and drop your .env file here to add more keys.",
"role": "Role", "role": "Role",
"role_admin": "admin", "role_admin": "admin",
"display-name": "Display Name" "display-name": "Display Name"
}, },
"nav": { "nav": {
"support": { "support": {
"slack": "[NEW] Join Slack Forum", "slack": "[NEW] Join Slack Forum",
"docs": "Read Docs", "docs": "Read Docs",
"issue": "Open a Github Issue", "issue": "Open a Github Issue",
"email": "Send us an Email" "email": "Send us an Email"
}, },
"user": { "user": {
"signed-in-as": "SIGNED IN AS", "signed-in-as": "SIGNED IN AS",
"current-organization": "CURRENT ORGANIZATION", "current-organization": "CURRENT ORGANIZATION",
"usage-billing": "Usage & Billing", "usage-billing": "Usage & Billing",
"invite": "Invite Members", "invite": "Invite Members",
"other-organizations": "OTHER ORGANIZATION" "other-organizations": "OTHER ORGANIZATION"
}, },
"menu": { "menu": {
"project": "PROJECT", "project": "PROJECT",
"secrets": "Secrets", "secrets": "Secrets",
"members": "Members", "members": "Members",
"integrations": "Integrations", "integrations": "Integrations",
"project-settings": "Project Settings" "project-settings": "Project Settings"
} }
}, },
"auth": { "auth": {
"meta": { "meta": {
"login": { "login": {
"title": "Login | Infiscal", "title": "Login | Infiscal",
"og-title": "Log In to Infisical", "og-title": "Log In to Infisical",
"og-description": "Infisical a simple end-to-end encrypted platform that enables teams to sync and manage their .env files." "og-description": "Infisical a simple end-to-end encrypted platform that enables teams to sync and manage their .env files."
}, },
"signup": { "signup": {
"title": "Sign Up | Infiscal", "title": "Sign Up | Infiscal",
"og-title": "Replace .env files with 1 line of code. Sign Up for Infisical in 3 minutes.", "og-title": "Replace .env files with 1 line of code. Sign Up for Infisical in 3 minutes.",
"og-description": "Infisical a simple end-to-end encrypted platform that enables teams to sync and manage API-keys and environemntal variables. Works with Node.js, Next.js, Gatsby, Nest.js..." "og-description": "Infisical a simple end-to-end encrypted platform that enables teams to sync and manage API-keys and environemntal variables. Works with Node.js, Next.js, Gatsby, Nest.js..."
} }
}, },
"login": "Log In", "login": "Log In",
"signup": "Sign Up", "signup": "Sign Up",
"need-account": "Need an Infisical account?", "need-account": "Need an Infisical account?",
"create-account": "Create an account", "create-account": "Create an account",
"already-have-account": "Have an account? Log in", "already-have-account": "Have an account? Log in",
"forgot-password": "Forgot your password?", "forgot-password": "Forgot your password?",
"step1-start": "Let's get started", "step1-start": "Let's get started",
"step1-privacy": "By creating an account, you agree to our Terms and have read and acknowledged the Privacy Policy.", "step1-privacy": "By creating an account, you agree to our Terms and have read and acknowledged the Privacy Policy.",
"step1-submit": "Get Started", "step1-submit": "Get Started",
"step2-message": "<wrapper>We've sent a verification email to</wrapper><email>{{email}}</email>", "step2-message": "<wrapper>We've sent a verification email to</wrapper><email>{{email}}</email>",
"step2-code-error": "Oops. Your code is wrong. Please try again.", "step2-code-error": "Oops. Your code is wrong. Please try again.",
"step2-spam-alert": "Make sure to check your spam inbox.", "step2-spam-alert": "Make sure to check your spam inbox.",
"step3-message": "Almost there!", "step3-message": "Almost there!",
"step4-message": "Save your Emergency Kit", "step4-message": "Save your Emergency Kit",
"step4-description1": "If you get locked out of your account, your Emergency Kit is the only way to sign in.", "step4-description1": "If you get locked out of your account, your Emergency Kit is the only way to sign in.",
"step4-description2": "We recommend you download it and keep it somewhere safe.", "step4-description2": "We recommend you download it and keep it somewhere safe.",
"step4-description3": "It contains your Secret Key which we cannot access or recover for you if you lose it.", "step4-description3": "It contains your Secret Key which we cannot access or recover for you if you lose it.",
"step4-download": "Download PDF" "step4-download": "Download PDF"
}, },
"dashboard": { "dashboard": {
"secrets": "Secrets", "secrets": "Secrets",
"meta": { "meta": {
"title": "Dashboard | Infiscal", "title": "Dashboard | Infiscal",
"og-title": "Manage your .env files in seconds", "og-title": "Manage your .env files in seconds",
"og-description": "Infisical a simple end-to-end encrypted platform that enables teams to sync and manage their .env files." "og-description": "Infisical a simple end-to-end encrypted platform that enables teams to sync and manage their .env files."
}, },
"search-keys": "Search keys...", "search-keys": "Search keys...",
"add-key": "Add Key", "add-key": "Add Key",
"personal": "Personal", "personal": "Personal",
"personal-description": "Personal keys are only visible to you", "personal-description": "Personal keys are only visible to you",
"shared": "Shared", "shared": "Shared",
"shared-description": "Shared keys are visible to your whole team", "shared-description": "Shared keys are visible to your whole team",
"make-shared": "Make Shared", "make-shared": "Make Shared",
"make-personal": "Make Personal", "make-personal": "Make Personal",
"check-docs": { "check-docs": {
"button": "Check Docs", "button": "Check Docs",
"title": "Good job!", "title": "Good job!",
"line1": "Congrats on adding more secrets.", "line1": "Congrats on adding more secrets.",
"line2": "Here is how to connect them to your codebase." "line2": "Here is how to connect them to your codebase."
} }
}, },
"settings": { "settings": {
"meta": { "meta": {
"members": { "members": {
"title": "Project Members", "title": "Project Members",
"head-title": "Project Settings | Infiscal", "head-title": "Project Settings | Infiscal",
"description": "This pages shows the members of the selected project." "description": "This pages shows the members of the selected project."
}, },
"org": { "org": {
"title": "Organization Settings", "title": "Organization Settings",
"head-title": "Organization Settings | Infiscal", "head-title": "Organization Settings | Infiscal",
"description": "Manage members of your organization. These users could afterwards be formed into projects." "description": "Manage members of your organization. These users could afterwards be formed into projects."
} }
}, },
"search-members": "Search members...", "search-members": "Search members...",
"search": "Search...", "search": "Search...",
"add-member": "Add Member", "add-member": "Add Member",
"org-members": "Organization Members", "org-members": "Organization Members",
"org-members-description": "Manage members of your organization. These users could afterwards be formed into projects.", "org-members-description": "Manage members of your organization. These users could afterwards be formed into projects.",
"incident-contacts": "Incident Contacts", "incident-contacts": "Incident Contacts",
"incident-contacts-description": "These contacts will be notified in the unlikely event of a severe incident.", "incident-contacts-description": "These contacts will be notified in the unlikely event of a severe incident.",
"no-incident-contacts": "No incident contacts found.", "no-incident-contacts": "No incident contacts found.",
"add-contact": "Add Contact", "add-contact": "Add Contact",
"add-member-dialog": { "add-member-dialog": {
"add-member-to-project": "Add a member to your project", "add-member-to-project": "Add a member to your project",
"already-all-invited": "All the users in your organization are already invited.", "already-all-invited": "All the users in your organization are already invited.",
"add-user-org-first": "Add more users to the organization first.", "add-user-org-first": "Add more users to the organization first.",
"user-will-email": "The user will receive an email with the instructions.", "user-will-email": "The user will receive an email with the instructions.",
"looking-add": "<0>If you are looking to add users to your org,</0><1>click here</1>", "looking-add": "<0>If you are looking to add users to your org,</0><1>click here</1>",
"add-user-to-org": "Add Users to Organization", "add-user-to-org": "Add Users to Organization",
"add-member": "Add Member" "add-member": "Add Member"
} }
} }
} }
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"common": {}, "common": {},
"nav": {}, "nav": {},
"auth": {}, "auth": {},
"dashboard": {} "dashboard": {}
} }
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
+45 -45
View File
@@ -4,13 +4,13 @@ const nextTranslate = require("next-translate");
const ContentSecurityPolicy = ` const ContentSecurityPolicy = `
default-src ${process.env.NEXT_PUBLIC_WEBSITE_URL}; default-src ${process.env.NEXT_PUBLIC_WEBSITE_URL};
script-src ${ script-src ${
process.env.NEXT_PUBLIC_WEBSITE_URL process.env.NEXT_PUBLIC_WEBSITE_URL
} https://app.posthog.com https://infisical.com https://assets.calendly.com/ https://js.stripe.com https://api.stripe.com 'unsafe-inline' 'unsafe-eval'; } https://app.posthog.com https://infisical.com https://assets.calendly.com/ https://js.stripe.com https://api.stripe.com 'unsafe-inline' 'unsafe-eval';
style-src 'self' https://rsms.me 'unsafe-inline'; style-src 'self' https://rsms.me 'unsafe-inline';
child-src https://infisical.com https://api.stripe.com; child-src https://infisical.com https://api.stripe.com;
frame-src https://js.stripe.com/ https://api.stripe.com; frame-src https://js.stripe.com/ https://api.stripe.com;
connect-src ws://${process.env.NEXT_PUBLIC_WEBSITE_URL?.split("//")[1]} ${ connect-src ws://${process.env.NEXT_PUBLIC_WEBSITE_URL?.split("//")[1]} ${
process.env.NEXT_PUBLIC_WEBSITE_URL process.env.NEXT_PUBLIC_WEBSITE_URL
} https://api.github.com/repos/Infisical/infisical-cli https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://infisical.com https://api.stripe.com https://vitals.vercel-insights.com/v1/vitals; } https://api.github.com/repos/Infisical/infisical-cli https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://infisical.com https://api.stripe.com https://vitals.vercel-insights.com/v1/vitals;
img-src 'self' https://*.stripe.com https://i.ytimg.com/ data:; img-src 'self' https://*.stripe.com https://i.ytimg.com/ data:;
media-src; media-src;
@@ -20,49 +20,49 @@ const ContentSecurityPolicy = `
// You can choose which headers to add to the list // You can choose which headers to add to the list
// after learning more below. // after learning more below.
const securityHeaders = [ const securityHeaders = [
{ {
key: "X-DNS-Prefetch-Control", key: "X-DNS-Prefetch-Control",
value: "on", value: "on",
}, },
{ {
key: "Strict-Transport-Security", key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload", value: "max-age=63072000; includeSubDomains; preload",
}, },
{ {
key: "X-XSS-Protection", key: "X-XSS-Protection",
value: "1; mode=block", value: "1; mode=block",
}, },
{ {
key: "X-Frame-Options", key: "X-Frame-Options",
value: "SAMEORIGIN", value: "SAMEORIGIN",
}, },
{ {
key: "Permissions-Policy", key: "Permissions-Policy",
value: "camera=(), microphone=()", value: "camera=(), microphone=()",
}, },
{ {
key: "X-Content-Type-Options", key: "X-Content-Type-Options",
value: "nosniff", value: "nosniff",
}, },
{ {
key: "Referrer-Policy", key: "Referrer-Policy",
value: "strict-origin-when-cross-origin", value: "strict-origin-when-cross-origin",
}, },
{ {
key: "Content-Security-Policy", key: "Content-Security-Policy",
value: ContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(), value: ContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(),
}, },
]; ];
module.exports = nextTranslate({ module.exports = nextTranslate({
output: "standalone", output: "standalone",
async headers() { async headers() {
return [ return [
{ {
// Apply these headers to all routes in your application. // Apply these headers to all routes in your application.
source: "/:path*", source: "/:path*",
headers: securityHeaders, headers: securityHeaders,
}, },
]; ];
}, },
}); });
+12315 -8005
View File
File diff suppressed because it is too large Load Diff
+63 -60
View File
@@ -1,63 +1,66 @@
{ {
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"start:docker": "next build && next start" "start:docker": "next build && next start",
}, "lint": "next lint"
"dependencies": { },
"@apihero/github": "^1.1.5", "dependencies": {
"@apihero/react": "^1.2.3", "@emotion/css": "^11.10.0",
"@emotion/css": "^11.10.0", "@emotion/server": "^11.10.0",
"@emotion/server": "^11.10.0", "@fortawesome/fontawesome-svg-core": "^6.1.2",
"@fortawesome/fontawesome-svg-core": "^6.1.2", "@fortawesome/free-brands-svg-icons": "^6.1.2",
"@fortawesome/free-brands-svg-icons": "^6.1.2", "@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.2",
"@fortawesome/free-solid-svg-icons": "^6.1.2", "@fortawesome/react-fontawesome": "^0.1.19",
"@fortawesome/react-fontawesome": "^0.1.19", "@headlessui/react": "^1.6.6",
"@headlessui/react": "^1.6.6", "@reduxjs/toolkit": "^1.8.3",
"@heroicons/react": "^1.0.6", "@stripe/react-stripe-js": "^1.10.0",
"@reduxjs/toolkit": "^1.8.3", "@stripe/stripe-js": "^1.35.0",
"@stripe/react-stripe-js": "^1.10.0", "add": "^2.0.6",
"@stripe/stripe-js": "^1.35.0", "axios": "^0.27.2",
"add": "^2.0.6", "axios-auth-refresh": "^3.3.3",
"axios": "^0.27.2", "classnames": "^2.3.1",
"axios-auth-refresh": "^3.3.3", "cookies": "^0.8.0",
"classnames": "^2.3.1", "fs": "^0.0.1-security",
"cookies": "^0.8.0", "gray-matter": "^4.0.3",
"fs": "^0.0.1-security", "http-proxy": "^1.18.1",
"gray-matter": "^4.0.3", "jspdf": "^2.5.1",
"http-proxy": "^1.18.1", "jsrp": "^0.2.4",
"jspdf": "^2.5.1", "markdown-it": "^13.0.1",
"jsrp": "^0.2.4", "next": "^12.2.5",
"markdown-it": "^13.0.1",
"next": "^12.2.5",
"next-translate": "^1.6.0", "next-translate": "^1.6.0",
"posthog-js": "^1.34.0", "posthog-js": "^1.34.0",
"query-string": "^7.1.1", "query-string": "^7.1.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
"react-code-input": "^3.10.1", "react-code-input": "^3.10.1",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-github-btn": "^1.4.0", "react-github-btn": "^1.4.0",
"react-grid-layout": "^1.3.4", "react-grid-layout": "^1.3.4",
"react-mailchimp-subscribe": "^2.1.3", "react-mailchimp-subscribe": "^2.1.3",
"react-markdown": "^8.0.3", "react-markdown": "^8.0.3",
"react-redux": "^8.0.2", "react-redux": "^8.0.2",
"react-table": "^7.8.0", "react-table": "^7.8.0",
"set-cookie-parser": "^2.5.1", "set-cookie-parser": "^2.5.1",
"styled-components": "^5.3.5", "styled-components": "^5.3.5",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1", "tweetnacl-util": "^0.15.1",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"uuidv4": "^6.2.13" "uuidv4": "^6.2.13"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.4", "@tailwindcss/typography": "^0.5.4",
"autoprefixer": "^10.4.7", "@types/node": "18.11.9",
"postcss": "^8.4.14", "autoprefixer": "^10.4.7",
"prettier": "2.7.1", "eslint": "^8.28.0",
"tailwindcss": "^3.1.4" "eslint-config-next": "^13.0.5",
} "eslint-plugin-simple-import-sort": "^8.0.0",
"postcss": "^8.4.14",
"prettier": "2.7.1",
"tailwindcss": "^3.1.4",
"typescript": "^4.9.3"
}
} }
+42 -42
View File
@@ -1,62 +1,62 @@
import "../styles/globals.css";
import "@fortawesome/fontawesome-svg-core/styles.css";
import { config } from "@fortawesome/fontawesome-svg-core";
import Layout from "../components/basic/layout";
import RouteGuard from "../components/RouteGuard";
import { publicPaths } from "../const.js";
import { useEffect } from "react"; import { useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { initPostHog } from "../components/analytics/posthog"; import { config } from "@fortawesome/fontawesome-svg-core";
import { ENV } from "../components/utilities/config";
import { initPostHog } from "~/components/analytics/posthog";
import Layout from "~/components/basic/layout";
import RouteGuard from "~/components/RouteGuard";
import { publicPaths } from "~/const";
import { ENV } from "~/utilities/config";
import "@fortawesome/fontawesome-svg-core/styles.css";
import "../styles/globals.css";
config.autoAddCss = false; config.autoAddCss = false;
const App = ({ Component, pageProps, ...appProps }) => { const App = ({ Component, pageProps, ...appProps }) => {
const router = useRouter(); const router = useRouter();
const posthog = initPostHog(); const posthog = initPostHog();
useEffect(() => { useEffect(() => {
// Init for auto capturing // Init for auto capturing
const posthog = initPostHog(); const posthog = initPostHog();
const handleRouteChange = () => { const handleRouteChange = () => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
if (ENV == "production") { if (ENV == "production") {
posthog.capture("$pageview"); posthog.capture("$pageview");
} }
} }
}; };
router.events.on("routeChangeComplete", handleRouteChange); router.events.on("routeChangeComplete", handleRouteChange);
return () => { return () => {
router.events.off("routeChangeComplete", handleRouteChange); router.events.off("routeChangeComplete", handleRouteChange);
}; };
}, [router.events]); }, [router.events]);
// If it's one of these routes, don't add the layout (e.g., these routes are external) // If it's one of these routes, don't add the layout (e.g., these routes are external)
if ( if (
publicPaths.includes("/" + appProps.router.pathname.split("/")[1]) || publicPaths.includes("/" + appProps.router.pathname.split("/")[1]) ||
!Component.requireAuth !Component.requireAuth
) { ) {
return ( return <Component {...pageProps} />;
<Component {...pageProps} /> }
)
}
return ( return (
<RouteGuard> <RouteGuard>
<Layout> <Layout>
<Component {...pageProps} /> <Component {...pageProps} />
</Layout> </Layout>
</RouteGuard> </RouteGuard>
); );
}; };
export default App; export default App;
{ {
/* <Script /* <Script
src="https://www.googletagmanager.com/gtag/js?id=G-DQ1XLJJGG1" src="https://www.googletagmanager.com/gtag/js?id=G-DQ1XLJJGG1"
strategy="afterInteractive" strategy="afterInteractive"
/> />
+30 -24
View File
@@ -1,33 +1,39 @@
import SecurityClient from "../../../components/utilities/SecurityClient"; import { PATH } from "~/const";
import { PATH } from "../../../const"; import SecurityClient from "~/utilities/SecurityClient";
/** /**
* This is the second step of the change password process (pake) * This is the second step of the change password process (pake)
* @param {*} clientPublicKey * @param {*} clientPublicKey
* @returns * @returns
*/ */
const changePassword2 = ({encryptedPrivateKey, iv, tag, salt, verifier, clientProof}) => { const changePassword2 = ({
return SecurityClient.fetchCall(PATH + "/api/v1/password/change-password", { encryptedPrivateKey,
method: "POST", iv,
headers: { tag,
"Content-Type": "application/json", salt,
}, verifier,
body: JSON.stringify({ clientProof,
"clientProof": clientProof, }) => {
"encryptedPrivateKey": encryptedPrivateKey, return SecurityClient.fetchCall(PATH + "/api/v1/password/change-password", {
"iv": iv, method: "POST",
"tag": tag, headers: {
"salt": salt, "Content-Type": "application/json",
"verifier": verifier },
}), body: JSON.stringify({
}) clientProof: clientProof,
.then(async res => { encryptedPrivateKey: encryptedPrivateKey,
if (res.status == 200) { iv: iv,
return res; tag: tag,
} else { salt: salt,
console.log('Failed to change the password'); verifier: verifier,
} }),
}) }).then(async (res) => {
if (res.status == 200) {
return res;
} else {
console.log("Failed to change the password");
}
});
}; };
export default changePassword2; export default changePassword2;

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