mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 12:26:38 +00:00
Merge branch 'main' of github.com:reginaldbondoc/infisical
This commit is contained in:
+6
-2
@@ -47,8 +47,12 @@ SMTP_PASSWORD=
|
|||||||
|
|
||||||
# Integration
|
# Integration
|
||||||
# Optional only if integration is used
|
# Optional only if integration is used
|
||||||
OAUTH_CLIENT_SECRET_HEROKU=
|
CLIENT_ID_HEROKU=
|
||||||
OAUTH_TOKEN_URL_HEROKU=
|
CLIENT_ID_VERCEL=
|
||||||
|
CLIENT_ID_NETLIFY=
|
||||||
|
CLIENT_SECRET_HEROKU=
|
||||||
|
CLIENT_SECRET_VERCEL=
|
||||||
|
CLIENT_SECRET_NETLIFY=
|
||||||
|
|
||||||
# Sentry (optional) for monitoring errors
|
# Sentry (optional) for monitoring errors
|
||||||
SENTRY_DSN=
|
SENTRY_DSN=
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": true,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"singleQuote": true,
|
|
||||||
"printWidth": 80,
|
|
||||||
"useTabs": false
|
|
||||||
}
|
|
||||||
@@ -27,6 +27,9 @@
|
|||||||
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">
|
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">
|
||||||
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
|
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://twitter.com/infisical">
|
||||||
|
<img src="https://img.shields.io/twitter/follow/infisical?label=Follow" alt="Infisical Twitter" />
|
||||||
|
</a>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<img src="/img/infisical_github_repo.png" width="100%" alt="Dashboard" />
|
<img src="/img/infisical_github_repo.png" width="100%" alt="Dashboard" />
|
||||||
@@ -92,12 +95,6 @@ Not sure where to get started? You can:
|
|||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 🔌 Integrations
|
## 🔌 Integrations
|
||||||
|
|
||||||
We're currently setting the foundation and building [integrations](https://infisical.com/docs/integrations/overview) so secrets can be synced everywhere. Any help is welcome! :)
|
We're currently setting the foundation and building [integrations](https://infisical.com/docs/integrations/overview) so secrets can be synced everywhere. Any help is welcome! :)
|
||||||
@@ -131,10 +128,14 @@ We're currently setting the foundation and building [integrations](https://infis
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" valign="middle">
|
<td align="left" valign="middle">
|
||||||
🔜 Vercel (https://github.com/Infisical/infisical/issues/60)
|
<a href="https://infisical.com/docs/integrations/cloud/vercel?ref=github.com">
|
||||||
|
✔️ Vercel
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="left" valign="middle">
|
<td align="left" valign="middle">
|
||||||
🔜 GitLab CI/CD
|
<a href="https://infisical.com/docs/integrations/platforms/kubernetes?ref=github.com">
|
||||||
|
✔️ Kubernetes
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="left" valign="middle">
|
<td align="left" valign="middle">
|
||||||
🔜 Fly.io
|
🔜 Fly.io
|
||||||
@@ -156,7 +157,7 @@ We're currently setting the foundation and building [integrations](https://infis
|
|||||||
🔜 GCP
|
🔜 GCP
|
||||||
</td>
|
</td>
|
||||||
<td align="left" valign="middle">
|
<td align="left" valign="middle">
|
||||||
🔜 Kubernetes
|
🔜 GitLab CI/CD
|
||||||
</td>
|
</td>
|
||||||
<td align="left" valign="middle">
|
<td align="left" valign="middle">
|
||||||
🔜 CircleCI
|
🔜 CircleCI
|
||||||
@@ -302,6 +303,12 @@ This repo is entirely MIT licensed, with the exception of the `ee` directory whi
|
|||||||
|
|
||||||
Looking to report a security vulnerability? Please don't post about it in GitHub issue. Instead, refer to our [SECURITY.md](./SECURITY.md) file.
|
Looking to report a security vulnerability? Please don't post about it in GitHub issue. Instead, refer to our [SECURITY.md](./SECURITY.md) file.
|
||||||
|
|
||||||
|
## 🚨 Stay Up-to-Date
|
||||||
|
|
||||||
|
Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of new features are coming very quickly. Watch **releases** of this repository to be notified about future updates:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 🦸 Contributors
|
## 🦸 Contributors
|
||||||
|
|
||||||
[//]: contributor-faces
|
[//]: contributor-faces
|
||||||
|
|||||||
+2
-3
@@ -1,11 +1,10 @@
|
|||||||
{
|
{
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"plugins": ["@typescript-eslint", "prettier"],
|
"plugins": ["@typescript-eslint"],
|
||||||
"extends": [
|
"extends": [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended"
|
||||||
"prettier"
|
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-console": 2
|
"no-console": 2
|
||||||
|
|||||||
+2
-2
@@ -4,12 +4,12 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
RUN npm ci --only-production
|
RUN npm ci --only-production --ignore-scripts
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
||||||
CMD node healthcheck.js
|
CMD node healthcheck.js
|
||||||
|
|
||||||
|
|
||||||
CMD ["npm", "run", "start"]
|
CMD ["npm", "run", "start"]
|
||||||
|
|||||||
Vendored
+6
-2
@@ -14,8 +14,12 @@ declare global {
|
|||||||
JWT_SIGNUP_SECRET: string;
|
JWT_SIGNUP_SECRET: string;
|
||||||
MONGO_URL: string;
|
MONGO_URL: string;
|
||||||
NODE_ENV: 'development' | 'staging' | 'testing' | 'production';
|
NODE_ENV: 'development' | 'staging' | 'testing' | 'production';
|
||||||
OAUTH_CLIENT_SECRET_HEROKU: string;
|
CLIENT_ID_HEROKU: string;
|
||||||
OAUTH_TOKEN_URL_HEROKU: string;
|
CLIENT_ID_VERCEL: string;
|
||||||
|
CLIENT_ID_NETLIFY: string;
|
||||||
|
CLIENT_SECRET_HEROKU: string;
|
||||||
|
CLIENT_SECRET_VERCEL: string;
|
||||||
|
CLIENT_SECRET_NETLIFY: string;
|
||||||
POSTHOG_HOST: string;
|
POSTHOG_HOST: string;
|
||||||
POSTHOG_PROJECT_API_KEY: string;
|
POSTHOG_PROJECT_API_KEY: string;
|
||||||
PRIVATE_KEY: string;
|
PRIVATE_KEY: string;
|
||||||
|
|||||||
Generated
-120
@@ -53,14 +53,11 @@
|
|||||||
"@typescript-eslint/parser": "^5.40.1",
|
"@typescript-eslint/parser": "^5.40.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.26.0",
|
"eslint": "^8.26.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
"jest": "^29.3.1",
|
"jest": "^29.3.1",
|
||||||
"jest-junit": "^15.0.0",
|
"jest-junit": "^15.0.0",
|
||||||
"nodemon": "^2.0.19",
|
"nodemon": "^2.0.19",
|
||||||
"npm": "^8.19.3",
|
"npm": "^8.19.3",
|
||||||
"prettier": "^2.7.1",
|
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
"ts-jest": "^29.0.3",
|
"ts-jest": "^29.0.3",
|
||||||
"ts-node": "^10.9.1"
|
"ts-node": "^10.9.1"
|
||||||
@@ -2943,12 +2940,6 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/prettier": {
|
|
||||||
"version": "2.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz",
|
|
||||||
"integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.9.7",
|
"version": "6.9.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
|
||||||
@@ -4368,39 +4359,6 @@
|
|||||||
"url": "https://opencollective.com/eslint"
|
"url": "https://opencollective.com/eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-config-prettier": {
|
|
||||||
"version": "8.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz",
|
|
||||||
"integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"eslint-config-prettier": "bin/cli.js"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"eslint": ">=7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-prettier": {
|
|
||||||
"version": "4.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
|
|
||||||
"integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"prettier-linter-helpers": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"eslint": ">=7.28.0",
|
|
||||||
"prettier": ">=2.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"eslint-config-prettier": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-scope": {
|
"node_modules/eslint-scope": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
||||||
@@ -4718,12 +4676,6 @@
|
|||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/fast-diff": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.2.12",
|
"version": "3.2.12",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
|
||||||
@@ -6027,7 +5979,6 @@
|
|||||||
"@jest/transform": "^29.3.1",
|
"@jest/transform": "^29.3.1",
|
||||||
"@jest/types": "^29.3.1",
|
"@jest/types": "^29.3.1",
|
||||||
"@types/babel__traverse": "^7.0.6",
|
"@types/babel__traverse": "^7.0.6",
|
||||||
"@types/prettier": "^2.1.5",
|
|
||||||
"babel-preset-current-node-syntax": "^1.0.0",
|
"babel-preset-current-node-syntax": "^1.0.0",
|
||||||
"chalk": "^4.0.0",
|
"chalk": "^4.0.0",
|
||||||
"expect": "^29.3.1",
|
"expect": "^29.3.1",
|
||||||
@@ -9716,33 +9667,6 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
|
||||||
"version": "2.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
|
|
||||||
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"prettier": "bin-prettier.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.13.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prettier-linter-helpers": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"fast-diff": "^1.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pretty-format": {
|
"node_modules/pretty-format": {
|
||||||
"version": "29.3.1",
|
"version": "29.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz",
|
||||||
@@ -13662,12 +13586,6 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/prettier": {
|
|
||||||
"version": "2.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz",
|
|
||||||
"integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/qs": {
|
"@types/qs": {
|
||||||
"version": "6.9.7",
|
"version": "6.9.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
|
||||||
@@ -14723,22 +14641,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eslint-config-prettier": {
|
|
||||||
"version": "8.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz",
|
|
||||||
"integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {}
|
|
||||||
},
|
|
||||||
"eslint-plugin-prettier": {
|
|
||||||
"version": "4.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
|
|
||||||
"integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"prettier-linter-helpers": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"eslint-scope": {
|
"eslint-scope": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
||||||
@@ -14961,12 +14863,6 @@
|
|||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"fast-diff": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"fast-glob": {
|
"fast-glob": {
|
||||||
"version": "3.2.12",
|
"version": "3.2.12",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
|
||||||
@@ -15927,7 +15823,6 @@
|
|||||||
"@jest/transform": "^29.3.1",
|
"@jest/transform": "^29.3.1",
|
||||||
"@jest/types": "^29.3.1",
|
"@jest/types": "^29.3.1",
|
||||||
"@types/babel__traverse": "^7.0.6",
|
"@types/babel__traverse": "^7.0.6",
|
||||||
"@types/prettier": "^2.1.5",
|
|
||||||
"babel-preset-current-node-syntax": "^1.0.0",
|
"babel-preset-current-node-syntax": "^1.0.0",
|
||||||
"chalk": "^4.0.0",
|
"chalk": "^4.0.0",
|
||||||
"expect": "^29.3.1",
|
"expect": "^29.3.1",
|
||||||
@@ -18582,21 +18477,6 @@
|
|||||||
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"prettier": {
|
|
||||||
"version": "2.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
|
|
||||||
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"prettier-linter-helpers": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"fast-diff": "^1.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pretty-format": {
|
"pretty-format": {
|
||||||
"version": "29.3.1",
|
"version": "29.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz",
|
||||||
|
|||||||
@@ -31,12 +31,12 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"prepare": "cd .. && npm install",
|
||||||
"start": "npm run build && node build/index.js",
|
"start": "npm run build && node build/index.js",
|
||||||
"dev": "nodemon",
|
"dev": "nodemon",
|
||||||
"build": "rimraf ./build && tsc && cp -R ./src/templates ./src/json ./build",
|
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build",
|
||||||
"lint": "eslint . --ext .ts",
|
"lint": "eslint . --ext .ts",
|
||||||
"lint-and-fix": "eslint . --ext .ts --fix",
|
"lint-and-fix": "eslint . --ext .ts --fix",
|
||||||
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
|
|
||||||
"lint-staged": "lint-staged",
|
"lint-staged": "lint-staged",
|
||||||
"pretest": "docker compose -f test-resources/docker-compose.test.yml up -d",
|
"pretest": "docker compose -f test-resources/docker-compose.test.yml up -d",
|
||||||
"test": "cross-env NODE_ENV=test jest --testTimeout=10000 --detectOpenHandles",
|
"test": "cross-env NODE_ENV=test jest --testTimeout=10000 --detectOpenHandles",
|
||||||
@@ -71,14 +71,11 @@
|
|||||||
"@typescript-eslint/parser": "^5.40.1",
|
"@typescript-eslint/parser": "^5.40.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.26.0",
|
"eslint": "^8.26.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
"jest": "^29.3.1",
|
"jest": "^29.3.1",
|
||||||
"jest-junit": "^15.0.0",
|
"jest-junit": "^15.0.0",
|
||||||
"nodemon": "^2.0.19",
|
"nodemon": "^2.0.19",
|
||||||
"npm": "^8.19.3",
|
"npm": "^8.19.3",
|
||||||
"prettier": "^2.7.1",
|
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
"ts-jest": "^29.0.3",
|
"ts-jest": "^29.0.3",
|
||||||
"ts-node": "^10.9.1"
|
"ts-node": "^10.9.1"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { apiLimiter } from './helpers/rateLimiter';
|
|||||||
import {
|
import {
|
||||||
signup as signupRouter,
|
signup as signupRouter,
|
||||||
auth as authRouter,
|
auth as authRouter,
|
||||||
|
bot as botRouter,
|
||||||
organization as organizationRouter,
|
organization as organizationRouter,
|
||||||
workspace as workspaceRouter,
|
workspace as workspaceRouter,
|
||||||
membershipOrg as membershipOrgRouter,
|
membershipOrg as membershipOrgRouter,
|
||||||
@@ -52,6 +53,7 @@ if (NODE_ENV === 'production') {
|
|||||||
// routers
|
// routers
|
||||||
app.use('/api/v1/signup', signupRouter);
|
app.use('/api/v1/signup', signupRouter);
|
||||||
app.use('/api/v1/auth', authRouter);
|
app.use('/api/v1/auth', authRouter);
|
||||||
|
app.use('/api/v1/bot', botRouter);
|
||||||
app.use('/api/v1/user', userRouter);
|
app.use('/api/v1/user', userRouter);
|
||||||
app.use('/api/v1/user-action', userActionRouter);
|
app.use('/api/v1/user-action', userActionRouter);
|
||||||
app.use('/api/v1/organization', organizationRouter);
|
app.use('/api/v1/organization', organizationRouter);
|
||||||
|
|||||||
@@ -10,8 +10,13 @@ const JWT_SIGNUP_LIFETIME = process.env.JWT_SIGNUP_LIFETIME! || '15m';
|
|||||||
const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!;
|
const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!;
|
||||||
const MONGO_URL = process.env.MONGO_URL!;
|
const MONGO_URL = process.env.MONGO_URL!;
|
||||||
const NODE_ENV = process.env.NODE_ENV! || 'production';
|
const NODE_ENV = process.env.NODE_ENV! || 'production';
|
||||||
const OAUTH_CLIENT_SECRET_HEROKU = process.env.OAUTH_CLIENT_SECRET_HEROKU!;
|
const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
|
||||||
const OAUTH_TOKEN_URL_HEROKU = process.env.OAUTH_TOKEN_URL_HEROKU!;
|
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
|
||||||
|
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
|
||||||
|
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!;
|
||||||
|
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
|
||||||
|
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
|
||||||
|
const CLIENT_SLUG_VERCEL= process.env.CLIENT_SLUG_VERCEL!;
|
||||||
const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com';
|
const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com';
|
||||||
const POSTHOG_PROJECT_API_KEY =
|
const POSTHOG_PROJECT_API_KEY =
|
||||||
process.env.POSTHOG_PROJECT_API_KEY! ||
|
process.env.POSTHOG_PROJECT_API_KEY! ||
|
||||||
@@ -46,8 +51,13 @@ export {
|
|||||||
JWT_SIGNUP_SECRET,
|
JWT_SIGNUP_SECRET,
|
||||||
MONGO_URL,
|
MONGO_URL,
|
||||||
NODE_ENV,
|
NODE_ENV,
|
||||||
OAUTH_CLIENT_SECRET_HEROKU,
|
CLIENT_ID_HEROKU,
|
||||||
OAUTH_TOKEN_URL_HEROKU,
|
CLIENT_ID_VERCEL,
|
||||||
|
CLIENT_ID_NETLIFY,
|
||||||
|
CLIENT_SECRET_HEROKU,
|
||||||
|
CLIENT_SECRET_VERCEL,
|
||||||
|
CLIENT_SECRET_NETLIFY,
|
||||||
|
CLIENT_SLUG_VERCEL,
|
||||||
POSTHOG_HOST,
|
POSTHOG_HOST,
|
||||||
POSTHOG_PROJECT_API_KEY,
|
POSTHOG_PROJECT_API_KEY,
|
||||||
PRIVATE_KEY,
|
PRIVATE_KEY,
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Bot, BotKey } from '../models';
|
||||||
|
import { createBot } from '../helpers/bot';
|
||||||
|
|
||||||
|
interface BotKey {
|
||||||
|
encryptedKey: string;
|
||||||
|
nonce: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return bot for workspace with id [workspaceId]. If a workspace bot doesn't exist,
|
||||||
|
* then create and return a new bot.
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
||||||
|
let bot;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
bot = await Bot.findOne({
|
||||||
|
workspace: workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bot) {
|
||||||
|
// case: bot doesn't exist for workspace with id [workspaceId]
|
||||||
|
// -> create a new bot and return it
|
||||||
|
bot = await createBot({
|
||||||
|
name: 'Infisical Bot',
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get bot for workspace'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
bot
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return bot with id [req.bot._id] with active state set to [isActive].
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const setBotActiveState = async (req: Request, res: Response) => {
|
||||||
|
let bot;
|
||||||
|
try {
|
||||||
|
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
// bot state set to active -> share workspace key with bot
|
||||||
|
if (!botKey?.encryptedKey || !botKey?.nonce) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to set bot state to active - missing bot key'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await BotKey.findOneAndUpdate({
|
||||||
|
workspace: req.bot.workspace
|
||||||
|
}, {
|
||||||
|
encryptedKey: botKey.encryptedKey,
|
||||||
|
nonce: botKey.nonce,
|
||||||
|
sender: req.user._id,
|
||||||
|
bot: req.bot._id,
|
||||||
|
workspace: req.bot.workspace
|
||||||
|
}, {
|
||||||
|
upsert: true,
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// case: bot state set to inactive -> delete bot's workspace key
|
||||||
|
await BotKey.deleteOne({
|
||||||
|
bot: req.bot._id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bot = await Bot.findOneAndUpdate({
|
||||||
|
_id: req.bot._id
|
||||||
|
}, {
|
||||||
|
isActive
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bot) throw new Error('Failed to update bot active state');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to update bot active state'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
bot
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as authController from './authController';
|
import * as authController from './authController';
|
||||||
|
import * as botController from './botController';
|
||||||
import * as integrationAuthController from './integrationAuthController';
|
import * as integrationAuthController from './integrationAuthController';
|
||||||
import * as integrationController from './integrationController';
|
import * as integrationController from './integrationController';
|
||||||
import * as keyController from './keyController';
|
import * as keyController from './keyController';
|
||||||
@@ -16,6 +17,7 @@ import * as workspaceController from './workspaceController';
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
authController,
|
authController,
|
||||||
|
botController,
|
||||||
integrationAuthController,
|
integrationAuthController,
|
||||||
integrationController,
|
integrationController,
|
||||||
keyController,
|
keyController,
|
||||||
|
|||||||
@@ -3,69 +3,45 @@ import * as Sentry from '@sentry/node';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { IntegrationAuth, Integration } from '../models';
|
import { IntegrationAuth, Integration } from '../models';
|
||||||
import { processOAuthTokenRes } from '../helpers/integrationAuth';
|
import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../variables';
|
||||||
import { INTEGRATION_SET, ENV_DEV } from '../variables';
|
import { IntegrationService } from '../services';
|
||||||
import { OAUTH_CLIENT_SECRET_HEROKU, OAUTH_TOKEN_URL_HEROKU } from '../config';
|
import { getApps, revokeAccess } from '../integrations';
|
||||||
|
|
||||||
|
export const getIntegrationOptions = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
return res.status(200).send({
|
||||||
|
integrationOptions: INTEGRATION_OPTIONS
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId]
|
* Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId]
|
||||||
* Note: integration [integration] must be set up compatible/designed for OAuth2
|
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const integrationAuthOauthExchange = async (
|
export const oAuthExchange = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
let clientSecret;
|
|
||||||
|
|
||||||
const { workspaceId, code, integration } = req.body;
|
const { workspaceId, code, integration } = req.body;
|
||||||
|
|
||||||
if (!INTEGRATION_SET.has(integration))
|
if (!INTEGRATION_SET.has(integration))
|
||||||
throw new Error('Failed to validate integration');
|
throw new Error('Failed to validate integration');
|
||||||
|
|
||||||
// use correct client secret
|
await IntegrationService.handleOAuthExchange({
|
||||||
switch (integration) {
|
|
||||||
case 'heroku':
|
|
||||||
clientSecret = OAUTH_CLIENT_SECRET_HEROKU;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: unfinished - make compatible with other integration types
|
|
||||||
const res = await axios.post(
|
|
||||||
OAUTH_TOKEN_URL_HEROKU!,
|
|
||||||
new URLSearchParams({
|
|
||||||
grant_type: 'authorization_code',
|
|
||||||
code: code,
|
|
||||||
client_secret: clientSecret
|
|
||||||
} as any)
|
|
||||||
);
|
|
||||||
|
|
||||||
const integrationAuth = await processOAuthTokenRes({
|
|
||||||
workspaceId,
|
workspaceId,
|
||||||
integration,
|
integration,
|
||||||
res
|
code
|
||||||
});
|
});
|
||||||
|
|
||||||
// create or replace integration
|
|
||||||
const integrationObj = await Integration.findOneAndUpdate(
|
|
||||||
{ workspace: workspaceId, integration },
|
|
||||||
{
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: ENV_DEV,
|
|
||||||
isActive: false,
|
|
||||||
app: null,
|
|
||||||
integration,
|
|
||||||
integrationAuth: integrationAuth._id
|
|
||||||
},
|
|
||||||
{ upsert: true, new: true }
|
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get OAuth2 token'
|
message: 'Failed to get OAuth2 code-token exchange'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,26 +51,25 @@ export const integrationAuthOauthExchange = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return list of applications allowed for integration with id [integrationAuthId]
|
* Return list of applications allowed for integration with integration authorization id [integrationAuthId]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
||||||
// TODO: unfinished - make compatible with other integration types
|
|
||||||
let apps;
|
let apps;
|
||||||
try {
|
try {
|
||||||
const res = await axios.get('https://api.heroku.com/apps', {
|
apps = await getApps({
|
||||||
headers: {
|
integrationAuth: req.integrationAuth,
|
||||||
Accept: 'application/vnd.heroku+json; version=3',
|
accessToken: req.accessToken
|
||||||
Authorization: 'Bearer ' + req.accessToken
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
apps = res.data.map((a: any) => ({
|
Sentry.setUser(null);
|
||||||
name: a.name
|
Sentry.captureException(err);
|
||||||
}));
|
return res.status(400).send({
|
||||||
} catch (err) {}
|
message: 'Failed to get integration authorization applications'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
apps
|
apps
|
||||||
@@ -108,46 +83,22 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
||||||
// TODO: unfinished - disable application via Heroku API and make compatible with other integration types
|
|
||||||
try {
|
try {
|
||||||
const { integrationAuthId } = req.params;
|
const { integrationAuthId } = req.params;
|
||||||
|
|
||||||
// TODO: disable application via Heroku API; figure out what authorization id is
|
await revokeAccess({
|
||||||
|
integrationAuth: req.integrationAuth,
|
||||||
const integrations = JSON.parse(
|
accessToken: req.accessToken
|
||||||
readFileSync('./src/json/integrations.json').toString()
|
|
||||||
);
|
|
||||||
|
|
||||||
let authorizationId;
|
|
||||||
switch (req.integrationAuth.integration) {
|
|
||||||
case 'heroku':
|
|
||||||
authorizationId = integrations.heroku.clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// not sure what authorizationId is?
|
|
||||||
// // revoke authorization
|
|
||||||
// const res2 = await axios.delete(
|
|
||||||
// `https://api.heroku.com/oauth/authorizations/${authorizationId}`,
|
|
||||||
// {
|
|
||||||
// headers: {
|
|
||||||
// 'Accept': 'application/vnd.heroku+json; version=3',
|
|
||||||
// 'Authorization': 'Bearer ' + req.accessToken
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
|
||||||
_id: integrationAuthId
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (deletedIntegrationAuth) {
|
|
||||||
await Integration.deleteMany({
|
|
||||||
integrationAuth: deletedIntegrationAuth._id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to delete integration authorization'
|
message: 'Failed to delete integration authorization'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully deleted integration authorization'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import axios from 'axios';
|
import { Integration, Bot, BotKey } from '../models';
|
||||||
import { Integration } from '../models';
|
import { EventService } from '../services';
|
||||||
import { decryptAsymmetric } from '../utils/crypto';
|
import { eventPushSecrets } from '../events';
|
||||||
import { decryptSecrets } from '../helpers/secret';
|
|
||||||
import { PRIVATE_KEY } from '../config';
|
|
||||||
|
|
||||||
interface Key {
|
interface Key {
|
||||||
encryptedKey: string;
|
encryptedKey: string;
|
||||||
@@ -24,104 +22,58 @@ interface PushSecret {
|
|||||||
type: 'shared' | 'personal';
|
type: 'shared' | 'personal';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return list of all available integrations on Infisical
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getIntegrations = async (req: Request, res: Response) => {
|
|
||||||
let integrations;
|
|
||||||
try {
|
|
||||||
integrations = JSON.parse(
|
|
||||||
readFileSync('./src/json/integrations.json').toString()
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get integrations'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
integrations
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync secrets [secrets] to integration with id [integrationId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const syncIntegration = async (req: Request, res: Response) => {
|
|
||||||
// TODO: unfinished - make more versatile to accomodate for other integrations
|
|
||||||
try {
|
|
||||||
const { key, secrets }: { key: Key; secrets: PushSecret[] } = req.body;
|
|
||||||
const symmetricKey = decryptAsymmetric({
|
|
||||||
ciphertext: key.encryptedKey,
|
|
||||||
nonce: key.nonce,
|
|
||||||
publicKey: req.user.publicKey,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
// decrypt secrets with symmetric key
|
|
||||||
const content = decryptSecrets({
|
|
||||||
secrets,
|
|
||||||
key: symmetricKey,
|
|
||||||
format: 'object'
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: make integration work for other integrations as well
|
|
||||||
const res = await axios.patch(
|
|
||||||
`https://api.heroku.com/apps/${req.integration.app}/config-vars`,
|
|
||||||
content,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/vnd.heroku+json; version=3',
|
|
||||||
Authorization: 'Bearer ' + req.accessToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to sync secrets with integration'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: 'Successfully synced secrets with integration'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change environment or name of integration with id [integrationId]
|
* Change environment or name of integration with id [integrationId]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const modifyIntegration = async (req: Request, res: Response) => {
|
export const updateIntegration = async (req: Request, res: Response) => {
|
||||||
let integration;
|
let integration;
|
||||||
|
|
||||||
|
// TODO: add integration-specific validation to ensure that each
|
||||||
|
// integration has the correct fields populated in [Integration]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { update } = req.body;
|
const {
|
||||||
|
app,
|
||||||
|
environment,
|
||||||
|
isActive,
|
||||||
|
target, // vercel-specific integration param
|
||||||
|
context, // netlify-specific integration param
|
||||||
|
siteId // netlify-specific integration param
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
integration = await Integration.findOneAndUpdate(
|
integration = await Integration.findOneAndUpdate(
|
||||||
{
|
{
|
||||||
_id: req.integration._id
|
_id: req.integration._id
|
||||||
},
|
},
|
||||||
update,
|
{
|
||||||
|
environment,
|
||||||
|
isActive,
|
||||||
|
app,
|
||||||
|
target,
|
||||||
|
context,
|
||||||
|
siteId
|
||||||
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (integration) {
|
||||||
|
// trigger event - push secrets
|
||||||
|
EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId: integration.workspace.toString()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to modify integration'
|
message: 'Failed to update integration'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +83,8 @@ export const modifyIntegration = async (req: Request, res: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete integration with id [integrationId]
|
* Delete integration with id [integrationId] and deactivate bot if there are
|
||||||
|
* no integrations left
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
@@ -144,6 +97,29 @@ export const deleteIntegration = async (req: Request, res: Response) => {
|
|||||||
deletedIntegration = await Integration.findOneAndDelete({
|
deletedIntegration = await Integration.findOneAndDelete({
|
||||||
_id: integrationId
|
_id: integrationId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!deletedIntegration) throw new Error('Failed to find integration');
|
||||||
|
|
||||||
|
const integrations = await Integration.find({
|
||||||
|
workspace: deletedIntegration.workspace
|
||||||
|
});
|
||||||
|
|
||||||
|
if (integrations.length === 0) {
|
||||||
|
// case: no integrations left, deactivate bot
|
||||||
|
const bot = await Bot.findOneAndUpdate({
|
||||||
|
workspace: deletedIntegration.workspace
|
||||||
|
}, {
|
||||||
|
isActive: false
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bot) {
|
||||||
|
await BotKey.deleteOne({
|
||||||
|
bot: bot._id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
|
|||||||
@@ -17,16 +17,6 @@ export const uploadKey = async (req: Request, res: Response) => {
|
|||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
const { key } = req.body;
|
const { key } = req.body;
|
||||||
|
|
||||||
// validate membership of sender
|
|
||||||
const senderMembership = await findMembership({
|
|
||||||
user: req.user._id,
|
|
||||||
workspace: workspaceId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!senderMembership) {
|
|
||||||
throw new Error('Failed sender membership validation for workspace');
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate membership of receiver
|
// validate membership of receiver
|
||||||
const receiverMembership = await findMembership({
|
const receiverMembership = await findMembership({
|
||||||
user: key.userId,
|
user: key.userId,
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import {
|
|||||||
reformatPullSecrets
|
reformatPullSecrets
|
||||||
} from '../helpers/secret';
|
} from '../helpers/secret';
|
||||||
import { pushKeys } from '../helpers/key';
|
import { pushKeys } from '../helpers/key';
|
||||||
|
import { eventPushSecrets } from '../events';
|
||||||
|
import { EventService } from '../services';
|
||||||
import { ENV_SET } from '../variables';
|
import { ENV_SET } from '../variables';
|
||||||
|
|
||||||
import { postHogClient } from '../services';
|
import { postHogClient } from '../services';
|
||||||
|
|
||||||
interface PushSecret {
|
interface PushSecret {
|
||||||
@@ -60,7 +61,8 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
keys
|
keys
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: 'secrets pushed',
|
event: 'secrets pushed',
|
||||||
@@ -74,6 +76,13 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trigger event - push secrets
|
||||||
|
EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
@@ -192,7 +201,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
// capture secrets pushed event in production
|
// capture secrets pulled event in production
|
||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
distinctId: req.serviceToken.user.email,
|
distinctId: req.serviceToken.user.email,
|
||||||
event: 'secrets pulled',
|
event: 'secrets pulled',
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { eventPushSecrets } from "./secret"
|
||||||
|
|
||||||
|
export {
|
||||||
|
eventPushSecrets
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { EVENT_PUSH_SECRETS } from '../variables';
|
||||||
|
|
||||||
|
interface PushSecret {
|
||||||
|
ciphertextKey: string;
|
||||||
|
ivKey: string;
|
||||||
|
tagKey: string;
|
||||||
|
hashKey: string;
|
||||||
|
ciphertextValue: string;
|
||||||
|
ivValue: string;
|
||||||
|
tagValue: string;
|
||||||
|
hashValue: string;
|
||||||
|
type: 'shared' | 'personal';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return event for pushing secrets
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace to push secrets to
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const eventPushSecrets = ({
|
||||||
|
workspaceId,
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
}) => {
|
||||||
|
return ({
|
||||||
|
name: EVENT_PUSH_SECRETS,
|
||||||
|
workspaceId,
|
||||||
|
payload: {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
eventPushSecrets
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Bot,
|
||||||
|
BotKey,
|
||||||
|
Secret,
|
||||||
|
ISecret,
|
||||||
|
IUser
|
||||||
|
} from '../models';
|
||||||
|
import {
|
||||||
|
generateKeyPair,
|
||||||
|
encryptSymmetric,
|
||||||
|
decryptSymmetric,
|
||||||
|
decryptAsymmetric
|
||||||
|
} from '../utils/crypto';
|
||||||
|
import { decryptSecrets } from '../helpers/secret';
|
||||||
|
import { ENCRYPTION_KEY } from '../config';
|
||||||
|
import { SECRET_SHARED } from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an inactive bot with name [name] for workspace with id [workspaceId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.name - name of bot
|
||||||
|
* @param {String} obj.workspaceId - id of workspace that bot belongs to
|
||||||
|
*/
|
||||||
|
const createBot = async ({
|
||||||
|
name,
|
||||||
|
workspaceId,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
workspaceId: string;
|
||||||
|
}) => {
|
||||||
|
let bot;
|
||||||
|
try {
|
||||||
|
const { publicKey, privateKey } = generateKeyPair();
|
||||||
|
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||||
|
plaintext: privateKey,
|
||||||
|
key: ENCRYPTION_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
bot = await new Bot({
|
||||||
|
name,
|
||||||
|
workspace: workspaceId,
|
||||||
|
isActive: false,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey: ciphertext,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
}).save();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to create bot');
|
||||||
|
}
|
||||||
|
|
||||||
|
return bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return decrypted secrets for workspace with id [workspaceId]
|
||||||
|
* and [environment] using bot
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @param {String} obj.environment - environment
|
||||||
|
*/
|
||||||
|
const getSecretsHelper = async ({
|
||||||
|
workspaceId,
|
||||||
|
environment
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
}) => {
|
||||||
|
const content = {} as any;
|
||||||
|
try {
|
||||||
|
const key = await getKey({ workspaceId });
|
||||||
|
const secrets = await Secret.find({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type: SECRET_SHARED
|
||||||
|
});
|
||||||
|
|
||||||
|
secrets.forEach((secret: ISecret) => {
|
||||||
|
const secretKey = decryptSymmetric({
|
||||||
|
ciphertext: secret.secretKeyCiphertext,
|
||||||
|
iv: secret.secretKeyIV,
|
||||||
|
tag: secret.secretKeyTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretValue = decryptSymmetric({
|
||||||
|
ciphertext: secret.secretValueCiphertext,
|
||||||
|
iv: secret.secretValueIV,
|
||||||
|
tag: secret.secretValueTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
content[secretKey] = secretValue;
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get secrets');
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return bot's copy of the workspace key for workspace
|
||||||
|
* with id [workspaceId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @returns {String} key - decrypted workspace key
|
||||||
|
*/
|
||||||
|
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
|
||||||
|
let key;
|
||||||
|
try {
|
||||||
|
const botKey = await BotKey.findOne({
|
||||||
|
workspace: workspaceId
|
||||||
|
}).populate<{ sender: IUser }>('sender', 'publicKey');
|
||||||
|
|
||||||
|
if (!botKey) throw new Error('Failed to find bot key');
|
||||||
|
|
||||||
|
const bot = await Bot.findOne({
|
||||||
|
workspace: workspaceId
|
||||||
|
}).select('+encryptedPrivateKey +iv +tag');
|
||||||
|
|
||||||
|
if (!bot) throw new Error('Failed to find bot');
|
||||||
|
if (!bot.isActive) throw new Error('Bot is not active');
|
||||||
|
|
||||||
|
const privateKeyBot = decryptSymmetric({
|
||||||
|
ciphertext: bot.encryptedPrivateKey,
|
||||||
|
iv: bot.iv,
|
||||||
|
tag: bot.tag,
|
||||||
|
key: ENCRYPTION_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
key = decryptAsymmetric({
|
||||||
|
ciphertext: botKey.encryptedKey,
|
||||||
|
nonce: botKey.nonce,
|
||||||
|
publicKey: botKey.sender.publicKey as string,
|
||||||
|
privateKey: privateKeyBot
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get workspace key');
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return symmetrically encrypted [plaintext] using the
|
||||||
|
* key for workspace with id [workspaceId]
|
||||||
|
* @param {Object} obj1
|
||||||
|
* @param {String} obj1.workspaceId - id of workspace
|
||||||
|
* @param {String} obj1.plaintext - plaintext to encrypt
|
||||||
|
*/
|
||||||
|
const encryptSymmetricHelper = async ({
|
||||||
|
workspaceId,
|
||||||
|
plaintext
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
plaintext: string;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const key = await getKey({ workspaceId });
|
||||||
|
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||||
|
plaintext,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
return ({
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to perform symmetric encryption with bot');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Return symmetrically decrypted [ciphertext] using the
|
||||||
|
* key for workspace with id [workspaceId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||||
|
* @param {String} obj.iv - iv
|
||||||
|
* @param {String} obj.tag - tag
|
||||||
|
*/
|
||||||
|
const decryptSymmetricHelper = async ({
|
||||||
|
workspaceId,
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
ciphertext: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
}) => {
|
||||||
|
let plaintext;
|
||||||
|
try {
|
||||||
|
const key = await getKey({ workspaceId });
|
||||||
|
const plaintext = decryptSymmetric({
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
return plaintext;
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to perform symmetric decryption with bot');
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
createBot,
|
||||||
|
getSecretsHelper,
|
||||||
|
encryptSymmetricHelper,
|
||||||
|
decryptSymmetricHelper
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Bot, IBot } from '../models';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { EVENT_PUSH_SECRETS } from '../variables';
|
||||||
|
import { IntegrationService } from '../services';
|
||||||
|
|
||||||
|
interface Event {
|
||||||
|
name: string;
|
||||||
|
workspaceId: string;
|
||||||
|
payload: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle event [event]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Event} obj.event - an event
|
||||||
|
* @param {String} obj.event.name - name of event
|
||||||
|
* @param {String} obj.event.workspaceId - id of workspace that event is part of
|
||||||
|
* @param {Object} obj.event.payload - payload of event (depends on event)
|
||||||
|
*/
|
||||||
|
const handleEventHelper = async ({
|
||||||
|
event
|
||||||
|
}: {
|
||||||
|
event: Event;
|
||||||
|
}) => {
|
||||||
|
const { workspaceId } = event;
|
||||||
|
|
||||||
|
// TODO: moduralize bot check into separate function
|
||||||
|
const bot = await Bot.findOne({
|
||||||
|
workspace: workspaceId,
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bot) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (event.name) {
|
||||||
|
case EVENT_PUSH_SECRETS:
|
||||||
|
IntegrationService.syncIntegrations({
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
handleEventHelper
|
||||||
|
}
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Bot,
|
||||||
|
Integration,
|
||||||
|
IIntegration,
|
||||||
|
IntegrationAuth,
|
||||||
|
IIntegrationAuth
|
||||||
|
} from '../models';
|
||||||
|
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
|
||||||
|
import { BotService, IntegrationService } from '../services';
|
||||||
|
import {
|
||||||
|
ENV_DEV,
|
||||||
|
EVENT_PUSH_SECRETS,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
interface Update {
|
||||||
|
workspace: string;
|
||||||
|
integration: string;
|
||||||
|
teamId?: string;
|
||||||
|
accountId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
|
||||||
|
* named [integration]
|
||||||
|
* - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
|
||||||
|
* - Add placeholder inactive integration
|
||||||
|
* - Create bot sequence for integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @param {String} obj.integration - name of integration
|
||||||
|
* @param {String} obj.code - code
|
||||||
|
*/
|
||||||
|
const handleOAuthExchangeHelper = async ({
|
||||||
|
workspaceId,
|
||||||
|
integration,
|
||||||
|
code
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
integration: string;
|
||||||
|
code: string;
|
||||||
|
}) => {
|
||||||
|
let action;
|
||||||
|
let integrationAuth;
|
||||||
|
try {
|
||||||
|
const bot = await Bot.findOne({
|
||||||
|
workspace: workspaceId,
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
|
||||||
|
|
||||||
|
// exchange code for access and refresh tokens
|
||||||
|
const res = await exchangeCode({
|
||||||
|
integration,
|
||||||
|
code
|
||||||
|
});
|
||||||
|
|
||||||
|
const update: Update = {
|
||||||
|
workspace: workspaceId,
|
||||||
|
integration
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (integration) {
|
||||||
|
case INTEGRATION_VERCEL:
|
||||||
|
update.teamId = res.teamId;
|
||||||
|
break;
|
||||||
|
case INTEGRATION_NETLIFY:
|
||||||
|
update.accountId = res.accountId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
|
workspace: workspaceId,
|
||||||
|
integration
|
||||||
|
}, update, {
|
||||||
|
new: true,
|
||||||
|
upsert: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.refreshToken) {
|
||||||
|
// case: refresh token returned from exchange
|
||||||
|
// set integration auth refresh token
|
||||||
|
await setIntegrationAuthRefreshHelper({
|
||||||
|
integrationAuthId: integrationAuth._id.toString(),
|
||||||
|
refreshToken: res.refreshToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.accessToken) {
|
||||||
|
// case: access token returned from exchange
|
||||||
|
// set integration auth access token
|
||||||
|
await setIntegrationAuthAccessHelper({
|
||||||
|
integrationAuthId: integrationAuth._id.toString(),
|
||||||
|
accessToken: res.accessToken,
|
||||||
|
accessExpiresAt: res.accessExpiresAt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize new integration after exchange
|
||||||
|
await new Integration({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: ENV_DEV,
|
||||||
|
isActive: false,
|
||||||
|
app: null,
|
||||||
|
integration,
|
||||||
|
integrationAuth: integrationAuth._id
|
||||||
|
}).save();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to handle OAuth2 code-token exchange')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sync/push environment variables in workspace with id [workspaceId] to
|
||||||
|
* all active integrations for that workspace
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.workspaceId - id of workspace
|
||||||
|
*/
|
||||||
|
const syncIntegrationsHelper = async ({
|
||||||
|
workspaceId
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
}) => {
|
||||||
|
let integrations;
|
||||||
|
try {
|
||||||
|
|
||||||
|
integrations = await Integration.find({
|
||||||
|
workspace: workspaceId,
|
||||||
|
isActive: true,
|
||||||
|
app: { $ne: null }
|
||||||
|
});
|
||||||
|
|
||||||
|
// for each workspace integration, sync/push secrets
|
||||||
|
// to that integration
|
||||||
|
for await (const integration of integrations) {
|
||||||
|
// get workspace, environment (shared) secrets
|
||||||
|
const secrets = await BotService.getSecrets({
|
||||||
|
workspaceId: integration.workspace.toString(),
|
||||||
|
environment: integration.environment
|
||||||
|
});
|
||||||
|
|
||||||
|
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
|
||||||
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
|
// get integration auth access token
|
||||||
|
const accessToken = await getIntegrationAuthAccessHelper({
|
||||||
|
integrationAuthId: integration.integrationAuth.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// sync secrets to integration
|
||||||
|
await syncSecrets({
|
||||||
|
integration,
|
||||||
|
integrationAuth,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to sync secrets to integrations');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return decrypted refresh token using the bot's copy
|
||||||
|
* of the workspace key for workspace belonging to integration auth
|
||||||
|
* with id [integrationAuthId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @param {String} refreshToken - decrypted refresh token
|
||||||
|
*/
|
||||||
|
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
|
||||||
|
let refreshToken;
|
||||||
|
try {
|
||||||
|
const integrationAuth = await IntegrationAuth
|
||||||
|
.findById(integrationAuthId)
|
||||||
|
.select('+refreshCiphertext +refreshIV +refreshTag');
|
||||||
|
|
||||||
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
|
refreshToken = await BotService.decryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
ciphertext: integrationAuth.refreshCiphertext as string,
|
||||||
|
iv: integrationAuth.refreshIV as string,
|
||||||
|
tag: integrationAuth.refreshTag as string
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get integration refresh token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return decrypted access token using the bot's copy
|
||||||
|
* of the workspace key for workspace belonging to integration auth
|
||||||
|
* with id [integrationAuthId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @returns {String} accessToken - decrypted access token
|
||||||
|
*/
|
||||||
|
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
|
||||||
|
let accessToken;
|
||||||
|
try {
|
||||||
|
const integrationAuth = await IntegrationAuth
|
||||||
|
.findById(integrationAuthId)
|
||||||
|
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext');
|
||||||
|
|
||||||
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
|
accessToken = await BotService.decryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
ciphertext: integrationAuth.accessCiphertext as string,
|
||||||
|
iv: integrationAuth.accessIV as string,
|
||||||
|
tag: integrationAuth.accessTag as string
|
||||||
|
});
|
||||||
|
|
||||||
|
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
|
||||||
|
// there is a access token expiration date
|
||||||
|
// and refresh token to exchange with the OAuth2 server
|
||||||
|
|
||||||
|
if (integrationAuth.accessExpiresAt < new Date()) {
|
||||||
|
// access token is expired
|
||||||
|
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
|
||||||
|
accessToken = await exchangeRefresh({
|
||||||
|
integration: integrationAuth.integration,
|
||||||
|
refreshToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get integration access token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt refresh token [refreshToken] using the bot's copy
|
||||||
|
* of the workspace key for workspace belonging to integration auth
|
||||||
|
* with id [integrationAuthId] and store it
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @param {String} obj.refreshToken - refresh token
|
||||||
|
*/
|
||||||
|
const setIntegrationAuthRefreshHelper = async ({
|
||||||
|
integrationAuthId,
|
||||||
|
refreshToken
|
||||||
|
}: {
|
||||||
|
integrationAuthId: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
let integrationAuth;
|
||||||
|
try {
|
||||||
|
integrationAuth = await IntegrationAuth
|
||||||
|
.findById(integrationAuthId);
|
||||||
|
|
||||||
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
|
const obj = await BotService.encryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
plaintext: refreshToken
|
||||||
|
});
|
||||||
|
|
||||||
|
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
|
_id: integrationAuthId
|
||||||
|
}, {
|
||||||
|
refreshCiphertext: obj.ciphertext,
|
||||||
|
refreshIV: obj.iv,
|
||||||
|
refreshTag: obj.tag
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to set integration auth refresh token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return integrationAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt access token [accessToken] using the bot's copy
|
||||||
|
* of the workspace key for workspace belonging to integration auth
|
||||||
|
* with id [integrationAuthId] and store it along with [accessExpiresAt]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @param {String} obj.accessToken - access token
|
||||||
|
* @param {Date} obj.accessExpiresAt - expiration date of access token
|
||||||
|
*/
|
||||||
|
const setIntegrationAuthAccessHelper = async ({
|
||||||
|
integrationAuthId,
|
||||||
|
accessToken,
|
||||||
|
accessExpiresAt
|
||||||
|
}: {
|
||||||
|
integrationAuthId: string;
|
||||||
|
accessToken: string;
|
||||||
|
accessExpiresAt: Date;
|
||||||
|
}) => {
|
||||||
|
let integrationAuth;
|
||||||
|
try {
|
||||||
|
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
||||||
|
|
||||||
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
|
const obj = await BotService.encryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
plaintext: accessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
|
_id: integrationAuthId
|
||||||
|
}, {
|
||||||
|
accessCiphertext: obj.ciphertext,
|
||||||
|
accessIV: obj.iv,
|
||||||
|
accessTag: obj.tag,
|
||||||
|
accessExpiresAt
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to save integration auth access token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return integrationAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
handleOAuthExchangeHelper,
|
||||||
|
syncIntegrationsHelper,
|
||||||
|
getIntegrationAuthRefreshHelper,
|
||||||
|
getIntegrationAuthAccessHelper,
|
||||||
|
setIntegrationAuthRefreshHelper,
|
||||||
|
setIntegrationAuthAccessHelper
|
||||||
|
}
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { IntegrationAuth } from '../models';
|
|
||||||
import { encryptSymmetric, decryptSymmetric } from '../utils/crypto';
|
|
||||||
import { IIntegrationAuth } from '../models';
|
|
||||||
import {
|
|
||||||
ENCRYPTION_KEY,
|
|
||||||
OAUTH_CLIENT_SECRET_HEROKU,
|
|
||||||
OAUTH_TOKEN_URL_HEROKU
|
|
||||||
} from '../config';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process token exchange and refresh responses from respective OAuth2 authorization servers by
|
|
||||||
* encrypting access and refresh tokens, computing new access token expiration times [accessExpiresAt],
|
|
||||||
* and upserting them into the DB for workspace with id [workspaceId] and integration [integration].
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {String} obj.workspaceId - id of workspace
|
|
||||||
* @param {String} obj.integration - name of integration (e.g. heroku)
|
|
||||||
* @param {Object} obj.res - response from OAuth2 authorization server
|
|
||||||
*/
|
|
||||||
const processOAuthTokenRes = async ({
|
|
||||||
workspaceId,
|
|
||||||
integration,
|
|
||||||
res
|
|
||||||
}: {
|
|
||||||
workspaceId: string;
|
|
||||||
integration: string;
|
|
||||||
res: any;
|
|
||||||
}): Promise<IIntegrationAuth> => {
|
|
||||||
let integrationAuth;
|
|
||||||
try {
|
|
||||||
// encrypt refresh + access tokens
|
|
||||||
const {
|
|
||||||
ciphertext: refreshCiphertext,
|
|
||||||
iv: refreshIV,
|
|
||||||
tag: refreshTag
|
|
||||||
} = encryptSymmetric({
|
|
||||||
plaintext: res.data.refresh_token,
|
|
||||||
key: ENCRYPTION_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
ciphertext: accessCiphertext,
|
|
||||||
iv: accessIV,
|
|
||||||
tag: accessTag
|
|
||||||
} = encryptSymmetric({
|
|
||||||
plaintext: res.data.access_token,
|
|
||||||
key: ENCRYPTION_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
// compute access token expiration date
|
|
||||||
const accessExpiresAt = new Date();
|
|
||||||
accessExpiresAt.setSeconds(
|
|
||||||
accessExpiresAt.getSeconds() + res.data.expires_in
|
|
||||||
);
|
|
||||||
|
|
||||||
// create or replace integration authorization with encrypted tokens
|
|
||||||
// and access token expiration date
|
|
||||||
integrationAuth = await IntegrationAuth.findOneAndUpdate(
|
|
||||||
{ workspace: workspaceId, integration },
|
|
||||||
{
|
|
||||||
workspace: workspaceId,
|
|
||||||
integration,
|
|
||||||
refreshCiphertext,
|
|
||||||
refreshIV,
|
|
||||||
refreshTag,
|
|
||||||
accessCiphertext,
|
|
||||||
accessIV,
|
|
||||||
accessTag,
|
|
||||||
accessExpiresAt
|
|
||||||
},
|
|
||||||
{ upsert: true, new: true }
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error(
|
|
||||||
'Failed to process OAuth2 authorization server token response'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return integrationAuth;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return access token for integration either by decrypting a non-expired access token [accessCiphertext] on
|
|
||||||
* the integration authorization document or by requesting a new one by decrypting and exchanging the
|
|
||||||
* refresh token [refreshCiphertext] with the respective OAuth2 authorization server.
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {IIntegrationAuth} obj.integrationAuth - an integration authorization document
|
|
||||||
* @returns {String} access token - new access token
|
|
||||||
*/
|
|
||||||
const getOAuthAccessToken = async ({
|
|
||||||
integrationAuth
|
|
||||||
}: {
|
|
||||||
integrationAuth: IIntegrationAuth;
|
|
||||||
}) => {
|
|
||||||
let accessToken;
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
refreshCiphertext,
|
|
||||||
refreshIV,
|
|
||||||
refreshTag,
|
|
||||||
accessCiphertext,
|
|
||||||
accessIV,
|
|
||||||
accessTag,
|
|
||||||
accessExpiresAt
|
|
||||||
} = integrationAuth;
|
|
||||||
|
|
||||||
if (
|
|
||||||
refreshCiphertext &&
|
|
||||||
refreshIV &&
|
|
||||||
refreshTag &&
|
|
||||||
accessCiphertext &&
|
|
||||||
accessIV &&
|
|
||||||
accessTag &&
|
|
||||||
accessExpiresAt
|
|
||||||
) {
|
|
||||||
if (accessExpiresAt < new Date()) {
|
|
||||||
// case: access token expired
|
|
||||||
// TODO: fetch another access token
|
|
||||||
|
|
||||||
let clientSecret;
|
|
||||||
switch (integrationAuth.integration) {
|
|
||||||
case 'heroku':
|
|
||||||
clientSecret = OAUTH_CLIENT_SECRET_HEROKU;
|
|
||||||
}
|
|
||||||
|
|
||||||
// record new access token and refresh token
|
|
||||||
// encrypt refresh + access tokens
|
|
||||||
const refreshToken = decryptSymmetric({
|
|
||||||
ciphertext: refreshCiphertext,
|
|
||||||
iv: refreshIV,
|
|
||||||
tag: refreshTag,
|
|
||||||
key: ENCRYPTION_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: make route compatible with other integration types
|
|
||||||
const res = await axios.post(
|
|
||||||
OAUTH_TOKEN_URL_HEROKU, // maybe shouldn't be a config variable?
|
|
||||||
new URLSearchParams({
|
|
||||||
grant_type: 'refresh_token',
|
|
||||||
refresh_token: refreshToken,
|
|
||||||
client_secret: clientSecret
|
|
||||||
} as any)
|
|
||||||
);
|
|
||||||
|
|
||||||
accessToken = res.data.access_token;
|
|
||||||
|
|
||||||
await processOAuthTokenRes({
|
|
||||||
workspaceId: integrationAuth.workspace.toString(),
|
|
||||||
integration: integrationAuth.integration,
|
|
||||||
res
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// case: access token still works
|
|
||||||
accessToken = decryptSymmetric({
|
|
||||||
ciphertext: accessCiphertext,
|
|
||||||
iv: accessIV,
|
|
||||||
tag: accessTag,
|
|
||||||
key: ENCRYPTION_KEY
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to get OAuth2 access token');
|
|
||||||
}
|
|
||||||
|
|
||||||
return accessToken;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { processOAuthTokenRes, getOAuthAccessToken };
|
|
||||||
|
|||||||
@@ -1,6 +1,51 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Membership, Key } from '../models';
|
import { Membership, Key } from '../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
|
||||||
|
* and has at least one of the roles in [acceptedRoles] and statuses in [acceptedStatuses]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.userId - id of user to validate
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
*/
|
||||||
|
const validateMembership = async ({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
acceptedRoles: string[];
|
||||||
|
acceptedStatuses: string[];
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
let membership;
|
||||||
|
try {
|
||||||
|
membership = await Membership.findOne({
|
||||||
|
user: userId,
|
||||||
|
workspace: workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership) throw new Error('Failed to find membership');
|
||||||
|
|
||||||
|
if (!acceptedRoles.includes(membership.role)) {
|
||||||
|
throw new Error('Failed to validate membership role');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!acceptedStatuses.includes(membership.status)) {
|
||||||
|
throw new Error('Failed to validate membership status');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to validate membership');
|
||||||
|
}
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return membership matching criteria specified in query [queryObj]
|
* Return membership matching criteria specified in query [queryObj]
|
||||||
* @param {Object} queryObj - query object
|
* @param {Object} queryObj - query object
|
||||||
@@ -97,4 +142,9 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
|||||||
return deletedMembership;
|
return deletedMembership;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { addMemberships, findMembership, deleteMembership };
|
export {
|
||||||
|
validateMembership,
|
||||||
|
addMemberships,
|
||||||
|
findMembership,
|
||||||
|
deleteMembership
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
|
Bot,
|
||||||
Membership,
|
Membership,
|
||||||
Key,
|
Key,
|
||||||
Secret
|
Secret
|
||||||
} from '../models';
|
} from '../models';
|
||||||
|
import { createBot } from '../helpers/bot';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a workspace with name [name] in organization with id [organizationId]
|
* Create a workspace with name [name] in organization with id [organizationId]
|
||||||
|
* and a bot for it.
|
||||||
* @param {String} name - name of workspace to create.
|
* @param {String} name - name of workspace to create.
|
||||||
* @param {String} organizationId - id of organization to create workspace in
|
* @param {String} organizationId - id of organization to create workspace in
|
||||||
* @param {Object} workspace - new workspace
|
* @param {Object} workspace - new workspace
|
||||||
@@ -21,10 +24,16 @@ const createWorkspace = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
let workspace;
|
let workspace;
|
||||||
try {
|
try {
|
||||||
|
// create workspace
|
||||||
workspace = await new Workspace({
|
workspace = await new Workspace({
|
||||||
name,
|
name,
|
||||||
organization: organizationId
|
organization: organizationId
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
|
const bot = await createBot({
|
||||||
|
name: 'Infisical Bot',
|
||||||
|
workspaceId: workspace._id.toString()
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
@@ -43,6 +52,9 @@ const createWorkspace = async ({
|
|||||||
const deleteWorkspace = async ({ id }: { id: string }) => {
|
const deleteWorkspace = async ({ id }: { id: string }) => {
|
||||||
try {
|
try {
|
||||||
await Workspace.deleteOne({ _id: id });
|
await Workspace.deleteOne({ _id: id });
|
||||||
|
await Bot.deleteOne({
|
||||||
|
workspace: id
|
||||||
|
});
|
||||||
await Membership.deleteMany({
|
await Membership.deleteMany({
|
||||||
workspace: id
|
workspace: id
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
IIntegrationAuth
|
||||||
|
} from '../models';
|
||||||
|
import {
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY,
|
||||||
|
INTEGRATION_HEROKU_API_URL,
|
||||||
|
INTEGRATION_VERCEL_API_URL,
|
||||||
|
INTEGRATION_NETLIFY_API_URL
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of names of apps for integration named [integration]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integration - name of integration
|
||||||
|
* @param {String} obj.accessToken - access token for integration
|
||||||
|
* @returns {Object[]} apps - names of integration apps
|
||||||
|
* @returns {String} apps.name - name of integration app
|
||||||
|
*/
|
||||||
|
const getApps = async ({
|
||||||
|
integrationAuth,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integrationAuth: IIntegrationAuth;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
interface App {
|
||||||
|
name: string;
|
||||||
|
siteId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let apps: App[]; // TODO: add type and define payloads for apps
|
||||||
|
try {
|
||||||
|
switch (integrationAuth.integration) {
|
||||||
|
case INTEGRATION_HEROKU:
|
||||||
|
apps = await getAppsHeroku({
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_VERCEL:
|
||||||
|
apps = await getAppsVercel({
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_NETLIFY:
|
||||||
|
apps = await getAppsNetlify({
|
||||||
|
integrationAuth,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get integration apps');
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of names of apps for Heroku integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.accessToken - access token for Heroku API
|
||||||
|
* @returns {Object[]} apps - names of Heroku apps
|
||||||
|
* @returns {String} apps.name - name of Heroku app
|
||||||
|
*/
|
||||||
|
const getAppsHeroku = async ({
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
let apps;
|
||||||
|
try {
|
||||||
|
const res = (await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/vnd.heroku+json; version=3',
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
})).data;
|
||||||
|
|
||||||
|
apps = res.map((a: any) => ({
|
||||||
|
name: a.name
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get Heroku integration apps');
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of names of apps for Vercel integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.accessToken - access token for Vercel API
|
||||||
|
* @returns {Object[]} apps - names of Vercel apps
|
||||||
|
* @returns {String} apps.name - name of Vercel app
|
||||||
|
*/
|
||||||
|
const getAppsVercel = async ({
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
let apps;
|
||||||
|
try {
|
||||||
|
const res = (await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
})).data;
|
||||||
|
|
||||||
|
apps = res.projects.map((a: any) => ({
|
||||||
|
name: a.name
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get Vercel integration apps');
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of names of sites for Netlify integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.accessToken - access token for Netlify API
|
||||||
|
* @returns {Object[]} apps - names of Netlify sites
|
||||||
|
* @returns {String} apps.name - name of Netlify site
|
||||||
|
*/
|
||||||
|
const getAppsNetlify = async ({
|
||||||
|
integrationAuth,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integrationAuth: IIntegrationAuth;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
let apps;
|
||||||
|
try {
|
||||||
|
const res = (await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
})).data;
|
||||||
|
|
||||||
|
apps = res.map((a: any) => ({
|
||||||
|
name: a.name,
|
||||||
|
siteId: a.site_id
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get Netlify integration apps');
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
getApps
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY,
|
||||||
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
|
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||||
|
ACTION_PUSH_TO_HEROKU
|
||||||
|
} from '../variables';
|
||||||
|
import {
|
||||||
|
SITE_URL,
|
||||||
|
CLIENT_SECRET_HEROKU,
|
||||||
|
CLIENT_ID_VERCEL,
|
||||||
|
CLIENT_ID_NETLIFY,
|
||||||
|
CLIENT_SECRET_VERCEL,
|
||||||
|
CLIENT_SECRET_NETLIFY
|
||||||
|
} from '../config';
|
||||||
|
|
||||||
|
interface ExchangeCodeHerokuResponse {
|
||||||
|
token_type: string;
|
||||||
|
access_token: string;
|
||||||
|
expires_in: number;
|
||||||
|
refresh_token: string;
|
||||||
|
user_id: string;
|
||||||
|
session_nonce?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExchangeCodeVercelResponse {
|
||||||
|
token_type: string;
|
||||||
|
access_token: string;
|
||||||
|
installation_id: string;
|
||||||
|
user_id: string;
|
||||||
|
team_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExchangeCodeNetlifyResponse {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
refresh_token: string;
|
||||||
|
scope: string;
|
||||||
|
created_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2
|
||||||
|
* code-token exchange for integration named [integration]
|
||||||
|
* @param {Object} obj1
|
||||||
|
* @param {String} obj1.integration - name of integration
|
||||||
|
* @param {String} obj1.code - code for code-token exchange
|
||||||
|
* @returns {Object} obj
|
||||||
|
* @returns {String} obj.accessToken - access token for integration
|
||||||
|
* @returns {String} obj.refreshToken - refresh token for integration
|
||||||
|
* @returns {Date} obj.accessExpiresAt - date of expiration for access token
|
||||||
|
* @returns {String} obj.action - integration action for bot sequence
|
||||||
|
*/
|
||||||
|
const exchangeCode = async ({
|
||||||
|
integration,
|
||||||
|
code
|
||||||
|
}: {
|
||||||
|
integration: string;
|
||||||
|
code: string;
|
||||||
|
}) => {
|
||||||
|
let obj = {} as any;
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (integration) {
|
||||||
|
case INTEGRATION_HEROKU:
|
||||||
|
obj = await exchangeCodeHeroku({
|
||||||
|
code
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_VERCEL:
|
||||||
|
obj = await exchangeCodeVercel({
|
||||||
|
code
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_NETLIFY:
|
||||||
|
obj = await exchangeCodeNetlify({
|
||||||
|
code
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed OAuth2 code-token exchange');
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
|
||||||
|
* OAuth2 code-token exchange
|
||||||
|
* @param {Object} obj1
|
||||||
|
* @param {Object} obj1.code - code for code-token exchange
|
||||||
|
* @returns {Object} obj2
|
||||||
|
* @returns {String} obj2.accessToken - access token for Heroku API
|
||||||
|
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
||||||
|
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||||
|
*/
|
||||||
|
const exchangeCodeHeroku = async ({
|
||||||
|
code
|
||||||
|
}: {
|
||||||
|
code: string;
|
||||||
|
}) => {
|
||||||
|
let res: ExchangeCodeHerokuResponse;
|
||||||
|
const accessExpiresAt = new Date();
|
||||||
|
try {
|
||||||
|
res = (await axios.post(
|
||||||
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code: code,
|
||||||
|
client_secret: CLIENT_SECRET_HEROKU
|
||||||
|
} as any)
|
||||||
|
)).data;
|
||||||
|
|
||||||
|
accessExpiresAt.setSeconds(
|
||||||
|
accessExpiresAt.getSeconds() + res.expires_in
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed OAuth2 code-token exchange with Heroku');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ({
|
||||||
|
accessToken: res.access_token,
|
||||||
|
refreshToken: res.refresh_token,
|
||||||
|
accessExpiresAt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
|
||||||
|
* code-token exchange
|
||||||
|
* @param {Object} obj1
|
||||||
|
* @param {Object} obj1.code - code for code-token exchange
|
||||||
|
* @returns {Object} obj2
|
||||||
|
* @returns {String} obj2.accessToken - access token for Heroku API
|
||||||
|
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
||||||
|
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||||
|
*/
|
||||||
|
const exchangeCodeVercel = async ({
|
||||||
|
code
|
||||||
|
}: {
|
||||||
|
code: string;
|
||||||
|
}) => {
|
||||||
|
let res: ExchangeCodeVercelResponse;
|
||||||
|
try {
|
||||||
|
res = (await axios.post(
|
||||||
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
code: code,
|
||||||
|
client_id: CLIENT_ID_VERCEL,
|
||||||
|
client_secret: CLIENT_SECRET_VERCEL,
|
||||||
|
redirect_uri: `${SITE_URL}/vercel`
|
||||||
|
} as any)
|
||||||
|
)).data;
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed OAuth2 code-token exchange with Vercel');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ({
|
||||||
|
accessToken: res.access_token,
|
||||||
|
refreshToken: null,
|
||||||
|
accessExpiresAt: null,
|
||||||
|
teamId: res.team_id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
|
||||||
|
* code-token exchange
|
||||||
|
* @param {Object} obj1
|
||||||
|
* @param {Object} obj1.code - code for code-token exchange
|
||||||
|
* @returns {Object} obj2
|
||||||
|
* @returns {String} obj2.accessToken - access token for Heroku API
|
||||||
|
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
||||||
|
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||||
|
*/
|
||||||
|
const exchangeCodeNetlify = async ({
|
||||||
|
code
|
||||||
|
}: {
|
||||||
|
code: string;
|
||||||
|
}) => {
|
||||||
|
let res: ExchangeCodeNetlifyResponse;
|
||||||
|
let accountId;
|
||||||
|
try {
|
||||||
|
res = (await axios.post(
|
||||||
|
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code: code,
|
||||||
|
client_id: CLIENT_ID_NETLIFY,
|
||||||
|
client_secret: CLIENT_SECRET_NETLIFY,
|
||||||
|
redirect_uri: `${SITE_URL}/netlify`
|
||||||
|
} as any)
|
||||||
|
)).data;
|
||||||
|
|
||||||
|
const res2 = await axios.get(
|
||||||
|
'https://api.netlify.com/api/v1/sites',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${res.access_token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const res3 = (await axios.get(
|
||||||
|
'https://api.netlify.com/api/v1/accounts',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${res.access_token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)).data;
|
||||||
|
|
||||||
|
accountId = res3[0].id;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed OAuth2 code-token exchange with Netlify');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ({
|
||||||
|
accessToken: res.access_token,
|
||||||
|
refreshToken: res.refresh_token,
|
||||||
|
accountId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
exchangeCode
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { exchangeCode } from './exchange';
|
||||||
|
import { exchangeRefresh } from './refresh';
|
||||||
|
import { getApps } from './apps';
|
||||||
|
import { syncSecrets } from './sync';
|
||||||
|
import { revokeAccess } from './revoke';
|
||||||
|
|
||||||
|
export {
|
||||||
|
exchangeCode,
|
||||||
|
exchangeRefresh,
|
||||||
|
getApps,
|
||||||
|
syncSecrets,
|
||||||
|
revokeAccess
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { INTEGRATION_HEROKU } from '../variables';
|
||||||
|
import {
|
||||||
|
CLIENT_SECRET_HEROKU
|
||||||
|
} from '../config';
|
||||||
|
import {
|
||||||
|
INTEGRATION_HEROKU_TOKEN_URL
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new access token by exchanging refresh token [refreshToken] for integration
|
||||||
|
* named [integration]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integration - name of integration
|
||||||
|
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
|
||||||
|
*/
|
||||||
|
const exchangeRefresh = async ({
|
||||||
|
integration,
|
||||||
|
refreshToken
|
||||||
|
}: {
|
||||||
|
integration: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}) => {
|
||||||
|
let accessToken;
|
||||||
|
try {
|
||||||
|
switch (integration) {
|
||||||
|
case INTEGRATION_HEROKU:
|
||||||
|
accessToken = await exchangeRefreshHeroku({
|
||||||
|
refreshToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get new OAuth2 access token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||||
|
* Heroku integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const exchangeRefreshHeroku = async ({
|
||||||
|
refreshToken
|
||||||
|
}: {
|
||||||
|
refreshToken: string;
|
||||||
|
}) => {
|
||||||
|
let accessToken;
|
||||||
|
try {
|
||||||
|
const res = await axios.post(
|
||||||
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
client_secret: CLIENT_SECRET_HEROKU
|
||||||
|
} as any)
|
||||||
|
);
|
||||||
|
|
||||||
|
accessToken = res.data.access_token;
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get new OAuth2 access token for Heroku');
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
exchangeRefresh
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
IIntegrationAuth,
|
||||||
|
IntegrationAuth,
|
||||||
|
Integration
|
||||||
|
} from '../models';
|
||||||
|
import {
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
const revokeAccess = async ({
|
||||||
|
integrationAuth,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integrationAuth: IIntegrationAuth,
|
||||||
|
accessToken: string
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
// add any integration-specific revocation logic
|
||||||
|
switch (integrationAuth.integration) {
|
||||||
|
case INTEGRATION_HEROKU:
|
||||||
|
break;
|
||||||
|
case INTEGRATION_VERCEL:
|
||||||
|
break;
|
||||||
|
case INTEGRATION_NETLIFY:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
||||||
|
_id: integrationAuth._id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deletedIntegrationAuth) {
|
||||||
|
await Integration.deleteMany({
|
||||||
|
integrationAuth: deletedIntegrationAuth._id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to delete integration authorization');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
revokeAccess
|
||||||
|
}
|
||||||
@@ -0,0 +1,598 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
IIntegration, IIntegrationAuth
|
||||||
|
} from '../models';
|
||||||
|
import {
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY,
|
||||||
|
INTEGRATION_HEROKU_API_URL,
|
||||||
|
INTEGRATION_VERCEL_API_URL,
|
||||||
|
INTEGRATION_NETLIFY_API_URL
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
// TODO: need a helper function in the future to handle integration
|
||||||
|
// envar priorities (i.e. prioritize secrets within integration or those on Infisical)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync/push [secrets] to [app] in integration named [integration]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {IIntegration} obj.integration - integration details
|
||||||
|
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
|
||||||
|
* @param {Object} obj.app - app in integration
|
||||||
|
* @param {Object} obj.target - (optional) target (environment) in integration
|
||||||
|
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||||
|
* @param {String} obj.accessToken - access token for integration
|
||||||
|
*/
|
||||||
|
const syncSecrets = async ({
|
||||||
|
integration,
|
||||||
|
integrationAuth,
|
||||||
|
secrets,
|
||||||
|
accessToken,
|
||||||
|
}: {
|
||||||
|
integration: IIntegration;
|
||||||
|
integrationAuth: IIntegrationAuth;
|
||||||
|
secrets: any;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
switch (integration.integration) {
|
||||||
|
case INTEGRATION_HEROKU:
|
||||||
|
await syncSecretsHeroku({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_VERCEL:
|
||||||
|
await syncSecretsVercel({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_NETLIFY:
|
||||||
|
await syncSecretsNetlify({
|
||||||
|
integration,
|
||||||
|
integrationAuth,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: set integration to inactive if it was not synced correctly (send alert?)
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to sync secrets to integration');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync/push [secrets] to Heroku [app]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {IIntegration} obj.integration - integration details
|
||||||
|
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||||
|
*/
|
||||||
|
const syncSecretsHeroku = async ({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integration: IIntegration,
|
||||||
|
secrets: any;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const herokuSecrets = (await axios.get(
|
||||||
|
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/vnd.heroku+json; version=3',
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)).data;
|
||||||
|
|
||||||
|
Object.keys(herokuSecrets).forEach(key => {
|
||||||
|
if (!(key in secrets)) {
|
||||||
|
secrets[key] = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await axios.patch(
|
||||||
|
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
|
||||||
|
secrets,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/vnd.heroku+json; version=3',
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to sync secrets to Heroku');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync/push [secrets] to Heroku [app]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {IIntegration} obj.integration - integration details
|
||||||
|
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||||
|
*/
|
||||||
|
const syncSecretsVercel = async ({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integration: IIntegration,
|
||||||
|
secrets: any;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
interface VercelSecret {
|
||||||
|
id?: string;
|
||||||
|
type: string;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
target: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all (decrypted) secrets back from Vercel in
|
||||||
|
// decrypted format
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
decrypt: "true"
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = (await Promise.all((await axios.get(
|
||||||
|
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
|
||||||
|
{
|
||||||
|
params,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.data
|
||||||
|
.envs
|
||||||
|
.filter((secret: VercelSecret) => secret.target.includes(integration.target))
|
||||||
|
.map(async (secret: VercelSecret) => (await axios.get(
|
||||||
|
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
)).data)
|
||||||
|
)).reduce((obj: any, secret: any) => ({
|
||||||
|
...obj,
|
||||||
|
[secret.key]: secret
|
||||||
|
}), {});
|
||||||
|
|
||||||
|
const updateSecrets: VercelSecret[] = [];
|
||||||
|
const deleteSecrets: VercelSecret[] = [];
|
||||||
|
const newSecrets: VercelSecret[] = [];
|
||||||
|
|
||||||
|
// Identify secrets to create
|
||||||
|
Object.keys(secrets).map((key) => {
|
||||||
|
if (!(key in res)) {
|
||||||
|
// case: secret has been created
|
||||||
|
newSecrets.push({
|
||||||
|
key: key,
|
||||||
|
value: secrets[key],
|
||||||
|
type: 'encrypted',
|
||||||
|
target: [integration.target]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Identify secrets to update and delete
|
||||||
|
Object.keys(res).map((key) => {
|
||||||
|
if (key in secrets) {
|
||||||
|
if (res[key].value !== secrets[key]) {
|
||||||
|
// case: secret value has changed
|
||||||
|
updateSecrets.push({
|
||||||
|
id: res[key].id,
|
||||||
|
key: key,
|
||||||
|
value: secrets[key],
|
||||||
|
type: 'encrypted',
|
||||||
|
target: [integration.target]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// case: secret has been deleted
|
||||||
|
deleteSecrets.push({
|
||||||
|
id: res[key].id,
|
||||||
|
key: key,
|
||||||
|
value: res[key].value,
|
||||||
|
type: 'encrypted',
|
||||||
|
target: [integration.target],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync/push new secrets
|
||||||
|
if (newSecrets.length > 0) {
|
||||||
|
await axios.post(
|
||||||
|
`${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`,
|
||||||
|
newSecrets,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync/push updated secrets
|
||||||
|
if (updateSecrets.length > 0) {
|
||||||
|
updateSecrets.forEach(async (secret: VercelSecret) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
...updatedSecret
|
||||||
|
} = secret;
|
||||||
|
await axios.patch(
|
||||||
|
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||||
|
updatedSecret,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete secrets
|
||||||
|
if (deleteSecrets.length > 0) {
|
||||||
|
deleteSecrets.forEach(async (secret: VercelSecret) => {
|
||||||
|
await axios.delete(
|
||||||
|
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to sync secrets to Vercel');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync/push [secrets] to Netlify site [app]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {IIntegration} obj.integration - integration details
|
||||||
|
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
|
||||||
|
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||||
|
*/
|
||||||
|
const syncSecretsNetlify = async ({
|
||||||
|
integration,
|
||||||
|
integrationAuth,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integration: IIntegration;
|
||||||
|
integrationAuth: IIntegrationAuth;
|
||||||
|
secrets: any;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
// TODO: Netlify revision in progress
|
||||||
|
// try {
|
||||||
|
// const getParams = new URLSearchParams({
|
||||||
|
// context_name: integration.context,
|
||||||
|
// site_id: integration.siteId
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const res = (await axios.get(
|
||||||
|
// `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||||
|
// {
|
||||||
|
// params: getParams,
|
||||||
|
// headers: {
|
||||||
|
// Authorization: `Bearer ${accessToken}`
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ))
|
||||||
|
// .data
|
||||||
|
// .reduce((obj: any, secret: any) => ({
|
||||||
|
// ...obj,
|
||||||
|
// [secret.key]: secret
|
||||||
|
// }), {});
|
||||||
|
|
||||||
|
// res.forEach((r: any) => console.log(r));
|
||||||
|
// console.log('getParams', getParams);
|
||||||
|
|
||||||
|
// interface UpdateNetlifySecret {
|
||||||
|
// key: string;
|
||||||
|
// context: string;
|
||||||
|
// value: string;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// interface DeleteNetlifySecret {
|
||||||
|
// key: string;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// interface NewNetlifySecretValue {
|
||||||
|
// value: string;
|
||||||
|
// context: string;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// interface NewNetlifySecret {
|
||||||
|
// key: string;
|
||||||
|
// values: NewNetlifySecretValue[];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let updateSecrets: UpdateNetlifySecret[] = [];
|
||||||
|
// let deleteSecrets: DeleteNetlifySecret[] = [];
|
||||||
|
// let newSecrets: NewNetlifySecret[] = [];
|
||||||
|
|
||||||
|
// interface NetlifyValue {
|
||||||
|
// id: string;
|
||||||
|
// value: string;
|
||||||
|
// context: string;
|
||||||
|
// role: string;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Identify secrets to create - GOOD
|
||||||
|
// Object.keys(secrets).map((key) => {
|
||||||
|
// if (!(key in res)) {
|
||||||
|
// // case: secret does not exist in Netlify -> create secret
|
||||||
|
// newSecrets.push({
|
||||||
|
// key: key,
|
||||||
|
// values: [{
|
||||||
|
// value: secrets[key],
|
||||||
|
// context: integration.context
|
||||||
|
// }]
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
// // case: secret exists in Netlify
|
||||||
|
|
||||||
|
// const netlifyContextsSet = new Set (res[key].values.map((netlifyValue: NetlifyValue) => netlifyValue.context));
|
||||||
|
|
||||||
|
// // TODO: check context/env.
|
||||||
|
// res[key].values.forEach((netlifyValue: NetlifyValue) => {
|
||||||
|
// if (netlifyValue.context === integration.context) {
|
||||||
|
// // case: Netlify value context matches integration context
|
||||||
|
// // TODO: check if value has changed
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // Identify secrets to update and delete
|
||||||
|
// Object.keys(res).map((key) => {
|
||||||
|
// if (key in secrets) {
|
||||||
|
// // if (res[key] !== secrets[key]) {
|
||||||
|
// // // case: secret value has changed
|
||||||
|
// // updateSecrets.push({
|
||||||
|
// // key: key,
|
||||||
|
// // context: integration.context,
|
||||||
|
// // value: secrets[key]
|
||||||
|
// // });
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // TODO: modify check and record of updated secrets
|
||||||
|
|
||||||
|
// // case 1: new context added.
|
||||||
|
// } else {
|
||||||
|
// // case: secret has been deleted
|
||||||
|
// deleteSecrets.push({
|
||||||
|
// key
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const syncParams = new URLSearchParams({
|
||||||
|
// site_id: integration.siteId
|
||||||
|
// });
|
||||||
|
|
||||||
|
// console.log('Netlify newSecrets', newSecrets);
|
||||||
|
// newSecrets.forEach(secret => console.log(secret.values));
|
||||||
|
// // Sync/push new secrets
|
||||||
|
// if (newSecrets.length > 0) {
|
||||||
|
// await axios.post(
|
||||||
|
// `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||||
|
// newSecrets,
|
||||||
|
// {
|
||||||
|
// params: syncParams,
|
||||||
|
// headers: {
|
||||||
|
// Authorization: `Bearer ${accessToken}`
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// console.log('Netlify updateSecrets', updateSecrets);
|
||||||
|
// // Sync/push updated secrets
|
||||||
|
// if (updateSecrets.length > 0) {
|
||||||
|
|
||||||
|
// updateSecrets.forEach(async (secret: UpdateNetlifySecret) => {
|
||||||
|
// await axios.patch(
|
||||||
|
// `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
|
||||||
|
// {
|
||||||
|
// context: secret.context,
|
||||||
|
// value: secret.value
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// params: syncParams,
|
||||||
|
// headers: {
|
||||||
|
// Authorization: `Bearer ${accessToken}`
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// console.log('Netlify deleteSecrets', deleteSecrets);
|
||||||
|
// // Delete secrets
|
||||||
|
// if (deleteSecrets.length > 0) {
|
||||||
|
// deleteSecrets.forEach(async (secret: DeleteNetlifySecret) => {
|
||||||
|
// await axios.delete(
|
||||||
|
// `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
|
||||||
|
// {
|
||||||
|
// params: syncParams,
|
||||||
|
// headers: {
|
||||||
|
// Authorization: `Bearer ${accessToken}`
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// } catch (err) {
|
||||||
|
// Sentry.setUser(null);
|
||||||
|
// Sentry.captureException(err);
|
||||||
|
// throw new Error('Failed to sync secrets to Heroku');
|
||||||
|
// }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const getParams = new URLSearchParams({
|
||||||
|
context_name: integration.context,
|
||||||
|
site_id: integration.siteId
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = (await axios.get(
|
||||||
|
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||||
|
{
|
||||||
|
params: getParams,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.data
|
||||||
|
.reduce((obj: any, secret: any) => ({
|
||||||
|
...obj,
|
||||||
|
[secret.key]: secret.values[0].value
|
||||||
|
}), {});
|
||||||
|
|
||||||
|
interface UpdateNetlifySecret {
|
||||||
|
key: string;
|
||||||
|
context: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteNetlifySecret {
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NewNetlifySecretValue {
|
||||||
|
value: string;
|
||||||
|
context: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NewNetlifySecret {
|
||||||
|
key: string;
|
||||||
|
values: NewNetlifySecretValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSecrets: UpdateNetlifySecret[] = [];
|
||||||
|
const deleteSecrets: DeleteNetlifySecret[] = [];
|
||||||
|
const newSecrets: NewNetlifySecret[] = [];
|
||||||
|
|
||||||
|
// Identify secrets to create
|
||||||
|
Object.keys(secrets).map((key) => {
|
||||||
|
if (!(key in res)) {
|
||||||
|
// case: secret has been created
|
||||||
|
newSecrets.push({
|
||||||
|
key: key,
|
||||||
|
values: [{
|
||||||
|
value: secrets[key], // include id?
|
||||||
|
context: integration.context
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Identify secrets to update and delete
|
||||||
|
Object.keys(res).map((key) => {
|
||||||
|
if (key in secrets) {
|
||||||
|
if (res[key] !== secrets[key]) {
|
||||||
|
// case: secret value has changed
|
||||||
|
updateSecrets.push({
|
||||||
|
key: key,
|
||||||
|
context: integration.context,
|
||||||
|
value: secrets[key]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// case: secret has been deleted
|
||||||
|
deleteSecrets.push({
|
||||||
|
key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const syncParams = new URLSearchParams({
|
||||||
|
site_id: integration.siteId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync/push new secrets
|
||||||
|
if (newSecrets.length > 0) {
|
||||||
|
await axios.post(
|
||||||
|
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||||
|
newSecrets,
|
||||||
|
{
|
||||||
|
params: syncParams,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync/push updated secrets
|
||||||
|
if (updateSecrets.length > 0) {
|
||||||
|
|
||||||
|
updateSecrets.forEach(async (secret: UpdateNetlifySecret) => {
|
||||||
|
await axios.patch(
|
||||||
|
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
|
||||||
|
{
|
||||||
|
context: secret.context,
|
||||||
|
value: secret.value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params: syncParams,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete secrets
|
||||||
|
if (deleteSecrets.length > 0) {
|
||||||
|
deleteSecrets.forEach(async (secret: DeleteNetlifySecret) => {
|
||||||
|
await axios.delete(
|
||||||
|
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
|
||||||
|
{
|
||||||
|
params: syncParams,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to sync secrets to Heroku');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
syncSecrets
|
||||||
|
}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"heroku": {
|
|
||||||
"name": "Heroku",
|
|
||||||
"type": "oauth2",
|
|
||||||
"clientId": "bc132901-935a-4590-b010-f1857efc380d",
|
|
||||||
"docsLink": ""
|
|
||||||
},
|
|
||||||
"netlify": {
|
|
||||||
"name": "Netlify",
|
|
||||||
"type": "oauth2",
|
|
||||||
"clientId": "",
|
|
||||||
"docsLink": ""
|
|
||||||
},
|
|
||||||
"digitalocean": {
|
|
||||||
"name": "Digital Ocean",
|
|
||||||
"type": "oauth2",
|
|
||||||
"clientId": "",
|
|
||||||
"docsLink": ""
|
|
||||||
},
|
|
||||||
"gcp": {
|
|
||||||
"name": "Google Cloud Platform",
|
|
||||||
"type": "oauth2",
|
|
||||||
"clientId": "",
|
|
||||||
"docsLink": ""
|
|
||||||
},
|
|
||||||
"aws": {
|
|
||||||
"name": "Amazon Web Services",
|
|
||||||
"type": "oauth2",
|
|
||||||
"clientId": "",
|
|
||||||
"docsLink": ""
|
|
||||||
},
|
|
||||||
"azure": {
|
|
||||||
"name": "Microsoft Azure",
|
|
||||||
"type": "oauth2",
|
|
||||||
"clientId": "",
|
|
||||||
"docsLink": ""
|
|
||||||
},
|
|
||||||
"travisci": {
|
|
||||||
"name": "Travis CI",
|
|
||||||
"type": "oauth2",
|
|
||||||
"clientId": "",
|
|
||||||
"docsLink": ""
|
|
||||||
},
|
|
||||||
"circleci": {
|
|
||||||
"name": "Circle CI",
|
|
||||||
"type": "oauth2",
|
|
||||||
"clientId": "",
|
|
||||||
"docsLink": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import requireAuth from './requireAuth';
|
import requireAuth from './requireAuth';
|
||||||
|
import requireBotAuth from './requireBotAuth';
|
||||||
import requireSignupAuth from './requireSignupAuth';
|
import requireSignupAuth from './requireSignupAuth';
|
||||||
import requireWorkspaceAuth from './requireWorkspaceAuth';
|
import requireWorkspaceAuth from './requireWorkspaceAuth';
|
||||||
import requireOrganizationAuth from './requireOrganizationAuth';
|
import requireOrganizationAuth from './requireOrganizationAuth';
|
||||||
@@ -9,6 +10,7 @@ import validateRequest from './validateRequest';
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
requireAuth,
|
requireAuth,
|
||||||
|
requireBotAuth,
|
||||||
requireSignupAuth,
|
requireSignupAuth,
|
||||||
requireWorkspaceAuth,
|
requireWorkspaceAuth,
|
||||||
requireOrganizationAuth,
|
requireOrganizationAuth,
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { Bot } from '../models';
|
||||||
|
import { validateMembership } from '../helpers/membership';
|
||||||
|
|
||||||
|
type req = 'params' | 'body' | 'query';
|
||||||
|
|
||||||
|
const requireBotAuth = ({
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses,
|
||||||
|
location = 'params'
|
||||||
|
}: {
|
||||||
|
acceptedRoles: string[];
|
||||||
|
acceptedStatuses: string[];
|
||||||
|
location?: req;
|
||||||
|
}) => {
|
||||||
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const bot = await Bot.findOne({ _id: req[location].botId });
|
||||||
|
|
||||||
|
if (!bot) {
|
||||||
|
throw new Error('Failed to find bot');
|
||||||
|
}
|
||||||
|
|
||||||
|
await validateMembership({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: bot.workspace.toString(),
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
|
});
|
||||||
|
|
||||||
|
req.bot = bot;
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(401).send({
|
||||||
|
error: 'Failed bot authorization'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default requireBotAuth;
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { Integration, IntegrationAuth, Membership } from '../models';
|
import { Bot, Integration, IntegrationAuth, Membership } from '../models';
|
||||||
import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
import { IntegrationService } from '../services';
|
||||||
|
import { validateMembership } from '../helpers/membership';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate if user on request is a member of workspace with proper roles associated
|
* Validate if user on request is a member of workspace with proper roles associated
|
||||||
@@ -31,24 +32,14 @@ const requireIntegrationAuth = ({
|
|||||||
if (!integration) {
|
if (!integration) {
|
||||||
throw new Error('Failed to find integration');
|
throw new Error('Failed to find integration');
|
||||||
}
|
}
|
||||||
|
|
||||||
const membership = await Membership.findOne({
|
await validateMembership({
|
||||||
user: req.user._id,
|
userId: req.user._id.toString(),
|
||||||
workspace: integration.workspace
|
workspaceId: integration.workspace.toString(),
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membership) {
|
|
||||||
throw new Error('Failed to find integration workspace membership');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptedRoles.includes(membership.role)) {
|
|
||||||
throw new Error('Failed to validate workspace membership role');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptedStatuses.includes(membership.status)) {
|
|
||||||
throw new Error('Failed to validate workspace membership status');
|
|
||||||
}
|
|
||||||
|
|
||||||
const integrationAuth = await IntegrationAuth.findOne({
|
const integrationAuth = await IntegrationAuth.findOne({
|
||||||
_id: integration.integrationAuth
|
_id: integration.integrationAuth
|
||||||
}).select(
|
}).select(
|
||||||
@@ -60,7 +51,9 @@ const requireIntegrationAuth = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.integration = integration;
|
req.integration = integration;
|
||||||
req.accessToken = await getOAuthAccessToken({ integrationAuth });
|
req.accessToken = await IntegrationService.getIntegrationAuthAccess({
|
||||||
|
integrationAuthId: integrationAuth._id.toString()
|
||||||
|
});
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { IntegrationAuth, Membership } from '../models';
|
import { IntegrationAuth } from '../models';
|
||||||
import { decryptSymmetric } from '../utils/crypto';
|
import { IntegrationService } from '../services';
|
||||||
import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
import { validateMembership } from '../helpers/membership';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate if user on request is a member of workspace with proper roles associated
|
* Validate if user on request is a member of workspace with proper roles associated
|
||||||
@@ -10,18 +10,18 @@ import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
|||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String[]} obj.acceptedRoles - accepted workspace roles
|
* @param {String[]} obj.acceptedRoles - accepted workspace roles
|
||||||
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses
|
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses
|
||||||
* @param {Boolean} obj.attachRefresh - whether or not to decrypt and attach integration authorization refresh token onto request
|
* @param {Boolean} obj.attachAccessToken - whether or not to decrypt and attach integration authorization access token onto request
|
||||||
*/
|
*/
|
||||||
const requireIntegrationAuthorizationAuth = ({
|
const requireIntegrationAuthorizationAuth = ({
|
||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
acceptedStatuses
|
acceptedStatuses,
|
||||||
|
attachAccessToken = true
|
||||||
}: {
|
}: {
|
||||||
acceptedRoles: string[];
|
acceptedRoles: string[];
|
||||||
acceptedStatuses: string[];
|
acceptedStatuses: string[];
|
||||||
|
attachAccessToken?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
// (authorization) integration authorization middleware
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { integrationAuthId } = req.params;
|
const { integrationAuthId } = req.params;
|
||||||
|
|
||||||
@@ -34,30 +34,21 @@ const requireIntegrationAuthorizationAuth = ({
|
|||||||
if (!integrationAuth) {
|
if (!integrationAuth) {
|
||||||
throw new Error('Failed to find integration authorization');
|
throw new Error('Failed to find integration authorization');
|
||||||
}
|
}
|
||||||
|
|
||||||
const membership = await Membership.findOne({
|
await validateMembership({
|
||||||
user: req.user._id,
|
userId: req.user._id.toString(),
|
||||||
workspace: integrationAuth.workspace
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membership) {
|
|
||||||
throw new Error(
|
|
||||||
'Failed to find integration authorization workspace membership'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptedRoles.includes(membership.role)) {
|
|
||||||
throw new Error('Failed to validate workspace membership role');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptedStatuses.includes(membership.status)) {
|
|
||||||
throw new Error('Failed to validate workspace membership status');
|
|
||||||
}
|
|
||||||
|
|
||||||
req.integrationAuth = integrationAuth;
|
req.integrationAuth = integrationAuth;
|
||||||
|
if (attachAccessToken) {
|
||||||
// TODO: make compatible with other integration types since they won't necessarily have access tokens
|
req.accessToken = await IntegrationService.getIntegrationAuthAccess({
|
||||||
req.accessToken = await getOAuthAccessToken({ integrationAuth });
|
integrationAuthId: integrationAuth._id.toString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { Membership, IWorkspace } from '../models';
|
import { validateMembership } from '../helpers/membership';
|
||||||
|
|
||||||
type req = 'params' | 'body' | 'query';
|
type req = 'params' | 'body' | 'query';
|
||||||
|
|
||||||
@@ -25,24 +25,12 @@ const requireWorkspaceAuth = ({
|
|||||||
// workspace authorization middleware
|
// workspace authorization middleware
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// validate workspace membership
|
const membership = await validateMembership({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
const membership = await Membership.findOne({
|
workspaceId: req[location].workspaceId,
|
||||||
user: req.user._id,
|
acceptedRoles,
|
||||||
workspace: req[location].workspaceId
|
acceptedStatuses
|
||||||
}).populate<{ workspace: IWorkspace }>('workspace');
|
});
|
||||||
|
|
||||||
if (!membership) {
|
|
||||||
throw new Error('Failed to find workspace membership');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptedRoles.includes(membership.role)) {
|
|
||||||
throw new Error('Failed to validate workspace membership role');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptedStatuses.includes(membership.status)) {
|
|
||||||
throw new Error('Failed to validate workspace membership status');
|
|
||||||
}
|
|
||||||
|
|
||||||
req.membership = membership;
|
req.membership = membership;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
|
||||||
|
export interface IBot {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
name: string;
|
||||||
|
workspace: Types.ObjectId;
|
||||||
|
isActive: boolean;
|
||||||
|
publicKey: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const botSchema = new Schema<IBot>(
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Workspace',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
publicKey: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
encryptedPrivateKey: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
iv: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
tag: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
select: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Bot = model<IBot>('Bot', botSchema);
|
||||||
|
|
||||||
|
export default Bot;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
|
||||||
|
export interface IBotKey {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
encryptedKey: string;
|
||||||
|
nonce: string;
|
||||||
|
sender: Types.ObjectId;
|
||||||
|
bot: Types.ObjectId;
|
||||||
|
workspace: Types.ObjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const botKeySchema = new Schema<IBotKey>(
|
||||||
|
{
|
||||||
|
encryptedKey: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
nonce: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
bot: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Bot',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Workspace',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const BotKey = model<IBotKey>('BotKey', botKeySchema);
|
||||||
|
|
||||||
|
export default BotKey;
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import BackupPrivateKey, { IBackupPrivateKey } from './backupPrivateKey';
|
import BackupPrivateKey, { IBackupPrivateKey } from './backupPrivateKey';
|
||||||
|
import Bot, { IBot } from './bot';
|
||||||
|
import BotKey, { IBotKey } from './botKey';
|
||||||
import IncidentContactOrg, { IIncidentContactOrg } from './incidentContactOrg';
|
import IncidentContactOrg, { IIncidentContactOrg } from './incidentContactOrg';
|
||||||
import Integration, { IIntegration } from './integration';
|
import Integration, { IIntegration } from './integration';
|
||||||
import IntegrationAuth, { IIntegrationAuth } from './integrationAuth';
|
import IntegrationAuth, { IIntegrationAuth } from './integrationAuth';
|
||||||
@@ -16,6 +18,10 @@ import Workspace, { IWorkspace } from './workspace';
|
|||||||
export {
|
export {
|
||||||
BackupPrivateKey,
|
BackupPrivateKey,
|
||||||
IBackupPrivateKey,
|
IBackupPrivateKey,
|
||||||
|
Bot,
|
||||||
|
IBot,
|
||||||
|
BotKey,
|
||||||
|
IBotKey,
|
||||||
IncidentContactOrg,
|
IncidentContactOrg,
|
||||||
IIncidentContactOrg,
|
IIncidentContactOrg,
|
||||||
Integration,
|
Integration,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
ENV_STAGING,
|
ENV_STAGING,
|
||||||
ENV_PROD,
|
ENV_PROD,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY
|
INTEGRATION_NETLIFY
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
|
|
||||||
@@ -14,7 +15,10 @@ export interface IIntegration {
|
|||||||
environment: 'dev' | 'test' | 'staging' | 'prod';
|
environment: 'dev' | 'test' | 'staging' | 'prod';
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
app: string;
|
app: string;
|
||||||
integration: 'heroku' | 'netlify';
|
target: string;
|
||||||
|
context: string;
|
||||||
|
siteId: string;
|
||||||
|
integration: 'heroku' | 'vercel' | 'netlify';
|
||||||
integrationAuth: Types.ObjectId;
|
integrationAuth: Types.ObjectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,15 +38,29 @@ const integrationSchema = new Schema<IIntegration>(
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
app: {
|
app: { // name of app in provider
|
||||||
// name of app in provider
|
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null
|
||||||
required: true
|
},
|
||||||
|
target: { // vercel-specific target (environment)
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
context: { // netlify-specific context (deploy)
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
siteId: { // netlify-specific site (app) id
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
},
|
},
|
||||||
integration: {
|
integration: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [INTEGRATION_HEROKU, INTEGRATION_NETLIFY],
|
enum: [
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY
|
||||||
|
],
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
integrationAuth: {
|
integrationAuth: {
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from 'mongoose';
|
||||||
import { INTEGRATION_HEROKU, INTEGRATION_NETLIFY } from '../variables';
|
import {
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
export interface IIntegrationAuth {
|
export interface IIntegrationAuth {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
workspace: Types.ObjectId;
|
workspace: Types.ObjectId;
|
||||||
integration: 'heroku' | 'netlify';
|
integration: 'heroku' | 'vercel' | 'netlify';
|
||||||
|
teamId: string;
|
||||||
|
accountId: string;
|
||||||
refreshCiphertext?: string;
|
refreshCiphertext?: string;
|
||||||
refreshIV?: string;
|
refreshIV?: string;
|
||||||
refreshTag?: string;
|
refreshTag?: string;
|
||||||
@@ -22,9 +28,19 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
|||||||
},
|
},
|
||||||
integration: {
|
integration: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [INTEGRATION_HEROKU, INTEGRATION_NETLIFY],
|
enum: [
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY
|
||||||
|
],
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
teamId: { // vercel-specific integration param
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
accountId: { // netlify-specific integration param
|
||||||
|
type: String
|
||||||
|
},
|
||||||
refreshCiphertext: {
|
refreshCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import { body, param } from 'express-validator';
|
||||||
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireBotAuth,
|
||||||
|
requireWorkspaceAuth,
|
||||||
|
validateRequest
|
||||||
|
} from '../middleware';
|
||||||
|
import { botController } from '../controllers';
|
||||||
|
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:workspaceId',
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [COMPLETED, GRANTED]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
botController.getBotByWorkspaceId
|
||||||
|
);
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
'/:botId/active',
|
||||||
|
requireAuth,
|
||||||
|
requireBotAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [COMPLETED, GRANTED]
|
||||||
|
}),
|
||||||
|
body('isActive').isBoolean(),
|
||||||
|
body('botKey'),
|
||||||
|
validateRequest,
|
||||||
|
botController.setBotActiveState
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import signup from './signup';
|
import signup from './signup';
|
||||||
|
import bot from './bot';
|
||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
import user from './user';
|
import user from './user';
|
||||||
import userAction from './userAction';
|
import userAction from './userAction';
|
||||||
@@ -18,6 +19,7 @@ import integrationAuth from './integrationAuth';
|
|||||||
export {
|
export {
|
||||||
signup,
|
signup,
|
||||||
auth,
|
auth,
|
||||||
|
bot,
|
||||||
user,
|
user,
|
||||||
userAction,
|
userAction,
|
||||||
organization,
|
organization,
|
||||||
|
|||||||
@@ -9,22 +9,6 @@ import { ADMIN, MEMBER, GRANTED } from '../variables';
|
|||||||
import { body, param } from 'express-validator';
|
import { body, param } from 'express-validator';
|
||||||
import { integrationController } from '../controllers';
|
import { integrationController } from '../controllers';
|
||||||
|
|
||||||
router.get('/integrations', requireAuth, integrationController.getIntegrations);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/:integrationId/sync',
|
|
||||||
requireAuth,
|
|
||||||
requireIntegrationAuth({
|
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
|
||||||
acceptedStatuses: [GRANTED]
|
|
||||||
}),
|
|
||||||
param('integrationId').exists().trim(),
|
|
||||||
body('key').exists(),
|
|
||||||
body('secrets').exists(),
|
|
||||||
validateRequest,
|
|
||||||
integrationController.syncIntegration
|
|
||||||
);
|
|
||||||
|
|
||||||
router.patch(
|
router.patch(
|
||||||
'/:integrationId',
|
'/:integrationId',
|
||||||
requireAuth,
|
requireAuth,
|
||||||
@@ -32,10 +16,15 @@ router.patch(
|
|||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
acceptedStatuses: [GRANTED]
|
acceptedStatuses: [GRANTED]
|
||||||
}),
|
}),
|
||||||
param('integrationId'),
|
param('integrationId').exists().trim(),
|
||||||
body('update'),
|
body('app').exists().trim(),
|
||||||
|
body('environment').exists().trim(),
|
||||||
|
body('isActive').exists().isBoolean(),
|
||||||
|
body('target').exists(),
|
||||||
|
body('context').exists(),
|
||||||
|
body('siteId').exists(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
integrationController.modifyIntegration
|
integrationController.updateIntegration
|
||||||
);
|
);
|
||||||
|
|
||||||
router.delete(
|
router.delete(
|
||||||
@@ -45,7 +34,7 @@ router.delete(
|
|||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
acceptedStatuses: [GRANTED]
|
acceptedStatuses: [GRANTED]
|
||||||
}),
|
}),
|
||||||
param('integrationId'),
|
param('integrationId').exists().trim(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
integrationController.deleteIntegration
|
integrationController.deleteIntegration
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ import {
|
|||||||
import { ADMIN, MEMBER, GRANTED } from '../variables';
|
import { ADMIN, MEMBER, GRANTED } from '../variables';
|
||||||
import { integrationAuthController } from '../controllers';
|
import { integrationAuthController } from '../controllers';
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/integration-options',
|
||||||
|
requireAuth,
|
||||||
|
integrationAuthController.getIntegrationOptions
|
||||||
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/oauth-token',
|
'/oauth-token',
|
||||||
requireAuth,
|
requireAuth,
|
||||||
@@ -22,7 +28,7 @@ router.post(
|
|||||||
body('code').exists().trim().notEmpty(),
|
body('code').exists().trim().notEmpty(),
|
||||||
body('integration').exists().trim().notEmpty(),
|
body('integration').exists().trim().notEmpty(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
integrationAuthController.integrationAuthOauthExchange
|
integrationAuthController.oAuthExchange
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
@@ -42,7 +48,8 @@ router.delete(
|
|||||||
requireAuth,
|
requireAuth,
|
||||||
requireIntegrationAuthorizationAuth({
|
requireIntegrationAuthorizationAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
acceptedStatuses: [GRANTED]
|
acceptedStatuses: [GRANTED],
|
||||||
|
attachAccessToken: false
|
||||||
}),
|
}),
|
||||||
param('integrationAuthId'),
|
param('integrationAuthId'),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ router.post(
|
|||||||
passwordController.changePassword
|
passwordController.changePassword
|
||||||
);
|
);
|
||||||
|
|
||||||
// NEW
|
|
||||||
router.post(
|
router.post(
|
||||||
'/email/password-reset',
|
'/email/password-reset',
|
||||||
passwordLimiter,
|
passwordLimiter,
|
||||||
@@ -36,7 +35,6 @@ router.post(
|
|||||||
passwordController.emailPasswordReset
|
passwordController.emailPasswordReset
|
||||||
);
|
);
|
||||||
|
|
||||||
// NEW
|
|
||||||
router.post(
|
router.post(
|
||||||
'/email/password-reset-verify',
|
'/email/password-reset-verify',
|
||||||
passwordLimiter,
|
passwordLimiter,
|
||||||
@@ -46,7 +44,6 @@ router.post(
|
|||||||
passwordController.emailPasswordResetVerify
|
passwordController.emailPasswordResetVerify
|
||||||
);
|
);
|
||||||
|
|
||||||
// NEW
|
|
||||||
router.get(
|
router.get(
|
||||||
'/backup-private-key',
|
'/backup-private-key',
|
||||||
passwordLimiter,
|
passwordLimiter,
|
||||||
@@ -68,7 +65,6 @@ router.post(
|
|||||||
passwordController.createBackupPrivateKey
|
passwordController.createBackupPrivateKey
|
||||||
);
|
);
|
||||||
|
|
||||||
// NEW
|
|
||||||
router.post(
|
router.post(
|
||||||
'/password-reset',
|
'/password-reset',
|
||||||
requireSignupAuth,
|
requireSignupAuth,
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import {
|
||||||
|
getSecretsHelper,
|
||||||
|
encryptSymmetricHelper,
|
||||||
|
decryptSymmetricHelper
|
||||||
|
} from '../helpers/bot';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to handle bot actions
|
||||||
|
*/
|
||||||
|
class BotService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return decrypted secrets for workspace with id [workspaceId] and
|
||||||
|
* environment [environmen] shared to bot.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace of secrets
|
||||||
|
* @param {String} obj.environment - environment for secrets
|
||||||
|
* @returns {Object} secretObj - object where keys are secret keys and values are secret values
|
||||||
|
*/
|
||||||
|
static async getSecrets({
|
||||||
|
workspaceId,
|
||||||
|
environment
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
}) {
|
||||||
|
return await getSecretsHelper({
|
||||||
|
workspaceId,
|
||||||
|
environment
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return symmetrically encrypted [plaintext] using the
|
||||||
|
* bot's copy of the workspace key for workspace with id [workspaceId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @param {String} obj.plaintext - plaintext to encrypt
|
||||||
|
*/
|
||||||
|
static async encryptSymmetric({
|
||||||
|
workspaceId,
|
||||||
|
plaintext
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
plaintext: string;
|
||||||
|
}) {
|
||||||
|
return await encryptSymmetricHelper({
|
||||||
|
workspaceId,
|
||||||
|
plaintext
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return symmetrically decrypted [ciphertext] using the
|
||||||
|
* bot's copy of the workspace key for workspace with id [workspaceId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||||
|
* @param {String} obj.iv - iv
|
||||||
|
* @param {String} obj.tag - tag
|
||||||
|
*/
|
||||||
|
static async decryptSymmetric({
|
||||||
|
workspaceId,
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
ciphertext: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
}) {
|
||||||
|
return await decryptSymmetricHelper({
|
||||||
|
workspaceId,
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BotService;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Bot, IBot } from '../models';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { handleEventHelper } from '../helpers/event';
|
||||||
|
|
||||||
|
interface Event {
|
||||||
|
name: string;
|
||||||
|
workspaceId: string;
|
||||||
|
payload: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to handle events.
|
||||||
|
*/
|
||||||
|
class EventService {
|
||||||
|
/**
|
||||||
|
* Handle event [event]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Event} obj.event - an event
|
||||||
|
* @param {String} obj.event.name - name of event
|
||||||
|
* @param {String} obj.event.workspaceId - id of workspace that event is part of
|
||||||
|
* @param {Object} obj.event.payload - payload of event (depends on event)
|
||||||
|
*/
|
||||||
|
static async handleEvent({ event }: { event: Event }): Promise<void> {
|
||||||
|
await handleEventHelper({
|
||||||
|
event
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventService;
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Integration
|
||||||
|
} from '../models';
|
||||||
|
import {
|
||||||
|
handleOAuthExchangeHelper,
|
||||||
|
syncIntegrationsHelper,
|
||||||
|
getIntegrationAuthRefreshHelper,
|
||||||
|
getIntegrationAuthAccessHelper,
|
||||||
|
setIntegrationAuthRefreshHelper,
|
||||||
|
setIntegrationAuthAccessHelper,
|
||||||
|
} from '../helpers/integration';
|
||||||
|
import { exchangeCode } from '../integrations';
|
||||||
|
import {
|
||||||
|
ENV_DEV,
|
||||||
|
EVENT_PUSH_SECRETS
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
// should sync stuff be here too? Probably.
|
||||||
|
// TODO: move bot functions to IntegrationService.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to handle integrations
|
||||||
|
*/
|
||||||
|
class IntegrationService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
|
||||||
|
* named [integration]
|
||||||
|
* - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
|
||||||
|
* - Add placeholder inactive integration
|
||||||
|
* - Create bot sequence for integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @param {String} obj.integration - name of integration
|
||||||
|
* @param {String} obj.code - code
|
||||||
|
*/
|
||||||
|
static async handleOAuthExchange({
|
||||||
|
workspaceId,
|
||||||
|
integration,
|
||||||
|
code
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
integration: string;
|
||||||
|
code: string;
|
||||||
|
}) {
|
||||||
|
await handleOAuthExchangeHelper({
|
||||||
|
workspaceId,
|
||||||
|
integration,
|
||||||
|
code
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync/push environment variables in workspace with id [workspaceId] to
|
||||||
|
* all associated integrations
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.workspaceId - id of workspace
|
||||||
|
*/
|
||||||
|
static async syncIntegrations({
|
||||||
|
workspaceId
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
}) {
|
||||||
|
return await syncIntegrationsHelper({
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return decrypted refresh token for integration auth
|
||||||
|
* with id [integrationAuthId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @param {String} refreshToken - decrypted refresh token
|
||||||
|
*/
|
||||||
|
static async getIntegrationAuthRefresh({ integrationAuthId }: { integrationAuthId: string}) {
|
||||||
|
return await getIntegrationAuthRefreshHelper({
|
||||||
|
integrationAuthId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return decrypted access token for integration auth
|
||||||
|
* with id [integrationAuthId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @param {String} accessToken - decrypted access token
|
||||||
|
*/
|
||||||
|
static async getIntegrationAuthAccess({ integrationAuthId }: { integrationAuthId: string}) {
|
||||||
|
return await getIntegrationAuthAccessHelper({
|
||||||
|
integrationAuthId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt refresh token [refreshToken] using the bot's copy
|
||||||
|
* of the workspace key for workspace belonging to integration auth
|
||||||
|
* with id [integrationAuthId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @param {String} obj.refreshToken - refresh token
|
||||||
|
* @returns {IntegrationAuth} integrationAuth - updated integration auth
|
||||||
|
*/
|
||||||
|
static async setIntegrationAuthRefresh({
|
||||||
|
integrationAuthId,
|
||||||
|
refreshToken
|
||||||
|
}: {
|
||||||
|
integrationAuthId: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}) {
|
||||||
|
return await setIntegrationAuthRefreshHelper({
|
||||||
|
integrationAuthId,
|
||||||
|
refreshToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt access token [accessToken] using the bot's copy
|
||||||
|
* of the workspace key for workspace belonging to integration auth
|
||||||
|
* with id [integrationAuthId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @param {String} obj.accessToken - access token
|
||||||
|
* @param {String} obj.accessExpiresAt - expiration date of access token
|
||||||
|
* @returns {IntegrationAuth} - updated integration auth
|
||||||
|
*/
|
||||||
|
static async setIntegrationAuthAccess({
|
||||||
|
integrationAuthId,
|
||||||
|
accessToken,
|
||||||
|
accessExpiresAt
|
||||||
|
}: {
|
||||||
|
integrationAuthId: string;
|
||||||
|
accessToken: string;
|
||||||
|
accessExpiresAt: Date;
|
||||||
|
}) {
|
||||||
|
return await setIntegrationAuthAccessHelper({
|
||||||
|
integrationAuthId,
|
||||||
|
accessToken,
|
||||||
|
accessExpiresAt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IntegrationService;
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
import postHogClient from './PostHogClient';
|
import postHogClient from './PostHogClient';
|
||||||
|
import BotService from './BotService';
|
||||||
|
import EventService from './EventService';
|
||||||
|
import IntegrationService from './IntegrationService';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
postHogClient
|
postHogClient,
|
||||||
|
BotService,
|
||||||
|
EventService,
|
||||||
|
IntegrationService
|
||||||
}
|
}
|
||||||
Vendored
+1
@@ -11,6 +11,7 @@ declare global {
|
|||||||
membershipOrg: any;
|
membershipOrg: any;
|
||||||
integration: any;
|
integration: any;
|
||||||
integrationAuth: any;
|
integrationAuth: any;
|
||||||
|
bot: any;
|
||||||
serviceToken: any;
|
serviceToken: any;
|
||||||
accessToken: any;
|
accessToken: any;
|
||||||
query?: any;
|
query?: any;
|
||||||
|
|||||||
@@ -2,6 +2,21 @@ import nacl from 'tweetnacl';
|
|||||||
import util from 'tweetnacl-util';
|
import util from 'tweetnacl-util';
|
||||||
import AesGCM from './aes-gcm';
|
import AesGCM from './aes-gcm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new base64, NaCl, public-private key pair.
|
||||||
|
* @returns {Object} obj
|
||||||
|
* @returns {String} obj.publicKey - base64, NaCl, public key
|
||||||
|
* @returns {String} obj.privateKey - base64, NaCl, private key
|
||||||
|
*/
|
||||||
|
const generateKeyPair = () => {
|
||||||
|
const pair = nacl.box.keyPair();
|
||||||
|
|
||||||
|
return ({
|
||||||
|
publicKey: util.encodeBase64(pair.publicKey),
|
||||||
|
privateKey: util.encodeBase64(pair.secretKey)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return assymmetrically encrypted [plaintext] using [publicKey] where
|
* Return assymmetrically encrypted [plaintext] using [publicKey] where
|
||||||
* [publicKey] likely belongs to the recipient.
|
* [publicKey] likely belongs to the recipient.
|
||||||
@@ -81,7 +96,7 @@ const decryptAsymmetric = ({
|
|||||||
* Return symmetrically encrypted [plaintext] using [key].
|
* Return symmetrically encrypted [plaintext] using [key].
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String} obj.plaintext - plaintext to encrypt
|
* @param {String} obj.plaintext - plaintext to encrypt
|
||||||
* @param {String} obj.key - 16-byte hex key
|
* @param {String} obj.key - hex key
|
||||||
*/
|
*/
|
||||||
const encryptSymmetric = ({
|
const encryptSymmetric = ({
|
||||||
plaintext,
|
plaintext,
|
||||||
@@ -114,7 +129,7 @@ const encryptSymmetric = ({
|
|||||||
* @param {String} obj.ciphertext - ciphertext to decrypt
|
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||||
* @param {String} obj.iv - iv
|
* @param {String} obj.iv - iv
|
||||||
* @param {String} obj.tag - tag
|
* @param {String} obj.tag - tag
|
||||||
* @param {String} obj.key - 32-byte hex key
|
* @param {String} obj.key - hex key
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const decryptSymmetric = ({
|
const decryptSymmetric = ({
|
||||||
@@ -139,6 +154,7 @@ const decryptSymmetric = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
generateKeyPair,
|
||||||
encryptAsymmetric,
|
encryptAsymmetric,
|
||||||
decryptAsymmetric,
|
decryptAsymmetric,
|
||||||
encryptSymmetric,
|
encryptSymmetric,
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
// membership roles
|
|
||||||
const OWNER = 'owner';
|
|
||||||
const ADMIN = 'admin';
|
|
||||||
const MEMBER = 'member';
|
|
||||||
|
|
||||||
// membership statuses
|
|
||||||
const INVITED = 'invited';
|
|
||||||
|
|
||||||
// -- organization
|
|
||||||
const ACCEPTED = 'accepted';
|
|
||||||
|
|
||||||
// -- workspace
|
|
||||||
const COMPLETED = 'completed';
|
|
||||||
const GRANTED = 'granted';
|
|
||||||
|
|
||||||
// subscriptions
|
|
||||||
const PLAN_STARTER = 'starter';
|
|
||||||
const PLAN_PRO = 'pro';
|
|
||||||
|
|
||||||
// secrets
|
|
||||||
const SECRET_SHARED = 'shared';
|
|
||||||
const SECRET_PERSONAL = 'personal';
|
|
||||||
|
|
||||||
// environments
|
|
||||||
const ENV_DEV = 'dev';
|
|
||||||
const ENV_TESTING = 'test';
|
|
||||||
const ENV_STAGING = 'staging';
|
|
||||||
const ENV_PROD = 'prod';
|
|
||||||
const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]);
|
|
||||||
|
|
||||||
// integrations
|
|
||||||
const INTEGRATION_HEROKU = 'heroku';
|
|
||||||
const INTEGRATION_NETLIFY = 'netlify';
|
|
||||||
const INTEGRATION_SET = new Set([INTEGRATION_HEROKU, INTEGRATION_NETLIFY]);
|
|
||||||
|
|
||||||
// integration types
|
|
||||||
const INTEGRATION_OAUTH2 = 'oauth2';
|
|
||||||
|
|
||||||
export {
|
|
||||||
OWNER,
|
|
||||||
ADMIN,
|
|
||||||
MEMBER,
|
|
||||||
INVITED,
|
|
||||||
ACCEPTED,
|
|
||||||
COMPLETED,
|
|
||||||
GRANTED,
|
|
||||||
PLAN_STARTER,
|
|
||||||
PLAN_PRO,
|
|
||||||
SECRET_SHARED,
|
|
||||||
SECRET_PERSONAL,
|
|
||||||
ENV_DEV,
|
|
||||||
ENV_TESTING,
|
|
||||||
ENV_STAGING,
|
|
||||||
ENV_PROD,
|
|
||||||
ENV_SET,
|
|
||||||
INTEGRATION_HEROKU,
|
|
||||||
INTEGRATION_NETLIFY,
|
|
||||||
INTEGRATION_SET,
|
|
||||||
INTEGRATION_OAUTH2
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const ACTION_PUSH_TO_HEROKU = 'pushToHeroku';
|
||||||
|
|
||||||
|
export {
|
||||||
|
ACTION_PUSH_TO_HEROKU
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// environments
|
||||||
|
const ENV_DEV = 'dev';
|
||||||
|
const ENV_TESTING = 'test';
|
||||||
|
const ENV_STAGING = 'staging';
|
||||||
|
const ENV_PROD = 'prod';
|
||||||
|
const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]);
|
||||||
|
|
||||||
|
export {
|
||||||
|
ENV_DEV,
|
||||||
|
ENV_TESTING,
|
||||||
|
ENV_STAGING,
|
||||||
|
ENV_PROD,
|
||||||
|
ENV_SET
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const EVENT_PUSH_SECRETS = 'pushSecrets';
|
||||||
|
const EVENT_PULL_SECRETS = 'pullSecrets';
|
||||||
|
|
||||||
|
export {
|
||||||
|
EVENT_PUSH_SECRETS,
|
||||||
|
EVENT_PULL_SECRETS
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
ENV_DEV,
|
||||||
|
ENV_TESTING,
|
||||||
|
ENV_STAGING,
|
||||||
|
ENV_PROD,
|
||||||
|
ENV_SET
|
||||||
|
} from './environment';
|
||||||
|
import {
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY,
|
||||||
|
INTEGRATION_SET,
|
||||||
|
INTEGRATION_OAUTH2,
|
||||||
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
|
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||||
|
INTEGRATION_HEROKU_API_URL,
|
||||||
|
INTEGRATION_VERCEL_API_URL,
|
||||||
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
|
INTEGRATION_OPTIONS
|
||||||
|
} from './integration';
|
||||||
|
import {
|
||||||
|
OWNER,
|
||||||
|
ADMIN,
|
||||||
|
MEMBER,
|
||||||
|
INVITED,
|
||||||
|
ACCEPTED,
|
||||||
|
COMPLETED,
|
||||||
|
GRANTED
|
||||||
|
} from './organization';
|
||||||
|
import {
|
||||||
|
SECRET_SHARED,
|
||||||
|
SECRET_PERSONAL
|
||||||
|
} from './secret';
|
||||||
|
import {
|
||||||
|
PLAN_STARTER,
|
||||||
|
PLAN_PRO
|
||||||
|
} from './stripe';
|
||||||
|
import {
|
||||||
|
EVENT_PUSH_SECRETS,
|
||||||
|
EVENT_PULL_SECRETS
|
||||||
|
} from './event';
|
||||||
|
import {
|
||||||
|
ACTION_PUSH_TO_HEROKU
|
||||||
|
} from './action';
|
||||||
|
|
||||||
|
export {
|
||||||
|
OWNER,
|
||||||
|
ADMIN,
|
||||||
|
MEMBER,
|
||||||
|
INVITED,
|
||||||
|
ACCEPTED,
|
||||||
|
COMPLETED,
|
||||||
|
GRANTED,
|
||||||
|
PLAN_STARTER,
|
||||||
|
PLAN_PRO,
|
||||||
|
SECRET_SHARED,
|
||||||
|
SECRET_PERSONAL,
|
||||||
|
ENV_DEV,
|
||||||
|
ENV_TESTING,
|
||||||
|
ENV_STAGING,
|
||||||
|
ENV_PROD,
|
||||||
|
ENV_SET,
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY,
|
||||||
|
INTEGRATION_SET,
|
||||||
|
INTEGRATION_OAUTH2,
|
||||||
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
|
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||||
|
INTEGRATION_HEROKU_API_URL,
|
||||||
|
INTEGRATION_VERCEL_API_URL,
|
||||||
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
|
EVENT_PUSH_SECRETS,
|
||||||
|
EVENT_PULL_SECRETS,
|
||||||
|
ACTION_PUSH_TO_HEROKU,
|
||||||
|
INTEGRATION_OPTIONS
|
||||||
|
};
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import {
|
||||||
|
CLIENT_ID_HEROKU,
|
||||||
|
CLIENT_ID_NETLIFY,
|
||||||
|
CLIENT_SLUG_VERCEL
|
||||||
|
} from '../config';
|
||||||
|
|
||||||
|
// integrations
|
||||||
|
const INTEGRATION_HEROKU = 'heroku';
|
||||||
|
const INTEGRATION_VERCEL = 'vercel';
|
||||||
|
const INTEGRATION_NETLIFY = 'netlify';
|
||||||
|
const INTEGRATION_SET = new Set([
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY
|
||||||
|
]);
|
||||||
|
|
||||||
|
// integration types
|
||||||
|
const INTEGRATION_OAUTH2 = 'oauth2';
|
||||||
|
|
||||||
|
// integration oauth endpoints
|
||||||
|
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
|
||||||
|
const INTEGRATION_VERCEL_TOKEN_URL = 'https://api.vercel.com/v2/oauth/access_token';
|
||||||
|
const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token';
|
||||||
|
|
||||||
|
// integration apps endpoints
|
||||||
|
const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com';
|
||||||
|
const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com';
|
||||||
|
const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com';
|
||||||
|
|
||||||
|
const INTEGRATION_OPTIONS = [
|
||||||
|
{
|
||||||
|
name: 'Heroku',
|
||||||
|
slug: 'heroku',
|
||||||
|
image: 'Heroku',
|
||||||
|
isAvailable: true,
|
||||||
|
type: 'oauth2',
|
||||||
|
clientId: CLIENT_ID_HEROKU,
|
||||||
|
docsLink: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Vercel',
|
||||||
|
slug: 'vercel',
|
||||||
|
image: 'Vercel',
|
||||||
|
isAvailable: true,
|
||||||
|
type: 'vercel',
|
||||||
|
clientId: '',
|
||||||
|
clientSlug: CLIENT_SLUG_VERCEL,
|
||||||
|
docsLink: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Netlify',
|
||||||
|
slug: 'netlify',
|
||||||
|
image: 'Netlify',
|
||||||
|
isAvailable: false,
|
||||||
|
type: 'oauth2',
|
||||||
|
clientId: CLIENT_ID_NETLIFY,
|
||||||
|
docsLink: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Google Cloud Platform',
|
||||||
|
slug: 'gcp',
|
||||||
|
image: 'Google Cloud Platform',
|
||||||
|
isAvailable: false,
|
||||||
|
type: '',
|
||||||
|
clientId: '',
|
||||||
|
docsLink: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Amazon Web Services',
|
||||||
|
slug: 'aws',
|
||||||
|
image: 'Amazon Web Services',
|
||||||
|
isAvailable: false,
|
||||||
|
type: '',
|
||||||
|
clientId: '',
|
||||||
|
docsLink: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Microsoft Azure',
|
||||||
|
slug: 'azure',
|
||||||
|
image: 'Microsoft Azure',
|
||||||
|
isAvailable: false,
|
||||||
|
type: '',
|
||||||
|
clientId: '',
|
||||||
|
docsLink: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Travis CI',
|
||||||
|
slug: 'travisci',
|
||||||
|
image: 'Travis CI',
|
||||||
|
isAvailable: false,
|
||||||
|
type: '',
|
||||||
|
clientId: '',
|
||||||
|
docsLink: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Circle CI',
|
||||||
|
slug: 'circleci',
|
||||||
|
image: 'Circle CI',
|
||||||
|
isAvailable: false,
|
||||||
|
type: '',
|
||||||
|
clientId: '',
|
||||||
|
docsLink: ''
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export {
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY,
|
||||||
|
INTEGRATION_SET,
|
||||||
|
INTEGRATION_OAUTH2,
|
||||||
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
|
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||||
|
INTEGRATION_HEROKU_API_URL,
|
||||||
|
INTEGRATION_VERCEL_API_URL,
|
||||||
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
|
INTEGRATION_OPTIONS
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// membership roles
|
||||||
|
const OWNER = 'owner';
|
||||||
|
const ADMIN = 'admin';
|
||||||
|
const MEMBER = 'member';
|
||||||
|
|
||||||
|
// membership statuses
|
||||||
|
const INVITED = 'invited';
|
||||||
|
|
||||||
|
// -- organization
|
||||||
|
const ACCEPTED = 'accepted';
|
||||||
|
|
||||||
|
// -- workspace
|
||||||
|
const COMPLETED = 'completed';
|
||||||
|
const GRANTED = 'granted';
|
||||||
|
|
||||||
|
export {
|
||||||
|
OWNER,
|
||||||
|
ADMIN,
|
||||||
|
MEMBER,
|
||||||
|
INVITED,
|
||||||
|
ACCEPTED,
|
||||||
|
COMPLETED,
|
||||||
|
GRANTED
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// secrets
|
||||||
|
const SECRET_SHARED = 'shared';
|
||||||
|
const SECRET_PERSONAL = 'personal';
|
||||||
|
|
||||||
|
export {
|
||||||
|
SECRET_SHARED,
|
||||||
|
SECRET_PERSONAL
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const PLAN_STARTER = 'starter';
|
||||||
|
const PLAN_PRO = 'pro';
|
||||||
|
|
||||||
|
export {
|
||||||
|
PLAN_STARTER,
|
||||||
|
PLAN_PRO
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ services:
|
|||||||
env_file: .env
|
env_file: .env
|
||||||
environment:
|
environment:
|
||||||
- NEXT_PUBLIC_ENV=development
|
- NEXT_PUBLIC_ENV=development
|
||||||
|
- INFISICAL_TELEMETRY_ENABLED=${TELEMETRY_ENABLED}
|
||||||
- NEXT_PUBLIC_STRIPE_PRODUCT_PRO=${STRIPE_PRODUCT_PRO}
|
- NEXT_PUBLIC_STRIPE_PRODUCT_PRO=${STRIPE_PRODUCT_PRO}
|
||||||
- NEXT_PUBLIC_STRIPE_PRODUCT_STARTER=${STRIPE_PRODUCT_STARTER}
|
- NEXT_PUBLIC_STRIPE_PRODUCT_STARTER=${STRIPE_PRODUCT_STARTER}
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ services:
|
|||||||
- 80:80
|
- 80:80
|
||||||
- 443:443
|
- 443:443
|
||||||
volumes:
|
volumes:
|
||||||
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
- ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- frontend
|
- frontend
|
||||||
- backend
|
- backend
|
||||||
|
|||||||
+4
-1
@@ -178,5 +178,8 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"backgroundImage": "/images/background.png"
|
"backgroundImage": "/images/background.png",
|
||||||
|
"integrations": {
|
||||||
|
"intercom": "hsg644ru"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ Configuring Infisical requires setting some environment variables. There is a fi
|
|||||||
| `SMTP_USERNAME` | ❗️ Credential to connect to host (e.g. `[email protected]`) | `None` |
|
| `SMTP_USERNAME` | ❗️ Credential to connect to host (e.g. `[email protected]`) | `None` |
|
||||||
| `SMTP_PASSWORD` | ❗️ Credential to connect to host | `None` |
|
| `SMTP_PASSWORD` | ❗️ Credential to connect to host | `None` |
|
||||||
| `TELEMETRY_ENABLED` | `true` or `false`. [More](../overview). | `true` |
|
| `TELEMETRY_ENABLED` | `true` or `false`. [More](../overview). | `true` |
|
||||||
| `OAUTH_CLIENT_SECRET_HEROKU` | OAuth client secret for Heroku integration | `None` |
|
| `CLIENT_ID_VERCEL` | OAuth client id for Vercel integration | `None` |
|
||||||
| `OAUTH_TOKEN_URL_HEROKU` | OAuth token URL for Heroku integration | `None` |
|
| `CLIENT_ID_NETLIFY` | OAuth client id for Netlify integration | `None` |
|
||||||
|
| `CLIENT_SECRET_HEROKU` | OAuth client secret for Heroku integration | `None` |
|
||||||
|
| `CLIENT_SECRET_VERCEL` | OAuth client secret for Vercel integration | `None` |
|
||||||
|
| `CLIENT_SECRET_NETLIFY` | OAuth client secret for Netlify integration | `None` |
|
||||||
| `SENTRY_DSN` | DSN for error-monitoring with Sentry | `None` |
|
| `SENTRY_DSN` | DSN for error-monitoring with Sentry | `None` |
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ WORKDIR /app
|
|||||||
COPY package.json package-lock.json next.config.js ./
|
COPY package.json package-lock.json next.config.js ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm ci --only-production
|
RUN npm ci --only-production --ignore-scripts
|
||||||
|
|
||||||
|
|
||||||
# Rebuild the source code only when needed
|
# Rebuild the source code only when needed
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ COPY package.json ./
|
|||||||
COPY package-lock.json ./
|
COPY package-lock.json ./
|
||||||
|
|
||||||
# Install
|
# Install
|
||||||
RUN npm install
|
RUN npm install --ignore-scripts
|
||||||
|
|
||||||
# Copy over next.js config
|
# Copy over next.js config
|
||||||
COPY next.config.js ./next.config.js
|
COPY next.config.js ./next.config.js
|
||||||
@@ -17,4 +17,4 @@ COPY next.config.js ./next.config.js
|
|||||||
# Copy all files
|
# Copy all files
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
CMD ["npm", "run", "dev"]
|
CMD ["npm", "run", "dev"]
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
# Base layer
|
|
||||||
FROM node:16-alpine
|
|
||||||
|
|
||||||
# Set the working directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy over dependency files
|
|
||||||
COPY package.json ./
|
|
||||||
COPY package-lock.json ./
|
|
||||||
|
|
||||||
# Install
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
# Copy over next.js config
|
|
||||||
COPY next.config.js ./next.config.js
|
|
||||||
|
|
||||||
# Copy all files
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
CMD ["npm", "run", "start:docker"]
|
|
||||||
@@ -10,10 +10,10 @@ import { Listbox, Transition } from "@headlessui/react";
|
|||||||
|
|
||||||
interface ListBoxProps {
|
interface ListBoxProps {
|
||||||
selected: string;
|
selected: string;
|
||||||
onChange: () => void;
|
onChange: (arg: string) => void;
|
||||||
data: string[];
|
data: string[] | null;
|
||||||
text: string;
|
text?: string;
|
||||||
buttonAction: () => void;
|
buttonAction?: () => void;
|
||||||
isFull?: boolean;
|
isFull?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
const classNames = require("classnames");
|
const classNames = require("classnames");
|
||||||
|
|
||||||
type ButtonProps = {
|
type ButtonProps = {
|
||||||
text: string;
|
text?: string;
|
||||||
onButtonPressed: () => void;
|
onButtonPressed: () => void;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
color?: string;
|
color?: string;
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { Fragment } from "react";
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
|
import setBotActiveStatus from "../../../pages/api/bot/setBotActiveStatus";
|
||||||
|
import getLatestFileKey from "../../../pages/api/workspace/getLatestFileKey";
|
||||||
|
import {
|
||||||
|
decryptAssymmetric,
|
||||||
|
encryptAssymmetric
|
||||||
|
} from "../../utilities/cryptography/crypto";
|
||||||
|
import Button from "../buttons/Button";
|
||||||
|
|
||||||
|
const ActivateBotDialog = ({
|
||||||
|
isOpen,
|
||||||
|
closeModal,
|
||||||
|
selectedIntegrationOption,
|
||||||
|
handleBotActivate,
|
||||||
|
handleIntegrationOption
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
try {
|
||||||
|
// 1. activate bot
|
||||||
|
await handleBotActivate();
|
||||||
|
|
||||||
|
// 2. start integration
|
||||||
|
await handleIntegrationOption({
|
||||||
|
integrationOption: selectedIntegrationOption
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||||
|
</Transition.Child>
|
||||||
|
<div className="fixed inset-0 overflow-y-auto">
|
||||||
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||||
|
<Dialog.Title
|
||||||
|
as="h3"
|
||||||
|
className="text-lg font-medium leading-6 text-gray-400"
|
||||||
|
>
|
||||||
|
Grant Infisical access to your secrets
|
||||||
|
</Dialog.Title>
|
||||||
|
<div className="mt-2 mb-2">
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Most cloud integrations require Infisical to be able to decrypt your secrets so they can be forwarded over.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 max-w-max">
|
||||||
|
<Button
|
||||||
|
onButtonPressed={submit}
|
||||||
|
color="mineshaft"
|
||||||
|
text="Grant access"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ActivateBotDialog;
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { Fragment } from "react";
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
|
import setBotActiveStatus from "../../../pages/api/bot/setBotActiveStatus";
|
||||||
|
import getLatestFileKey from "../../../pages/api/workspace/getLatestFileKey";
|
||||||
|
import {
|
||||||
|
decryptAssymmetric,
|
||||||
|
encryptAssymmetric
|
||||||
|
} from "../../utilities/cryptography/crypto";
|
||||||
|
import Button from "../buttons/Button";
|
||||||
|
import InputField from "../InputField";
|
||||||
|
|
||||||
|
const IntegrationAccessTokenDialog = ({
|
||||||
|
isOpen,
|
||||||
|
closeModal,
|
||||||
|
selectedIntegrationOption,
|
||||||
|
handleBotActivate,
|
||||||
|
handleIntegrationOption
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
try {
|
||||||
|
// 1. activate bot
|
||||||
|
await handleBotActivate();
|
||||||
|
|
||||||
|
// 2. start integration
|
||||||
|
await handleIntegrationOption({
|
||||||
|
integrationOption: selectedIntegrationOption
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||||
|
</Transition.Child>
|
||||||
|
<div className="fixed inset-0 overflow-y-auto">
|
||||||
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||||
|
<Dialog.Title
|
||||||
|
as="h3"
|
||||||
|
className="text-lg font-medium leading-6 text-gray-400"
|
||||||
|
>
|
||||||
|
Grant Infisical access to your secrets
|
||||||
|
</Dialog.Title>
|
||||||
|
<div className="mt-2 mb-2">
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Most cloud integrations require Infisical to be able to decrypt your secrets so they can be forwarded over.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 max-w-max">
|
||||||
|
{/* <Button
|
||||||
|
onButtonPressed={submit}
|
||||||
|
color="mineshaft"
|
||||||
|
text="Grant access"
|
||||||
|
size="md"
|
||||||
|
/> */}
|
||||||
|
<InputField
|
||||||
|
label="Access token"
|
||||||
|
onChangeHandler={() => {}}
|
||||||
|
type="varName"
|
||||||
|
value={"Hello"}
|
||||||
|
placeholder=""
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IntegrationAccessTokenDialog;
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import {
|
||||||
|
faCheck,
|
||||||
|
faX,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import deleteIntegrationAuth from "../../pages/api/integrations/DeleteIntegrationAuth";
|
||||||
|
|
||||||
|
interface CloudIntegrationOption {
|
||||||
|
isAvailable: boolean;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
clientId: string;
|
||||||
|
docsLink: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IntegrationAuth {
|
||||||
|
_id: string;
|
||||||
|
integration: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
cloudIntegrationOption: CloudIntegrationOption;
|
||||||
|
setSelectedIntegrationOption: (cloudIntegration: CloudIntegrationOption) => void;
|
||||||
|
integrationOptionPress: (cloudIntegrationOption: CloudIntegrationOption) => void;
|
||||||
|
integrationAuths: IntegrationAuth[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const CloudIntegration = ({
|
||||||
|
cloudIntegrationOption,
|
||||||
|
setSelectedIntegrationOption,
|
||||||
|
integrationOptionPress,
|
||||||
|
integrationAuths
|
||||||
|
}: Props) => {
|
||||||
|
const router = useRouter();
|
||||||
|
return integrationAuths ? (
|
||||||
|
<div
|
||||||
|
className={`relative ${
|
||||||
|
cloudIntegrationOption.isAvailable
|
||||||
|
? "hover:bg-white/10 duration-200 cursor-pointer"
|
||||||
|
: "opacity-50"
|
||||||
|
} flex flex-row bg-white/5 h-32 rounded-md p-4 items-center`}
|
||||||
|
onClick={() => {
|
||||||
|
if (!cloudIntegrationOption.isAvailable) return;
|
||||||
|
setSelectedIntegrationOption(cloudIntegrationOption);
|
||||||
|
integrationOptionPress(cloudIntegrationOption);
|
||||||
|
}}
|
||||||
|
key={cloudIntegrationOption.name}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={`/images/integrations/${cloudIntegrationOption.name}.png`}
|
||||||
|
height={70}
|
||||||
|
width={70}
|
||||||
|
alt="integration logo"
|
||||||
|
/>
|
||||||
|
{cloudIntegrationOption.name.split(" ").length > 2 ? (
|
||||||
|
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-3xl ml-4 max-w-xs">
|
||||||
|
<div>{cloudIntegrationOption.name.split(" ")[0]}</div>
|
||||||
|
<div className="text-base">
|
||||||
|
{cloudIntegrationOption.name.split(" ")[1]}{" "}
|
||||||
|
{cloudIntegrationOption.name.split(" ")[2]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-xl ml-4 max-w-xs">
|
||||||
|
{cloudIntegrationOption.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{cloudIntegrationOption.isAvailable &&
|
||||||
|
integrationAuths
|
||||||
|
.map((authorization) => authorization.integration)
|
||||||
|
.includes(cloudIntegrationOption.name.toLowerCase()) && (
|
||||||
|
<div className="absolute group z-50 top-0 right-0 flex flex-row">
|
||||||
|
<div
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
deleteIntegrationAuth({
|
||||||
|
integrationAuthId: integrationAuths
|
||||||
|
.filter(
|
||||||
|
(authorization) =>
|
||||||
|
authorization.integration ==
|
||||||
|
cloudIntegrationOption.name.toLowerCase()
|
||||||
|
)
|
||||||
|
.map((authorization) => authorization._id)[0],
|
||||||
|
});
|
||||||
|
|
||||||
|
router.reload();
|
||||||
|
}}
|
||||||
|
className="cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faX}
|
||||||
|
className="text-xs mr-2 py-px"
|
||||||
|
/>
|
||||||
|
Revoke
|
||||||
|
</div>
|
||||||
|
<div className="w-max bg-primary py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90 group-hover:opacity-100 duration-200">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCheck}
|
||||||
|
className="text-xs mr-2"
|
||||||
|
/>
|
||||||
|
Authorized
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!cloudIntegrationOption.isAvailable && (
|
||||||
|
<div className="absolute group z-50 top-0 right-0 flex flex-row">
|
||||||
|
<div className="w-max bg-yellow py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90">
|
||||||
|
Coming Soon
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : <div></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CloudIntegration;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import CloudIntegration from "./CloudIntegration";
|
||||||
|
|
||||||
|
interface CloudIntegrationOption {
|
||||||
|
isAvailable: boolean;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
clientId: string;
|
||||||
|
docsLink: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
cloudIntegrationOptions: CloudIntegrationOption[];
|
||||||
|
setSelectedIntegrationOption: () => void;
|
||||||
|
integrationOptionPress: () => void;
|
||||||
|
integrationAuths: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CloudIntegrationSection = ({
|
||||||
|
cloudIntegrationOptions,
|
||||||
|
setSelectedIntegrationOption,
|
||||||
|
integrationOptionPress,
|
||||||
|
integrationAuths
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={`flex flex-col justify-between items-start m-4 mt-7 text-xl max-w-5xl px-2`}>
|
||||||
|
<h1 className="font-semibold text-3xl">Cloud Integrations</h1>
|
||||||
|
<p className="text-base text-gray-400">
|
||||||
|
Click on an integration to begin syncing secrets to it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4 grid-cols-4 grid-rows-2 mx-6 max-w-5xl">
|
||||||
|
{cloudIntegrationOptions.map((cloudIntegrationOption) => (
|
||||||
|
<CloudIntegration
|
||||||
|
cloudIntegrationOption={cloudIntegrationOption}
|
||||||
|
setSelectedIntegrationOption={setSelectedIntegrationOption}
|
||||||
|
integrationOptionPress={integrationOptionPress}
|
||||||
|
integrationAuths={integrationAuths}
|
||||||
|
key={`cloud-integration-${cloudIntegrationOption.slug}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CloudIntegrationSection;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
interface Framework {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
image: string;
|
||||||
|
docsLink: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FrameworkIntegration = ({
|
||||||
|
framework
|
||||||
|
}: {
|
||||||
|
framework: Framework;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={framework.docsLink}
|
||||||
|
rel="noopener"
|
||||||
|
className={`relative flex flex-row items-center justify-center bg-bunker-500 hover:bg-gradient-to-tr duration-200 h-32 rounded-md p-0.5 items-center cursor-pointer`}
|
||||||
|
>
|
||||||
|
<div className={`hover:bg-white/10 duration-200 cursor-pointer font-semibold bg-bunker-500 flex flex-col items-center justify-center h-full w-full rounded-md text-gray-300 group-hover:text-gray-200 duration-200 ${framework?.name?.split(" ").length > 1 ? "text-sm px-1" : "text-xl px-2"} text-center w-full max-w-xs`}>
|
||||||
|
{framework?.image && <Image
|
||||||
|
src={`/images/integrations/${framework.image}.png`}
|
||||||
|
height={framework?.name ? 60 : 90}
|
||||||
|
width={framework?.name ? 60 : 90}
|
||||||
|
alt="integration logo"
|
||||||
|
></Image>}
|
||||||
|
{framework?.name && framework?.image && <div className="h-2"></div>}
|
||||||
|
{framework?.name && framework.name}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FrameworkIntegration;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import FrameworkIntegration from "./FrameworkIntegration";
|
||||||
|
|
||||||
|
interface Framework {
|
||||||
|
name: string;
|
||||||
|
image: string;
|
||||||
|
link: string;
|
||||||
|
slug: string;
|
||||||
|
docsLink: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
frameworks: [Framework]
|
||||||
|
}
|
||||||
|
|
||||||
|
const FrameworkIntegrationSection = ({ frameworks }: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col justify-between items-start mx-4 mt-12 mb-4 text-xl max-w-5xl px-2">
|
||||||
|
<h1 className="font-semibold text-3xl">Framework Integrations</h1>
|
||||||
|
<p className="text-base text-gray-400">
|
||||||
|
Click on a framework to get the setup instructions.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4 grid-cols-7 grid-rows-2 mx-6 mt-4 max-w-5xl">
|
||||||
|
{frameworks.map((framework) => (
|
||||||
|
<FrameworkIntegration
|
||||||
|
framework={framework}
|
||||||
|
key={`framework-integration-${framework.slug}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FrameworkIntegrationSection;
|
||||||
|
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import {
|
||||||
|
faArrowRight,
|
||||||
|
faRotate,
|
||||||
|
faX,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import Button from "~/components/basic/buttons/Button";
|
||||||
|
import ListBox from "~/components/basic/Listbox";
|
||||||
|
|
||||||
|
import deleteIntegration from "../../pages/api/integrations/DeleteIntegration"
|
||||||
|
import getIntegrationApps from "../../pages/api/integrations/GetIntegrationApps";
|
||||||
|
import updateIntegration from "../../pages/api/integrations/updateIntegration"
|
||||||
|
import {
|
||||||
|
contextNetlifyMapping,
|
||||||
|
envMapping,
|
||||||
|
reverseContextNetlifyMapping,
|
||||||
|
reverseEnvMapping,
|
||||||
|
} from "../../public/data/frequentConstants";
|
||||||
|
|
||||||
|
interface Integration {
|
||||||
|
_id: string;
|
||||||
|
app?: string;
|
||||||
|
environment: string;
|
||||||
|
integration: string;
|
||||||
|
integrationAuth: string;
|
||||||
|
isActive: boolean;
|
||||||
|
context: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IntegrationApp {
|
||||||
|
name: string;
|
||||||
|
siteId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Integration = ({
|
||||||
|
integration
|
||||||
|
}: {
|
||||||
|
integration: Integration;
|
||||||
|
}) => {
|
||||||
|
const [integrationEnvironment, setIntegrationEnvironment] = useState(
|
||||||
|
reverseEnvMapping[integration.environment]
|
||||||
|
);
|
||||||
|
const [fileState, setFileState] = useState([]);
|
||||||
|
const router = useRouter();
|
||||||
|
const [apps, setApps] = useState<IntegrationApp[]>([]); // integration app objects
|
||||||
|
const [integrationApp, setIntegrationApp] = useState(""); // integration app name
|
||||||
|
const [integrationTarget, setIntegrationTarget] = useState(""); // vercel-specific integration param
|
||||||
|
const [integrationContext, setIntegrationContext] = useState(""); // netlify-specific integration param
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
const loadIntegration = async () => {
|
||||||
|
interface App {
|
||||||
|
name: string;
|
||||||
|
siteId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempApps: [IntegrationApp] = await getIntegrationApps({
|
||||||
|
integrationAuthId: integration.integrationAuth,
|
||||||
|
});
|
||||||
|
|
||||||
|
setApps(tempApps);
|
||||||
|
setIntegrationApp(
|
||||||
|
integration.app ? integration.app : tempApps[0].name
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (integration.integration) {
|
||||||
|
case "vercel":
|
||||||
|
setIntegrationTarget("Development");
|
||||||
|
break;
|
||||||
|
case "netlify":
|
||||||
|
setIntegrationContext(integration?.context ? contextNetlifyMapping[integration.context] : "Local development");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadIntegration();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderIntegrationSpecificParams = (integration: Integration) => {
|
||||||
|
try {
|
||||||
|
switch (integration.integration) {
|
||||||
|
case "vercel":
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="text-gray-400 text-xs font-semibold mb-2">
|
||||||
|
ENVIRONMENT
|
||||||
|
</div>
|
||||||
|
<ListBox
|
||||||
|
data={!integration.isActive ? [
|
||||||
|
"Production",
|
||||||
|
"Preview",
|
||||||
|
"Development"
|
||||||
|
] : null}
|
||||||
|
selected={"Production"}
|
||||||
|
onChange={setIntegrationTarget}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "netlify":
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="text-gray-400 text-xs font-semibold mb-2">
|
||||||
|
CONTEXT
|
||||||
|
</div>
|
||||||
|
<ListBox
|
||||||
|
data={!integration.isActive ? [
|
||||||
|
"Production",
|
||||||
|
"Deploy previews",
|
||||||
|
"Branch deploys",
|
||||||
|
"Local development"
|
||||||
|
] : null}
|
||||||
|
selected={integrationContext}
|
||||||
|
onChange={setIntegrationContext}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!integrationApp || apps.length === 0) return <div></div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between">
|
||||||
|
<div className="flex">
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-400 text-xs font-semibold mb-2">ENVIRONMENT</p>
|
||||||
|
<ListBox data={!integration.isActive ? [
|
||||||
|
"Development",
|
||||||
|
"Staging",
|
||||||
|
"Testing",
|
||||||
|
"Production",
|
||||||
|
] : null}
|
||||||
|
selected={integrationEnvironment}
|
||||||
|
onChange={(environment) => {
|
||||||
|
setIntegrationEnvironment(environment);
|
||||||
|
}}
|
||||||
|
isFull={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="pt-2">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowRight}
|
||||||
|
className="mx-4 text-gray-400 mt-8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mr-2">
|
||||||
|
<p className="text-gray-400 text-xs font-semibold mb-2">
|
||||||
|
INTEGRATION
|
||||||
|
</p>
|
||||||
|
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300">
|
||||||
|
{integration.integration.charAt(0).toUpperCase() +
|
||||||
|
integration.integration.slice(1)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mr-2">
|
||||||
|
<div className="text-gray-400 text-xs font-semibold mb-2">
|
||||||
|
APP
|
||||||
|
</div>
|
||||||
|
<ListBox
|
||||||
|
data={!integration.isActive ? apps.map((app) => app.name) : null}
|
||||||
|
selected={integrationApp}
|
||||||
|
onChange={(app) => {
|
||||||
|
setIntegrationApp(app);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{renderIntegrationSpecificParams(integration)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end">
|
||||||
|
{integration.isActive ? (
|
||||||
|
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faRotate}
|
||||||
|
className="text-lg mr-2.5 text-primary animate-spin"
|
||||||
|
/>
|
||||||
|
<div className="text-gray-300 font-semibold">In Sync</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
text="Start Integration"
|
||||||
|
onButtonPressed={async () => {
|
||||||
|
|
||||||
|
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
|
||||||
|
const siteId = siteApp?.siteId ? siteApp.siteId : null;
|
||||||
|
|
||||||
|
const result = await updateIntegration({
|
||||||
|
integrationId: integration._id,
|
||||||
|
environment: envMapping[integrationEnvironment],
|
||||||
|
app: integrationApp,
|
||||||
|
isActive: true,
|
||||||
|
target: integrationTarget ? integrationTarget.toLowerCase() : null,
|
||||||
|
context: integrationContext ? reverseContextNetlifyMapping[integrationContext] : null,
|
||||||
|
siteId
|
||||||
|
});
|
||||||
|
|
||||||
|
router.reload();
|
||||||
|
}}
|
||||||
|
color="mineshaft"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
|
||||||
|
<Button
|
||||||
|
onButtonPressed={async () => {
|
||||||
|
await deleteIntegration({
|
||||||
|
integrationId: integration._id,
|
||||||
|
});
|
||||||
|
router.reload();
|
||||||
|
}}
|
||||||
|
color="red"
|
||||||
|
size="icon-md"
|
||||||
|
icon={faX}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Integration;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import guidGenerator from "~/utilities/randomId";
|
||||||
|
|
||||||
|
import Integration from "./Integration";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
integrations: any
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IntegrationType {
|
||||||
|
_id: string;
|
||||||
|
app?: string;
|
||||||
|
environment: string;
|
||||||
|
integration: string;
|
||||||
|
integrationAuth: string;
|
||||||
|
isActive: boolean;
|
||||||
|
context: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectIntegrationSection = ({
|
||||||
|
integrations
|
||||||
|
}: Props) => {
|
||||||
|
return integrations.length > 0 ? (
|
||||||
|
<div className="mb-12">
|
||||||
|
<div className="flex flex-col justify-between items-start mx-4 mb-4 mt-6 text-xl max-w-5xl px-2">
|
||||||
|
<h1 className="font-semibold text-3xl">Current Integrations</h1>
|
||||||
|
<p className="text-base text-gray-400">
|
||||||
|
Manage your integrations of Infisical with third-party services.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{integrations.map((integration: IntegrationType) => (
|
||||||
|
<Integration
|
||||||
|
key={guidGenerator()}
|
||||||
|
integration={integration}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : <div></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectIntegrationSection;
|
||||||
@@ -73,7 +73,7 @@ const attemptLogin = async (
|
|||||||
tag,
|
tag,
|
||||||
privateKey
|
privateKey
|
||||||
});
|
});
|
||||||
|
|
||||||
const userOrgs = await getOrganizations();
|
const userOrgs = await getOrganizations();
|
||||||
const userOrgsData = userOrgs.map((org) => org._id);
|
const userOrgsData = userOrgs.map((org) => org._id);
|
||||||
|
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ export {
|
|||||||
POSTHOG_API_KEY,
|
POSTHOG_API_KEY,
|
||||||
POSTHOG_HOST,
|
POSTHOG_HOST,
|
||||||
STRIPE_PRODUCT_PRO,
|
STRIPE_PRODUCT_PRO,
|
||||||
STRIPE_PRODUCT_STARTER,
|
STRIPE_PRODUCT_STARTER
|
||||||
};
|
};
|
||||||
Generated
-22
@@ -60,7 +60,6 @@
|
|||||||
"eslint-import-resolver-typescript": "^3.5.2",
|
"eslint-import-resolver-typescript": "^3.5.2",
|
||||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||||
"postcss": "^8.4.14",
|
"postcss": "^8.4.14",
|
||||||
"prettier": "2.7.1",
|
|
||||||
"tailwindcss": "^3.1.4",
|
"tailwindcss": "^3.1.4",
|
||||||
"typescript": "^4.9.3"
|
"typescript": "^4.9.3"
|
||||||
}
|
}
|
||||||
@@ -5999,21 +5998,6 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
|
||||||
"version": "2.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
|
|
||||||
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"prettier": "bin-prettier.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.13.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/process-nextick-args": {
|
"node_modules/process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
@@ -11950,12 +11934,6 @@
|
|||||||
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"prettier": {
|
|
||||||
"version": "2.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
|
|
||||||
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"process-nextick-args": {
|
"process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"prepare": "cd .. && npm install",
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
@@ -63,7 +64,6 @@
|
|||||||
"eslint-import-resolver-typescript": "^3.5.2",
|
"eslint-import-resolver-typescript": "^3.5.2",
|
||||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||||
"postcss": "^8.4.14",
|
"postcss": "^8.4.14",
|
||||||
"prettier": "2.7.1",
|
|
||||||
"tailwindcss": "^3.1.4",
|
"tailwindcss": "^3.1.4",
|
||||||
"typescript": "^4.9.3"
|
"typescript": "^4.9.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import SecurityClient from "~/utilities/SecurityClient";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
workspaceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function fetches the bot for a project
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getBot = async ({ workspaceId }: Props) => {
|
||||||
|
return SecurityClient.fetchCall(
|
||||||
|
"/api/v1/bot/" + workspaceId,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).then(async (res) => {
|
||||||
|
if (res && res.status == 200) {
|
||||||
|
return (await res.json()).bot;
|
||||||
|
} else {
|
||||||
|
console.log("Failed to get bot for project");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getBot;
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import SecurityClient from "~/utilities/SecurityClient";
|
||||||
|
|
||||||
|
interface BotKey {
|
||||||
|
encryptedKey: string;
|
||||||
|
nonce: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
botId: string;
|
||||||
|
isActive: boolean;
|
||||||
|
botKey: BotKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function sets the active status of a bot and shares a copy of
|
||||||
|
* the project key (encrypted under the bot's public key) with the
|
||||||
|
* project's bot
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.botId
|
||||||
|
* @param {String} obj.isActive
|
||||||
|
* @param {Object} obj.botKey
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const setBotActiveStatus = async ({ botId, isActive, botKey }: Props) => {
|
||||||
|
return SecurityClient.fetchCall(
|
||||||
|
"/api/v1/bot/" + botId + "/active",
|
||||||
|
{
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
isActive,
|
||||||
|
botKey
|
||||||
|
})
|
||||||
|
}
|
||||||
|
).then(async (res) => {
|
||||||
|
if (res && res.status == 200) {
|
||||||
|
return await res.json();
|
||||||
|
} else {
|
||||||
|
console.log("Failed to get bot for project");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default setBotActiveStatus;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import SecurityClient from '~/utilities/SecurityClient';
|
||||||
|
|
||||||
|
const getIntegrationOptions = () => {
|
||||||
|
return SecurityClient.fetchCall(
|
||||||
|
'/api/v1/integration-auth/integration-options',
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).then(async (res) => {
|
||||||
|
if (res && res.status == 200) {
|
||||||
|
return (await res.json()).integrationOptions;
|
||||||
|
} else {
|
||||||
|
console.log('Failed to get (cloud) integration options');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getIntegrationOptions;
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import SecurityClient from '~/utilities/SecurityClient';
|
|
||||||
|
|
||||||
const getIntegrations = () => {
|
|
||||||
return SecurityClient.fetchCall('/api/v1/integration/integrations', {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}).then(async (res) => {
|
|
||||||
if (res && res.status == 200) {
|
|
||||||
return (await res.json()).integrations;
|
|
||||||
} else {
|
|
||||||
console.log('Failed to get project integrations');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getIntegrations;
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import SecurityClient from "~/utilities/SecurityClient";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This route starts the integration after teh default one if gonna set up.
|
||||||
|
* Update integration with id [integrationId] to sync envars from the project's
|
||||||
|
* [environment] to the integration [app] with active state [isActive]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationId - id of integration
|
||||||
|
* @param {String} obj.app - name of app
|
||||||
|
* @param {String} obj.environment - project environment to push secrets from
|
||||||
|
* @param {Boolean} obj.isActive - active state
|
||||||
|
* @param {String} obj.target - (optional) target (environment) for Vercel integration
|
||||||
|
* @param {String} obj.context - (optional) context (environment) for Netlify integration
|
||||||
|
* @param {String} obj.siteId - (optional) app (site_id) for Netlify integration
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const updateIntegration = ({
|
||||||
|
integrationId,
|
||||||
|
app,
|
||||||
|
environment,
|
||||||
|
isActive,
|
||||||
|
target,
|
||||||
|
context,
|
||||||
|
siteId
|
||||||
|
}: {
|
||||||
|
integrationId: string,
|
||||||
|
app: string,
|
||||||
|
environment: string,
|
||||||
|
isActive: boolean,
|
||||||
|
target: string | null,
|
||||||
|
context: string | null,
|
||||||
|
siteId: string | null
|
||||||
|
|
||||||
|
}) => {
|
||||||
|
return SecurityClient.fetchCall(
|
||||||
|
"/api/v1/integration/" + integrationId,
|
||||||
|
{
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
app,
|
||||||
|
environment,
|
||||||
|
isActive,
|
||||||
|
target,
|
||||||
|
context,
|
||||||
|
siteId
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
).then(async (res) => {
|
||||||
|
if (res && res.status == 200) {
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
console.log("Failed to start an integration");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default updateIntegration;
|
||||||
@@ -413,29 +413,6 @@ export default function Dashboard() {
|
|||||||
setButtonReady(false);
|
setButtonReady(false);
|
||||||
pushKeys({ obj, workspaceId: router.query.id, env });
|
pushKeys({ obj, workspaceId: router.query.id, env });
|
||||||
|
|
||||||
/**
|
|
||||||
* Check which integrations are active for this project and environment
|
|
||||||
* If there are any, update environment variables for those integrations
|
|
||||||
*/
|
|
||||||
let integrations = await getWorkspaceIntegrations({
|
|
||||||
workspaceId: router.query.id
|
|
||||||
});
|
|
||||||
integrations.map(async (integration) => {
|
|
||||||
if (
|
|
||||||
envMapping[env] == integration.environment &&
|
|
||||||
integration.isActive == true
|
|
||||||
) {
|
|
||||||
let objIntegration = Object.assign(
|
|
||||||
{},
|
|
||||||
...data.map((row) => ({ [row.key]: row.value }))
|
|
||||||
);
|
|
||||||
await pushKeysIntegration({
|
|
||||||
obj: objIntegration,
|
|
||||||
integrationId: integration._id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If this user has never saved environment variables before, show them a prompt to read docs
|
// If this user has never saved environment variables before, show them a prompt to read docs
|
||||||
if (!hasUserEverPushed) {
|
if (!hasUserEverPushed) {
|
||||||
setCheckDocsPopUpVisible(true);
|
setCheckDocsPopUpVisible(true);
|
||||||
|
|||||||
@@ -16,16 +16,17 @@ export default function Heroku() {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
useEffect(async () => {
|
useEffect(async () => {
|
||||||
try {
|
try {
|
||||||
if (state == localStorage.getItem("latestCSRFToken")) {
|
if (state === localStorage.getItem('latestCSRFToken')) {
|
||||||
|
localStorage.removeItem('latestCSRFToken');
|
||||||
await AuthorizeIntegration({
|
await AuthorizeIntegration({
|
||||||
workspaceId: localStorage.getItem("projectData.id"),
|
workspaceId: localStorage.getItem('projectData.id'),
|
||||||
code,
|
code,
|
||||||
integration: "heroku",
|
integration: "heroku",
|
||||||
});
|
});
|
||||||
router.push("/integrations/" + localStorage.getItem("projectData.id"));
|
router.push("/integrations/" + localStorage.getItem("projectData.id"));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error - Not logged in yet");
|
console.error('Heroku integration error: ', error);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
+198
-377
@@ -1,199 +1,179 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from "react";
|
||||||
import Head from 'next/head';
|
import Head from "next/head";
|
||||||
import Image from 'next/image';
|
import Image from "next/image";
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from "next/router";
|
||||||
import {
|
|
||||||
faArrowRight,
|
|
||||||
faCheck,
|
|
||||||
faRotate,
|
|
||||||
faX
|
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
|
|
||||||
import Button from '~/components/basic/buttons/Button';
|
import ActivateBotDialog from "~/components/basic/dialog/ActivateBotDialog";
|
||||||
import ListBox from '~/components/basic/Listbox';
|
import IntegrationAccessTokenDialog from "~/components/basic/dialog/IntegrationAccessTokenDialog";
|
||||||
import NavHeader from '~/components/navigation/NavHeader';
|
import CloudIntegrationSection from "~/components/integrations/CloudIntegrationSection";
|
||||||
import getSecretsForProject from '~/components/utilities/secrets/getSecretsForProject';
|
import FrameworkIntegrationSection from "~/components/integrations/FrameworkIntegrationSection";
|
||||||
import pushKeysIntegration from '~/components/utilities/secrets/pushKeysIntegration';
|
import Integration from "~/components/integrations/Integration";
|
||||||
import guidGenerator from '~/utilities/randomId';
|
import IntegrationSection from "~/components/integrations/IntegrationSection";
|
||||||
|
import NavHeader from "~/components/navigation/NavHeader";
|
||||||
|
|
||||||
import {
|
import frameworkIntegrationOptions from "../../public/json/frameworkIntegrations.json";
|
||||||
envMapping,
|
import getBot from "../api/bot/getBot";
|
||||||
frameworks,
|
import setBotActiveStatus from "../api/bot/setBotActiveStatus";
|
||||||
reverseEnvMapping
|
import getIntegrationOptions from "../api/integrations/GetIntegrationOptions";
|
||||||
} from '../../public/data/frequentConstants';
|
import getWorkspaceAuthorizations from "../api/integrations/getWorkspaceAuthorizations";
|
||||||
import deleteIntegration from '../api/integrations/DeleteIntegration';
|
import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations";
|
||||||
import deleteIntegrationAuth from '../api/integrations/DeleteIntegrationAuth';
|
import getLatestFileKey from "../api/workspace/getLatestFileKey";
|
||||||
import getIntegrationApps from '../api/integrations/GetIntegrationApps';
|
const {
|
||||||
import getIntegrations from '../api/integrations/GetIntegrations';
|
decryptAssymmetric,
|
||||||
import getWorkspaceAuthorizations from '../api/integrations/getWorkspaceAuthorizations';
|
encryptAssymmetric
|
||||||
import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations';
|
} = require('../../components/utilities/cryptography/crypto');
|
||||||
import startIntegration from '../api/integrations/StartIntegration';
|
const crypto = require("crypto");
|
||||||
|
|
||||||
const crypto = require('crypto');
|
|
||||||
|
|
||||||
const Integration = ({ projectIntegration }) => {
|
|
||||||
const [integrationEnvironment, setIntegrationEnvironment] = useState(
|
|
||||||
reverseEnvMapping[projectIntegration.environment]
|
|
||||||
);
|
|
||||||
const [fileState, setFileState] = useState([]);
|
|
||||||
const [data, setData] = useState();
|
|
||||||
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
|
|
||||||
const router = useRouter();
|
|
||||||
const [apps, setApps] = useState([]);
|
|
||||||
const [integrationApp, setIntegrationApp] = useState(
|
|
||||||
projectIntegration.app ? projectIntegration.app : apps[0]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(async () => {
|
|
||||||
const tempHerokuApps = await getIntegrationApps({
|
|
||||||
integrationAuthId: projectIntegration.integrationAuth
|
|
||||||
});
|
|
||||||
const tempHerokuAppNames = tempHerokuApps.map((app) => app.name);
|
|
||||||
setApps(tempHerokuAppNames);
|
|
||||||
setIntegrationApp(
|
|
||||||
projectIntegration.app ? projectIntegration.app : tempHerokuAppNames[0]
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col max-w-5xl justify-center bg-white/5 p-6 rounded-md mx-6 mt-8">
|
|
||||||
<div className="relative px-4 flex flex-row items-center justify-between mb-4">
|
|
||||||
<div className="flex flex-row">
|
|
||||||
<div>
|
|
||||||
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
|
||||||
ENVIRONMENT
|
|
||||||
</div>
|
|
||||||
<ListBox
|
|
||||||
data={
|
|
||||||
!projectIntegration.isActive && [
|
|
||||||
'Development',
|
|
||||||
'Staging',
|
|
||||||
'Production'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
selected={integrationEnvironment}
|
|
||||||
onChange={setIntegrationEnvironment}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faArrowRight}
|
|
||||||
className="mx-4 text-gray-400 mt-8"
|
|
||||||
/>
|
|
||||||
<div className="mr-2">
|
|
||||||
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
|
||||||
INTEGRATION
|
|
||||||
</div>
|
|
||||||
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-20 text-sm font-semibold text-gray-300">
|
|
||||||
{projectIntegration.integration.charAt(0).toUpperCase() +
|
|
||||||
projectIntegration.integration.slice(1)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
|
||||||
HEROKU APP
|
|
||||||
</div>
|
|
||||||
<ListBox
|
|
||||||
data={!projectIntegration.isActive && apps}
|
|
||||||
selected={integrationApp}
|
|
||||||
onChange={setIntegrationApp}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row mt-6">
|
|
||||||
{projectIntegration.isActive ? (
|
|
||||||
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faRotate}
|
|
||||||
className="text-lg mr-2.5 text-primary animate-spin"
|
|
||||||
/>
|
|
||||||
<div className="text-gray-300 font-semibold">In Sync</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
text="Start Integration"
|
|
||||||
onButtonPressed={async () => {
|
|
||||||
const result = await startIntegration({
|
|
||||||
integrationId: projectIntegration._id,
|
|
||||||
environment: envMapping[integrationEnvironment],
|
|
||||||
appName: integrationApp
|
|
||||||
});
|
|
||||||
if (result?.status == 200) {
|
|
||||||
let currentSecrets = await getSecretsForProject({
|
|
||||||
env: integrationEnvironment,
|
|
||||||
setFileState,
|
|
||||||
setIsKeyAvailable,
|
|
||||||
setData,
|
|
||||||
workspaceId: router.query.id
|
|
||||||
});
|
|
||||||
|
|
||||||
let obj = Object.assign(
|
|
||||||
{},
|
|
||||||
...currentSecrets.map((row) => ({
|
|
||||||
[row.key]: row.value
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
await pushKeysIntegration({
|
|
||||||
obj,
|
|
||||||
integrationId: projectIntegration._id
|
|
||||||
});
|
|
||||||
router.reload();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
color="mineshaft"
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
|
|
||||||
<Button
|
|
||||||
onButtonPressed={async () => {
|
|
||||||
await deleteIntegration({
|
|
||||||
integrationId: projectIntegration._id
|
|
||||||
});
|
|
||||||
router.reload();
|
|
||||||
}}
|
|
||||||
color="red"
|
|
||||||
size="icon-md"
|
|
||||||
icon={faX}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Integrations() {
|
export default function Integrations() {
|
||||||
const [integrations, setIntegrations] = useState();
|
const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]);
|
||||||
const [projectIntegrations, setProjectIntegrations] = useState();
|
const [integrationAuths, setIntegrationAuths] = useState([]);
|
||||||
const [authorizations, setAuthorizations] = useState();
|
const [integrations, setIntegrations] = useState([]);
|
||||||
|
const [bot, setBot] = useState(null);
|
||||||
|
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
|
||||||
|
// const [isIntegrationAccessTokenDialogOpen, setIntegrationAccessTokenDialogOpen] = useState(true);
|
||||||
|
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [csrfToken, setCsrfToken] = useState('');
|
|
||||||
|
|
||||||
useEffect(async () => {
|
useEffect(async () => {
|
||||||
const tempCSRFToken = crypto.randomBytes(16).toString('hex');
|
|
||||||
setCsrfToken(tempCSRFToken);
|
|
||||||
localStorage.setItem('latestCSRFToken', tempCSRFToken);
|
|
||||||
|
|
||||||
let projectAuthorizations = await getWorkspaceAuthorizations({
|
|
||||||
workspaceId: router.query.id
|
|
||||||
});
|
|
||||||
setAuthorizations(projectAuthorizations);
|
|
||||||
|
|
||||||
const projectIntegrations = await getWorkspaceIntegrations({
|
|
||||||
workspaceId: router.query.id
|
|
||||||
});
|
|
||||||
setProjectIntegrations(projectIntegrations);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const integrationsData = await getIntegrations();
|
// get cloud integration options
|
||||||
setIntegrations(integrationsData);
|
setCloudIntegrationOptions(
|
||||||
} catch (error) {
|
await getIntegrationOptions()
|
||||||
console.log('Error', error);
|
);
|
||||||
|
|
||||||
|
// get project integration authorizations
|
||||||
|
setIntegrationAuths(
|
||||||
|
await getWorkspaceAuthorizations({
|
||||||
|
workspaceId: router.query.id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// get project integrations
|
||||||
|
setIntegrations(
|
||||||
|
await getWorkspaceIntegrations({
|
||||||
|
workspaceId: router.query.id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// get project bot
|
||||||
|
setBot(
|
||||||
|
await getBot({
|
||||||
|
workspaceId: router.query.id
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return integrations ? (
|
/**
|
||||||
|
* Activate bot for project by performing the following steps:
|
||||||
|
* 1. Get the (encrypted) project key
|
||||||
|
* 2. Decrypt project key with user's private key
|
||||||
|
* 3. Encrypt project key with bot's public key
|
||||||
|
* 4. Send encrypted project key to backend and set bot status to active
|
||||||
|
*/
|
||||||
|
const handleBotActivate = async () => {
|
||||||
|
let botKey;
|
||||||
|
try {
|
||||||
|
|
||||||
|
if (bot) {
|
||||||
|
// case: there is a bot
|
||||||
|
const key = await getLatestFileKey({ workspaceId: router.query.id });
|
||||||
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||||
|
|
||||||
|
const WORKSPACE_KEY = decryptAssymmetric({
|
||||||
|
ciphertext: key.latestKey.encryptedKey,
|
||||||
|
nonce: key.latestKey.nonce,
|
||||||
|
publicKey: key.latestKey.sender.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ciphertext, nonce } = encryptAssymmetric({
|
||||||
|
plaintext: WORKSPACE_KEY,
|
||||||
|
publicKey: bot.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
botKey = {
|
||||||
|
encryptedKey: ciphertext,
|
||||||
|
nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
setBot((await setBotActiveStatus({
|
||||||
|
botId: bot._id,
|
||||||
|
isActive: bot.isActive ? false : true,
|
||||||
|
botKey
|
||||||
|
})).bot);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start integration for a given integration option [integrationOption]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.integrationOption - an integration option
|
||||||
|
* @param {String} obj.name
|
||||||
|
* @param {String} obj.type
|
||||||
|
* @param {String} obj.docsLink
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const handleIntegrationOption = async ({ integrationOption }) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
// generate CSRF token for OAuth2 code-token exchange integrations
|
||||||
|
const state = crypto.randomBytes(16).toString("hex");
|
||||||
|
localStorage.setItem('latestCSRFToken', state);
|
||||||
|
|
||||||
|
switch (integrationOption.name) {
|
||||||
|
case 'Heroku':
|
||||||
|
window.location = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
|
||||||
|
break;
|
||||||
|
case 'Vercel':
|
||||||
|
window.location = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
|
||||||
|
break;
|
||||||
|
case 'Netlify':
|
||||||
|
window.location = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`;
|
||||||
|
break;
|
||||||
|
// case 'Fly.io':
|
||||||
|
// console.log('fly.io');
|
||||||
|
// setIntegrationAccessTokenDialogOpen(true);
|
||||||
|
// break;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open dialog to activate bot if bot is not active.
|
||||||
|
* Otherwise, start integration [integrationOption]
|
||||||
|
* @param {Object} integrationOption - an integration option
|
||||||
|
* @param {String} integrationOption.name
|
||||||
|
* @param {String} integrationOption.type
|
||||||
|
* @param {String} integrationOption.docsLink
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const integrationOptionPress = (integrationOption) => {
|
||||||
|
try {
|
||||||
|
if (bot.isActive) {
|
||||||
|
// case: bot is active -> proceed with integration
|
||||||
|
handleIntegrationOption({ integrationOption });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// case: bot is not active -> open modal to activate bot
|
||||||
|
setIsActivateBotDialogOpen(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||||
<Head>
|
<Head>
|
||||||
<title>Dashboard</title>
|
<title>Dashboard</title>
|
||||||
@@ -205,200 +185,41 @@ export default function Integrations() {
|
|||||||
content="Infisical a simple end-to-end encrypted platform that enables teams to sync and manage their .env files."
|
content="Infisical a simple end-to-end encrypted platform that enables teams to sync and manage their .env files."
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex flex-row">
|
<div className="w-full max-h-96 pb-2 h-screen max-h-[calc(100vh-10px)] overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
|
||||||
<div className="w-full max-h-96 pb-2 h-screen max-h-[calc(100vh-10px)] overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
|
<NavHeader
|
||||||
<NavHeader pageName="Project Integrations" isProjectRelated={true} />
|
pageName="Project Integrations"
|
||||||
<div className="flex flex-col justify-between items-start mx-4 mt-6 mb-4 text-xl max-w-5xl px-2">
|
isProjectRelated={true}
|
||||||
<div className="flex flex-row justify-start items-center text-3xl">
|
/>
|
||||||
<p className="font-semibold mr-4">Current Project Integrations</p>
|
<ActivateBotDialog
|
||||||
</div>
|
isOpen={isActivateBotDialogOpen}
|
||||||
<p className="mr-4 text-base text-gray-400">
|
closeModal={() => setIsActivateBotDialogOpen(false)}
|
||||||
Manage your integrations of Infisical with third-party services.
|
selectedIntegrationOption={selectedIntegrationOption}
|
||||||
</p>
|
handleBotActivate={handleBotActivate}
|
||||||
</div>
|
handleIntegrationOption={handleIntegrationOption}
|
||||||
{projectIntegrations.length > 0 ? (
|
/>
|
||||||
projectIntegrations.map((projectIntegration) => (
|
{/* <IntegrationAccessTokenDialog
|
||||||
<Integration
|
isOpen={isIntegrationAccessTokenDialogOpen}
|
||||||
key={guidGenerator()}
|
closeModal={() => setIntegrationAccessTokenDialogOpen(false)}
|
||||||
projectIntegration={projectIntegration}
|
selectedIntegrationOption={selectedIntegrationOption}
|
||||||
/>
|
handleBotActivate={handleBotActivate}
|
||||||
))
|
handleIntegrationOption={handleIntegrationOption}
|
||||||
) : (
|
/> */}
|
||||||
<div className="flex flex-col max-w-5xl justify-center bg-white/5 p-6 rounded-md mx-6 mt-8">
|
<IntegrationSection integrations={integrations} />
|
||||||
<div className="relative px-4 flex flex-col text-gray-400 items-center justify-center">
|
{cloudIntegrationOptions.length > 0 ? (
|
||||||
<div className="mb-1">
|
<CloudIntegrationSection
|
||||||
You {"don't"} have any integrations set up yet. When you do,
|
cloudIntegrationOptions={cloudIntegrationOptions}
|
||||||
they will appear here.
|
setSelectedIntegrationOption={setSelectedIntegrationOption}
|
||||||
</div>
|
integrationOptionPress={integrationOptionPress}
|
||||||
<div className="">
|
integrationAuths={integrationAuths}
|
||||||
To start, click on any of the options below. It takes 5 clicks
|
/>
|
||||||
to set up.
|
) : (
|
||||||
</div>
|
<div></div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
<FrameworkIntegrationSection
|
||||||
)}
|
frameworks={frameworkIntegrationOptions}
|
||||||
<div className="flex flex-col justify-between items-start mx-4 mt-12 mb-4 text-xl max-w-5xl px-2">
|
/>
|
||||||
<div className="flex flex-row justify-start items-center text-3xl">
|
|
||||||
<p className="font-semibold mr-4">
|
|
||||||
Platform & Cloud Integrations
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p className="mr-4 text-base text-gray-400">
|
|
||||||
Click on the itegration you want to connect. This will let your
|
|
||||||
environment variables flow automatically into selected third-party
|
|
||||||
services.
|
|
||||||
</p>
|
|
||||||
<p className="mr-4 text-xs text-gray-600 mt-1">
|
|
||||||
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 support@infisical.com.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-4 grid-cols-4 grid-rows-2 mx-6 mt-4 max-w-5xl">
|
|
||||||
{Object.keys(integrations).map((integration) => (
|
|
||||||
<div
|
|
||||||
className={`relative ${
|
|
||||||
['Heroku'].includes(integrations[integration].name)
|
|
||||||
? ''
|
|
||||||
: 'opacity-50'
|
|
||||||
}`}
|
|
||||||
key={integrations[integration].name}
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href={`${
|
|
||||||
['Heroku'].includes(integrations[integration].name)
|
|
||||||
? `https://id.heroku.com/oauth/authorize?client_id=bc132901-935a-4590-b010-f1857efc380d&response_type=code&scope=write-protected&state=${csrfToken}`
|
|
||||||
: '#'
|
|
||||||
}`}
|
|
||||||
rel="noopener"
|
|
||||||
className={`relative flex flex-row bg-white/5 h-32 rounded-md p-4 items-center ${
|
|
||||||
['Heroku'].includes(integrations[integration].name)
|
|
||||||
? 'hover:bg-white/10 duration-200 cursor-pointer'
|
|
||||||
: 'cursor-default grayscale'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={`/images/integrations/${integrations[integration].name}.png`}
|
|
||||||
height={70}
|
|
||||||
width={70}
|
|
||||||
alt="integration logo"
|
|
||||||
></Image>
|
|
||||||
{integrations[integration].name.split(' ').length > 2 ? (
|
|
||||||
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-3xl ml-4 max-w-xs">
|
|
||||||
<div>{integrations[integration].name.split(' ')[0]}</div>
|
|
||||||
<div className="text-base">
|
|
||||||
{integrations[integration].name.split(' ')[1]}{' '}
|
|
||||||
{integrations[integration].name.split(' ')[2]}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-xl ml-4 max-w-xs">
|
|
||||||
{integrations[integration].name}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
{['Heroku'].includes(integrations[integration].name) &&
|
|
||||||
authorizations
|
|
||||||
.map((authorization) => authorization.integration)
|
|
||||||
.includes(integrations[integration].name.toLowerCase()) && (
|
|
||||||
<div className="absolute group z-50 top-0 right-0 flex flex-row">
|
|
||||||
<div
|
|
||||||
onClick={() => {
|
|
||||||
deleteIntegrationAuth({
|
|
||||||
integrationAuthId: authorizations
|
|
||||||
.filter(
|
|
||||||
(authorization) =>
|
|
||||||
authorization.integration ==
|
|
||||||
integrations[integration].name.toLowerCase()
|
|
||||||
)
|
|
||||||
.map((authorization) => authorization._id)[0]
|
|
||||||
});
|
|
||||||
router.reload();
|
|
||||||
}}
|
|
||||||
className="cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faX}
|
|
||||||
className="text-xs mr-2 py-px"
|
|
||||||
/>
|
|
||||||
Revoke
|
|
||||||
</div>
|
|
||||||
<div className="w-max bg-primary py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90 group-hover:opacity-100 duration-200">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faCheck}
|
|
||||||
className="text-xs mr-2"
|
|
||||||
/>
|
|
||||||
Authorized
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!['Heroku'].includes(integrations[integration].name) && (
|
|
||||||
<div className="absolute group z-50 top-0 right-0 flex flex-row">
|
|
||||||
<div className="w-max bg-yellow py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90">
|
|
||||||
Coming Soon
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col justify-between items-start mx-4 mt-12 mb-4 text-xl max-w-5xl px-2">
|
|
||||||
<div className="flex flex-row justify-start items-center text-3xl">
|
|
||||||
<p className="font-semibold mr-4">Framework Integrations</p>
|
|
||||||
</div>
|
|
||||||
<p className="mr-4 text-base text-gray-400">
|
|
||||||
Click on a framework to get the setup instructions.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-4 grid-cols-7 grid-rows-2 mx-6 mt-4 max-w-5xl">
|
|
||||||
{frameworks.map((framework) => (
|
|
||||||
<div key={framework.name}>
|
|
||||||
<a
|
|
||||||
href={framework.link}
|
|
||||||
rel="noopener"
|
|
||||||
className={`relative flex flex-row items-center justify-center bg-bunker-500 hover:bg-gradient-to-tr hover:from-sky-400 hover:to-primary duration-200 h-32 rounded-md p-0.5 items-center cursor-pointer`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`font-semibold bg-bunker-500 flex flex-col items-center justify-center h-full w-full rounded-md text-gray-300 group-hover:text-gray-200 duration-200 ${
|
|
||||||
framework?.name?.split(' ').length > 1
|
|
||||||
? 'text-sm px-1'
|
|
||||||
: 'text-xl px-2'
|
|
||||||
} text-center w-full max-w-xs`}
|
|
||||||
>
|
|
||||||
{framework?.image && (
|
|
||||||
<Image
|
|
||||||
src={`/images/integrations/${framework.image}.png`}
|
|
||||||
height={framework?.name ? 60 : 90}
|
|
||||||
width={framework?.name ? 60 : 90}
|
|
||||||
alt="integration logo"
|
|
||||||
></Image>
|
|
||||||
)}
|
|
||||||
{framework?.name && framework?.image && (
|
|
||||||
<div className="h-2"></div>
|
|
||||||
)}
|
|
||||||
{framework?.name && framework.name}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="relative z-10 w-10/12 mr-auto h-full ml-2 bg-bunker-800 flex flex-col items-center justify-center">
|
|
||||||
<div className="absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full"></div>
|
|
||||||
<Image
|
|
||||||
src="/images/loading/loading.gif"
|
|
||||||
height={70}
|
|
||||||
width={120}
|
|
||||||
alt="loading animation"
|
|
||||||
></Image>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import Head from "next/head";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
const queryString = require("query-string");
|
||||||
|
import AuthorizeIntegration from "./api/integrations/authorizeIntegration";
|
||||||
|
|
||||||
|
export default function Netlify() {
|
||||||
|
const router = useRouter();
|
||||||
|
const parsedUrl = queryString.parse(router.asPath.split("?")[1]);
|
||||||
|
const code = parsedUrl.code;
|
||||||
|
const state = parsedUrl.state;
|
||||||
|
// modify comment here
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Here we forward to the default workspace if a user opens this url
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
useEffect(async () => {
|
||||||
|
try {
|
||||||
|
if (state === localStorage.getItem('latestCSRFToken')) {
|
||||||
|
localStorage.removeItem('latestCSRFToken');
|
||||||
|
|
||||||
|
await AuthorizeIntegration({
|
||||||
|
workspaceId: localStorage.getItem('projectData.id'),
|
||||||
|
code,
|
||||||
|
integration: "netlify"
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push("/integrations/" + localStorage.getItem("projectData.id"));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Netlify integration error: ', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
Netlify.requireAuth = true;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import Head from "next/head";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
const queryString = require("query-string");
|
||||||
|
import AuthorizeIntegration from "./api/integrations/authorizeIntegration";
|
||||||
|
|
||||||
|
export default function Vercel() {
|
||||||
|
const router = useRouter();
|
||||||
|
const parsedUrl = queryString.parse(router.asPath.split("?")[1]);
|
||||||
|
const code = parsedUrl.code;
|
||||||
|
const state = parsedUrl.state
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Here we forward to the default workspace if a user opens this url
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
useEffect(async () => {
|
||||||
|
try {
|
||||||
|
if (state === localStorage.getItem('latestCSRFToken')) {
|
||||||
|
localStorage.removeItem('latestCSRFToken');
|
||||||
|
|
||||||
|
await AuthorizeIntegration({
|
||||||
|
workspaceId: localStorage.getItem('projectData.id'),
|
||||||
|
code,
|
||||||
|
integration: "vercel"
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push("/integrations/" + localStorage.getItem("projectData.id"));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Vercel integration error: ', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vercel.requireAuth = true;
|
||||||
@@ -1,82 +1,37 @@
|
|||||||
const envMapping = {
|
interface Mapping {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const envMapping: Mapping = {
|
||||||
Development: "dev",
|
Development: "dev",
|
||||||
Staging: "staging",
|
Staging: "staging",
|
||||||
Production: "prod",
|
Production: "prod",
|
||||||
Testing: "test",
|
Testing: "test",
|
||||||
};
|
};
|
||||||
|
|
||||||
const reverseEnvMapping = {
|
const reverseEnvMapping: Mapping = {
|
||||||
dev: "Development",
|
dev: "Development",
|
||||||
staging: "Staging",
|
staging: "Staging",
|
||||||
prod: "Production",
|
prod: "Production",
|
||||||
test: "Testing",
|
test: "Testing",
|
||||||
};
|
};
|
||||||
|
|
||||||
const frameworks = [{
|
const contextNetlifyMapping: Mapping = {
|
||||||
"name": "Docker",
|
"dev": "Local development",
|
||||||
"image": "Docker",
|
"branch-deploy": "Branch deploys",
|
||||||
"link": "https://infisical.com/docs/integrations/platforms/docker"
|
"deploy-review": "Deploy Previews",
|
||||||
}, {
|
"production": "Production"
|
||||||
"name": "Docker Compose",
|
}
|
||||||
"image": "Docker Compose",
|
|
||||||
"link": "https://infisical.com/docs/integrations/platforms/docker-compose"
|
const reverseContextNetlifyMapping: Mapping = {
|
||||||
}, {
|
"Local development": "dev",
|
||||||
"name": "React",
|
"Branch deploys": "branch-deploy",
|
||||||
"image": "React",
|
"Deploy Previews": "deploy-preview",
|
||||||
"link": "https://infisical.com/docs/integrations/frameworks/react"
|
"Production": "production"
|
||||||
}, {
|
}
|
||||||
"name": "Vue",
|
|
||||||
"image": "Vue",
|
|
||||||
"link": "https://infisical.com/docs/integrations/frameworks/vue"
|
|
||||||
}, {
|
|
||||||
"image": "Express",
|
|
||||||
"link": "https://infisical.com/docs/integrations/frameworks/express"
|
|
||||||
},{
|
|
||||||
"image": "Next.js",
|
|
||||||
"link": "https://infisical.com/docs/integrations/frameworks/nextjs"
|
|
||||||
}, {
|
|
||||||
"name": "Django",
|
|
||||||
"image": "Django",
|
|
||||||
"link": "https://infisical.com/docs/integrations/frameworks/django"
|
|
||||||
}, {
|
|
||||||
"name": "NestJS",
|
|
||||||
"image": "NestJS",
|
|
||||||
"link": "https://infisical.com/docs/integrations/frameworks/nestjs"
|
|
||||||
}, {
|
|
||||||
"name": "Nuxt",
|
|
||||||
"image": "Nuxt",
|
|
||||||
"link": "https://infisical.com/docs/integrations/frameworks/nuxt"
|
|
||||||
}, {
|
|
||||||
"name": "Gatsby",
|
|
||||||
"image": "Gatsby",
|
|
||||||
"link": "https://infisical.com/docs/integrations/frameworks/gatsby"
|
|
||||||
}, {
|
|
||||||
"name": "Remix",
|
|
||||||
"image": "Remix",
|
|
||||||
"link": "https://infisical.com/docs/integrations/frameworks/remix"
|
|
||||||
}, {
|
|
||||||
"name": "Vite",
|
|
||||||
"image": "Vite",
|
|
||||||
"link": "https://infisical.com/docs/integrations/frameworks/vite"
|
|
||||||
}, {
|
|
||||||
"image": "Fiber",
|
|
||||||
"link": "https://infisical.com/docs/integrations/frameworks/fiber"
|
|
||||||
}, {
|
|
||||||
"name": "Flask",
|
|
||||||
"image": "Flask",
|
|
||||||
"link": "https://infisical.com/docs/integrations/frameworks/flask"
|
|
||||||
}, {
|
|
||||||
"name": "Laravel",
|
|
||||||
"image": "Laravel",
|
|
||||||
"link": "https://infisical.com/docs/integrations/frameworks/laravel"
|
|
||||||
}, {
|
|
||||||
"image": "Rails",
|
|
||||||
"link": "https://infisical.com/docs/integrations/frameworks/rails"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
contextNetlifyMapping,
|
||||||
envMapping,
|
envMapping,
|
||||||
frameworks,
|
reverseContextNetlifyMapping,
|
||||||
reverseEnvMapping
|
reverseEnvMapping}
|
||||||
};
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1,98 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Docker",
|
||||||
|
"slug": "docker",
|
||||||
|
"image": "Docker",
|
||||||
|
"docsLink": "https://infisical.com/docs/integrations/platforms/docker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Docker Compose",
|
||||||
|
"slug": "docker-compose",
|
||||||
|
"image": "Docker Compose",
|
||||||
|
"docsLink": "https://infisical.com/docs/integrations/platforms/docker-compose"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "React",
|
||||||
|
"slug": "react",
|
||||||
|
"image": "React",
|
||||||
|
"docsLink": "https://infisical.com/docs/integrations/frameworks/react"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vue",
|
||||||
|
"slug": "vue",
|
||||||
|
"image": "Vue",
|
||||||
|
"docsLink": "https://infisical.com/docs/integrations/frameworks/vue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Express",
|
||||||
|
"slug": "express",
|
||||||
|
"image": "Express",
|
||||||
|
"docsLink": "https://infisical.com/docs/integrations/frameworks/express"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Next.js",
|
||||||
|
"slug": "nextjs",
|
||||||
|
"image": "Next.js",
|
||||||
|
"docsLink": "https://infisical.com/docs/integrations/frameworks/nextjs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Django",
|
||||||
|
"slug": "django",
|
||||||
|
"image": "Django",
|
||||||
|
"docsLink": "https://infisical.com/docs/integrations/frameworks/django"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NestJS",
|
||||||
|
"slug": "nestjs",
|
||||||
|
"image": "NestJS",
|
||||||
|
"docsLink": "https://infisical.com/docs/integrations/frameworks/nestjs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nuxt",
|
||||||
|
"slug": "nuxt",
|
||||||
|
"image": "Nuxt",
|
||||||
|
"docsLink": "https://infisical.com/docs/integrations/frameworks/nuxt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gatsby",
|
||||||
|
"slug": "gatsby",
|
||||||
|
"image": "Gatsby",
|
||||||
|
"docsLink": "https://infisical.com/docs/integrations/frameworks/gatsby"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Remix",
|
||||||
|
"slug": "remix",
|
||||||
|
"image": "Remix",
|
||||||
|
"docsLink": "https://infisical.com/docs/integrations/frameworks/remix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vite",
|
||||||
|
"slug": "vite",
|
||||||
|
"image": "Vite",
|
||||||
|
"docsLink": "https://infisical.com/docs/integrations/frameworks/vite"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fiber",
|
||||||
|
"slug": "fiber",
|
||||||
|
"image": "Fiber",
|
||||||
|
"docsLink": "https://infisical.com/docs/integrations/frameworks/fiber"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Flask",
|
||||||
|
"slug": "flask",
|
||||||
|
"image": "Flask",
|
||||||
|
"docsLink": "https://infisical.com/docs/integrations/frameworks/flask"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Laravel",
|
||||||
|
"slug": "laravel",
|
||||||
|
"image": "Laravel",
|
||||||
|
"docsLink": "https://infisical.com/docs/integrations/frameworks/laravel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Rails",
|
||||||
|
"slug": "rails",
|
||||||
|
"image": "Rails",
|
||||||
|
"docsLink": "https://infisical.com/docs/integrations/frameworks/rails"
|
||||||
|
}
|
||||||
|
]
|
||||||
Generated
+1
-23
@@ -8,8 +8,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.29.0",
|
"eslint": "^8.29.0",
|
||||||
"husky": "^8.0.2",
|
"husky": "^8.0.2"
|
||||||
"prettier": "^2.8.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/eslintrc": {
|
"node_modules/@eslint/eslintrc": {
|
||||||
@@ -899,21 +898,6 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
|
||||||
"version": "2.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz",
|
|
||||||
"integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"prettier": "bin-prettier.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.13.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||||
@@ -1809,12 +1793,6 @@
|
|||||||
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"prettier": {
|
|
||||||
"version": "2.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz",
|
|
||||||
"integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"punycode": {
|
"punycode": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||||
|
|||||||
+2
-5
@@ -14,15 +14,12 @@
|
|||||||
"prepare": "husky install"
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"**/*": "prettier --write --ignore-unknown",
|
|
||||||
"*.{js,jsx,ts,tsx}": [
|
"*.{js,jsx,ts,tsx}": [
|
||||||
"eslint --fix",
|
"eslint --fix"
|
||||||
"prettier --write"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.29.0",
|
"eslint": "^8.29.0",
|
||||||
"husky": "^8.0.2",
|
"husky": "^8.0.2"
|
||||||
"prettier": "^2.8.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user