diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts
index 747004f44..be42647de 100644
--- a/backend/src/controllers/v1/integrationAuthController.ts
+++ b/backend/src/controllers/v1/integrationAuthController.ts
@@ -10,6 +10,31 @@ import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
import { IntegrationService } from '../../services';
import { getApps, revokeAccess } from '../../integrations';
+/***
+ * Return integration authorization with id [integrationAuthId]
+ */
+export const getIntegrationAuth = async (req: Request, res: Response) => {
+ let integrationAuth;
+ try {
+ const { integrationAuthId } = req.params;
+ integrationAuth = await IntegrationAuth.findById(integrationAuthId);
+
+ if (!integrationAuth) return res.status(400).send({
+ message: 'Failed to find integration authorization'
+ });
+ } catch (err) {
+ Sentry.setUser({ email: req.user.email });
+ Sentry.captureException(err);
+ return res.status(400).send({
+ message: 'Failed to get integration authorization'
+ });
+ }
+
+ return res.status(200).send({
+ integrationAuth
+ });
+}
+
export const getIntegrationOptions = async (
req: Request,
res: Response
@@ -31,7 +56,6 @@ export const oAuthExchange = async (
) => {
try {
const { workspaceId, code, integration } = req.body;
-
if (!INTEGRATION_SET.has(integration))
throw new Error('Failed to validate integration');
@@ -81,6 +105,13 @@ export const saveIntegrationAccessToken = async (
integration: string;
} = req.body;
+ const bot = await Bot.findOne({
+ workspace: new Types.ObjectId(workspaceId),
+ isActive: true
+ });
+
+ if (!bot) throw new Error('Bot must be enabled to save integration access token');
+
integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: new Types.ObjectId(workspaceId),
integration
@@ -91,13 +122,6 @@ export const saveIntegrationAccessToken = async (
new: true,
upsert: true
});
-
- const bot = await Bot.findOne({
- workspace: new Types.ObjectId(workspaceId),
- isActive: true
- });
-
- if (!bot) throw new Error('Bot must be enabled to save integration access token');
// encrypt and save integration access token
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts
index 0ffb8cbb8..779cb3cb7 100644
--- a/backend/src/controllers/v1/integrationController.ts
+++ b/backend/src/controllers/v1/integrationController.ts
@@ -24,23 +24,34 @@ export const createIntegration = async (req: Request, res: Response) => {
app,
appId,
isActive,
+ sourceEnvironment,
targetEnvironment,
owner
} = req.body;
+
+ // TODO: validate [sourceEnvironment] and [targetEnvironment]
// initialize new integration after saving integration access token
integration = await new Integration({
workspace: req.integrationAuth.workspace._id,
- environment: req.integrationAuth.workspace?.environments[0].slug,
+ environment: sourceEnvironment,
isActive,
app,
appId,
targetEnvironment,
+ owner,
integration: req.integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId)
}).save();
- // TODO: run sync function
+ if (integration) {
+ // trigger event - push secrets
+ EventService.handleEvent({
+ event: eventPushSecrets({
+ workspaceId: integration.workspace.toString()
+ })
+ });
+ }
} catch (err) {
Sentry.setUser({ email: req.user.email });
diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts
index 30e370188..17338ee68 100644
--- a/backend/src/helpers/integration.ts
+++ b/backend/src/helpers/integration.ts
@@ -43,9 +43,7 @@ const handleOAuthExchangeHelper = async ({
code: string;
environment: string;
}) => {
- let action;
let integrationAuth;
- // let newIntegration;
try {
const bot = await Bot.findOne({
workspace: workspaceId,
@@ -100,16 +98,6 @@ const handleOAuthExchangeHelper = async ({
accessExpiresAt: res.accessExpiresAt
});
}
-
- // // initialize new integration after exchange
- // newIntegration = await new Integration({
- // workspace: workspaceId,
- // isActive: false,
- // app: null,
- // environment,
- // integration,
- // integrationAuth: integrationAuth._id
- // }).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts
index ada2b76bd..846c9e7db 100644
--- a/backend/src/integrations/exchange.ts
+++ b/backend/src/integrations/exchange.ts
@@ -144,7 +144,7 @@ const exchangeCodeAzure = async ({
scope: 'https://vault.azure.net/.default openid offline_access', // TODO: do we need all these permissions?
client_id: CLIENT_ID_AZURE,
client_secret: CLIENT_SECRET_AZURE,
- redirect_uri: `${SITE_URL}/azure-key-vault`
+ redirect_uri: `${SITE_URL}/integrations/azure-key-vault/oauth2/callback`
} as any)
)).data;
@@ -227,7 +227,7 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
code: code,
client_id: CLIENT_ID_VERCEL,
client_secret: CLIENT_SECRET_VERCEL,
- redirect_uri: `${SITE_URL}/vercel`
+ redirect_uri: `${SITE_URL}/integrations/vercel/oauth2/callback`
} as any)
)
).data;
@@ -267,7 +267,7 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => {
code: code,
client_id: CLIENT_ID_NETLIFY,
client_secret: CLIENT_SECRET_NETLIFY,
- redirect_uri: `${SITE_URL}/netlify`
+ redirect_uri: `${SITE_URL}/integrations/netlify/oauth2/callback`
} as any)
)
).data;
@@ -319,7 +319,7 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
client_id: CLIENT_ID_GITHUB,
client_secret: CLIENT_SECRET_GITHUB,
code: code,
- redirect_uri: `${SITE_URL}/github`
+ redirect_uri: `${SITE_URL}/integrations/github/oauth2/callback`
},
headers: {
Accept: 'application/json'
diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts
index 4604d744a..401c7f9a1 100644
--- a/backend/src/integrations/sync.ts
+++ b/backend/src/integrations/sync.ts
@@ -1,9 +1,7 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import { Octokit } from '@octokit/rest';
-// import * as sodium from 'libsodium-wrappers';
import sodium from 'libsodium-wrappers';
-// const sodium = require('libsodium-wrappers');
import { IIntegration, IIntegrationAuth } from '../models';
import {
INTEGRATION_AZURE_KEY_VAULT,
diff --git a/backend/src/routes/v1/integration.ts b/backend/src/routes/v1/integration.ts
index 3db9c412e..90cea3a38 100644
--- a/backend/src/routes/v1/integration.ts
+++ b/backend/src/routes/v1/integration.ts
@@ -23,6 +23,7 @@ router.post( // new: add new integration for integration auth
body('app').isString().trim(),
body('isActive').exists().isBoolean(),
body('appId').trim(),
+ body('sourceEnvironment').trim(),
body('targetEnvironment').trim(),
body('owner').trim(),
validateRequest,
diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts
index 3f88aa7e5..1918140c2 100644
--- a/backend/src/routes/v1/integrationAuth.ts
+++ b/backend/src/routes/v1/integrationAuth.ts
@@ -18,6 +18,19 @@ router.get(
integrationAuthController.getIntegrationOptions
);
+router.get(
+ '/:integrationAuthId',
+ requireAuth({
+ acceptedAuthModes: ['jwt']
+ }),
+ requireIntegrationAuthorizationAuth({
+ acceptedRoles: [ADMIN, MEMBER]
+ }),
+ param('integrationAuthId'),
+ validateRequest,
+ integrationAuthController.getIntegrationAuth
+);
+
router.post(
'/oauth-token',
requireAuth({
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 7f52e04ae..b0317ff7c 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -15,6 +15,7 @@
"@fortawesome/react-fontawesome": "^0.1.19",
"@headlessui/react": "^1.6.6",
"@hookform/resolvers": "^2.9.10",
+ "@octokit/rest": "^19.0.7",
"@radix-ui/react-accordion": "^1.1.0",
"@radix-ui/react-alert-dialog": "^1.0.2",
"@radix-ui/react-checkbox": "^1.0.1",
@@ -3521,6 +3522,153 @@
"node": ">= 8"
}
},
+ "node_modules/@octokit/auth-token": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.3.tgz",
+ "integrity": "sha512-/aFM2M4HVDBT/jjDBa84sJniv1t9Gm/rLkalaz9htOm+L+8JMj1k9w0CkUdcxNyNxZPlTxKPVko+m1VlM58ZVA==",
+ "dependencies": {
+ "@octokit/types": "^9.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@octokit/core": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.2.0.tgz",
+ "integrity": "sha512-AgvDRUg3COpR82P7PBdGZF/NNqGmtMq2NiPqeSsDIeCfYFOZ9gddqWNQHnFdEUf+YwOj4aZYmJnlPp7OXmDIDg==",
+ "dependencies": {
+ "@octokit/auth-token": "^3.0.0",
+ "@octokit/graphql": "^5.0.0",
+ "@octokit/request": "^6.0.0",
+ "@octokit/request-error": "^3.0.0",
+ "@octokit/types": "^9.0.0",
+ "before-after-hook": "^2.2.0",
+ "universal-user-agent": "^6.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@octokit/endpoint": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.5.tgz",
+ "integrity": "sha512-LG4o4HMY1Xoaec87IqQ41TQ+glvIeTKqfjkCEmt5AIwDZJwQeVZFIEYXrYY6yLwK+pAScb9Gj4q+Nz2qSw1roA==",
+ "dependencies": {
+ "@octokit/types": "^9.0.0",
+ "is-plain-object": "^5.0.0",
+ "universal-user-agent": "^6.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@octokit/graphql": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.5.tgz",
+ "integrity": "sha512-Qwfvh3xdqKtIznjX9lz2D458r7dJPP8l6r4GQkIdWQouZwHQK0mVT88uwiU2bdTU2OtT1uOlKpRciUWldpG0yQ==",
+ "dependencies": {
+ "@octokit/request": "^6.0.0",
+ "@octokit/types": "^9.0.0",
+ "universal-user-agent": "^6.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@octokit/openapi-types": {
+ "version": "16.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-16.0.0.tgz",
+ "integrity": "sha512-JbFWOqTJVLHZSUUoF4FzAZKYtqdxWu9Z5m2QQnOyEa04fOFljvyh7D3GYKbfuaSWisqehImiVIMG4eyJeP5VEA=="
+ },
+ "node_modules/@octokit/plugin-paginate-rest": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-6.0.0.tgz",
+ "integrity": "sha512-Sq5VU1PfT6/JyuXPyt04KZNVsFOSBaYOAq2QRZUwzVlI10KFvcbUo8lR258AAQL1Et60b0WuVik+zOWKLuDZxw==",
+ "dependencies": {
+ "@octokit/types": "^9.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=4"
+ }
+ },
+ "node_modules/@octokit/plugin-request-log": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz",
+ "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==",
+ "peerDependencies": {
+ "@octokit/core": ">=3"
+ }
+ },
+ "node_modules/@octokit/plugin-rest-endpoint-methods": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-7.0.1.tgz",
+ "integrity": "sha512-pnCaLwZBudK5xCdrR823xHGNgqOzRnJ/mpC/76YPpNP7DybdsJtP7mdOwh+wYZxK5jqeQuhu59ogMI4NRlBUvA==",
+ "dependencies": {
+ "@octokit/types": "^9.0.0",
+ "deprecation": "^2.3.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=3"
+ }
+ },
+ "node_modules/@octokit/request": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.3.tgz",
+ "integrity": "sha512-TNAodj5yNzrrZ/VxP+H5HiYaZep0H3GU0O7PaF+fhDrt8FPrnkei9Aal/txsN/1P7V3CPiThG0tIvpPDYUsyAA==",
+ "dependencies": {
+ "@octokit/endpoint": "^7.0.0",
+ "@octokit/request-error": "^3.0.0",
+ "@octokit/types": "^9.0.0",
+ "is-plain-object": "^5.0.0",
+ "node-fetch": "^2.6.7",
+ "universal-user-agent": "^6.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@octokit/request-error": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz",
+ "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==",
+ "dependencies": {
+ "@octokit/types": "^9.0.0",
+ "deprecation": "^2.0.0",
+ "once": "^1.4.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@octokit/rest": {
+ "version": "19.0.7",
+ "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.7.tgz",
+ "integrity": "sha512-HRtSfjrWmWVNp2uAkEpQnuGMJsu/+dBr47dRc5QVgsCbnIc1+GFEaoKBWkYG+zjrsHpSqcAElMio+n10c0b5JA==",
+ "dependencies": {
+ "@octokit/core": "^4.1.0",
+ "@octokit/plugin-paginate-rest": "^6.0.0",
+ "@octokit/plugin-request-log": "^1.0.4",
+ "@octokit/plugin-rest-endpoint-methods": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@octokit/types": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.0.0.tgz",
+ "integrity": "sha512-LUewfj94xCMH2rbD5YJ+6AQ4AVjFYTgpp6rboWM5T7N3IsIF65SBEOVcYMGAEzO/kKNiNaW4LoWtoThOhH06gw==",
+ "dependencies": {
+ "@octokit/openapi-types": "^16.0.0"
+ }
+ },
"node_modules/@pkgr/utils": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz",
@@ -8789,6 +8937,11 @@
}
]
},
+ "node_modules/before-after-hook": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
+ "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
+ },
"node_modules/better-opn": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/better-opn/-/better-opn-2.1.1.tgz",
@@ -10371,6 +10524,11 @@
"node": ">= 0.8"
}
},
+ "node_modules/deprecation": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
+ "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
+ },
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -14099,7 +14257,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -16455,7 +16612,6 @@
"version": "2.6.8",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz",
"integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==",
- "dev": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
@@ -21217,8 +21373,7 @@
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
- "dev": true
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/trim-lines": {
"version": "3.0.1",
@@ -21664,6 +21819,11 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/universal-user-agent": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
+ "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
+ },
"node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@@ -22068,8 +22228,7 @@
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
- "dev": true
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/webpack": {
"version": "5.75.0",
@@ -22267,7 +22426,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
- "dev": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
@@ -24829,6 +24987,118 @@
"fastq": "^1.6.0"
}
},
+ "@octokit/auth-token": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.3.tgz",
+ "integrity": "sha512-/aFM2M4HVDBT/jjDBa84sJniv1t9Gm/rLkalaz9htOm+L+8JMj1k9w0CkUdcxNyNxZPlTxKPVko+m1VlM58ZVA==",
+ "requires": {
+ "@octokit/types": "^9.0.0"
+ }
+ },
+ "@octokit/core": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.2.0.tgz",
+ "integrity": "sha512-AgvDRUg3COpR82P7PBdGZF/NNqGmtMq2NiPqeSsDIeCfYFOZ9gddqWNQHnFdEUf+YwOj4aZYmJnlPp7OXmDIDg==",
+ "requires": {
+ "@octokit/auth-token": "^3.0.0",
+ "@octokit/graphql": "^5.0.0",
+ "@octokit/request": "^6.0.0",
+ "@octokit/request-error": "^3.0.0",
+ "@octokit/types": "^9.0.0",
+ "before-after-hook": "^2.2.0",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "@octokit/endpoint": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.5.tgz",
+ "integrity": "sha512-LG4o4HMY1Xoaec87IqQ41TQ+glvIeTKqfjkCEmt5AIwDZJwQeVZFIEYXrYY6yLwK+pAScb9Gj4q+Nz2qSw1roA==",
+ "requires": {
+ "@octokit/types": "^9.0.0",
+ "is-plain-object": "^5.0.0",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "@octokit/graphql": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.5.tgz",
+ "integrity": "sha512-Qwfvh3xdqKtIznjX9lz2D458r7dJPP8l6r4GQkIdWQouZwHQK0mVT88uwiU2bdTU2OtT1uOlKpRciUWldpG0yQ==",
+ "requires": {
+ "@octokit/request": "^6.0.0",
+ "@octokit/types": "^9.0.0",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "@octokit/openapi-types": {
+ "version": "16.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-16.0.0.tgz",
+ "integrity": "sha512-JbFWOqTJVLHZSUUoF4FzAZKYtqdxWu9Z5m2QQnOyEa04fOFljvyh7D3GYKbfuaSWisqehImiVIMG4eyJeP5VEA=="
+ },
+ "@octokit/plugin-paginate-rest": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-6.0.0.tgz",
+ "integrity": "sha512-Sq5VU1PfT6/JyuXPyt04KZNVsFOSBaYOAq2QRZUwzVlI10KFvcbUo8lR258AAQL1Et60b0WuVik+zOWKLuDZxw==",
+ "requires": {
+ "@octokit/types": "^9.0.0"
+ }
+ },
+ "@octokit/plugin-request-log": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz",
+ "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==",
+ "requires": {}
+ },
+ "@octokit/plugin-rest-endpoint-methods": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-7.0.1.tgz",
+ "integrity": "sha512-pnCaLwZBudK5xCdrR823xHGNgqOzRnJ/mpC/76YPpNP7DybdsJtP7mdOwh+wYZxK5jqeQuhu59ogMI4NRlBUvA==",
+ "requires": {
+ "@octokit/types": "^9.0.0",
+ "deprecation": "^2.3.1"
+ }
+ },
+ "@octokit/request": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.3.tgz",
+ "integrity": "sha512-TNAodj5yNzrrZ/VxP+H5HiYaZep0H3GU0O7PaF+fhDrt8FPrnkei9Aal/txsN/1P7V3CPiThG0tIvpPDYUsyAA==",
+ "requires": {
+ "@octokit/endpoint": "^7.0.0",
+ "@octokit/request-error": "^3.0.0",
+ "@octokit/types": "^9.0.0",
+ "is-plain-object": "^5.0.0",
+ "node-fetch": "^2.6.7",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "@octokit/request-error": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz",
+ "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==",
+ "requires": {
+ "@octokit/types": "^9.0.0",
+ "deprecation": "^2.0.0",
+ "once": "^1.4.0"
+ }
+ },
+ "@octokit/rest": {
+ "version": "19.0.7",
+ "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.7.tgz",
+ "integrity": "sha512-HRtSfjrWmWVNp2uAkEpQnuGMJsu/+dBr47dRc5QVgsCbnIc1+GFEaoKBWkYG+zjrsHpSqcAElMio+n10c0b5JA==",
+ "requires": {
+ "@octokit/core": "^4.1.0",
+ "@octokit/plugin-paginate-rest": "^6.0.0",
+ "@octokit/plugin-request-log": "^1.0.4",
+ "@octokit/plugin-rest-endpoint-methods": "^7.0.0"
+ }
+ },
+ "@octokit/types": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.0.0.tgz",
+ "integrity": "sha512-LUewfj94xCMH2rbD5YJ+6AQ4AVjFYTgpp6rboWM5T7N3IsIF65SBEOVcYMGAEzO/kKNiNaW4LoWtoThOhH06gw==",
+ "requires": {
+ "@octokit/openapi-types": "^16.0.0"
+ }
+ },
"@pkgr/utils": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz",
@@ -28742,6 +29012,11 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
+ "before-after-hook": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
+ "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
+ },
"better-opn": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/better-opn/-/better-opn-2.1.1.tgz",
@@ -29938,6 +30213,11 @@
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
+ "deprecation": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
+ "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
+ },
"dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -32732,8 +33012,7 @@
"is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
- "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
- "dev": true
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
},
"is-regex": {
"version": "1.1.4",
@@ -34410,7 +34689,6 @@
"version": "2.6.8",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz",
"integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==",
- "dev": true,
"requires": {
"whatwg-url": "^5.0.0"
}
@@ -37863,8 +38141,7 @@
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
- "dev": true
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"trim-lines": {
"version": "3.0.1",
@@ -38186,6 +38463,11 @@
"unist-util-is": "^5.0.0"
}
},
+ "universal-user-agent": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
+ "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
+ },
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@@ -38479,8 +38761,7 @@
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
- "dev": true
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"webpack": {
"version": "5.75.0",
@@ -38626,7 +38907,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
- "dev": true,
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
diff --git a/frontend/package.json b/frontend/package.json
index db8153a0a..4e2e3e585 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -22,6 +22,7 @@
"@fortawesome/react-fontawesome": "^0.1.19",
"@headlessui/react": "^1.6.6",
"@hookform/resolvers": "^2.9.10",
+ "@octokit/rest": "^19.0.7",
"@radix-ui/react-accordion": "^1.1.0",
"@radix-ui/react-alert-dialog": "^1.0.2",
"@radix-ui/react-checkbox": "^1.0.1",
diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts
index 6ac74cdbd..f25da26d3 100644
--- a/frontend/public/data/frequentConstants.ts
+++ b/frontend/public/data/frequentConstants.ts
@@ -29,7 +29,7 @@ const reverseEnvMapping: Mapping = {
const contextNetlifyMapping: Mapping = {
"dev": "Local development",
"branch-deploy": "Branch deploys",
- "deploy-review": "Deploy Previews",
+ "deploy-preview": "Deploy Previews",
"production": "Production"
}
diff --git a/frontend/src/components/basic/Layout.tsx b/frontend/src/components/basic/Layout.tsx
index 1f1ee246d..c6e234058 100644
--- a/frontend/src/components/basic/Layout.tsx
+++ b/frontend/src/components/basic/Layout.tsx
@@ -199,13 +199,13 @@ const Layout = ({ children }: LayoutProps) => {
.split('/')
[router.asPath.split('/').length - 1].split('?')[0];
- if (!['heroku', 'vercel', 'github', 'netlify', 'azure-key-vault'].includes(intendedWorkspaceId)) {
+ if (!['callback', 'create', 'authorize'].includes(intendedWorkspaceId)) {
localStorage.setItem('projectData.id', intendedWorkspaceId);
}
-
+
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
if (
- !['heroku', 'vercel', 'github', 'netlify', 'azure-key-vault'].includes(intendedWorkspaceId) &&
+ !['callback', 'create', 'authorize'].includes(intendedWorkspaceId) &&
!userWorkspaces
.map((workspace: { _id: string }) => workspace._id)
.includes(intendedWorkspaceId)
diff --git a/frontend/src/components/basic/dialog/IntegrationAccessTokenDialog.tsx b/frontend/src/components/basic/dialog/IntegrationAccessTokenDialog.tsx
deleted file mode 100644
index 0a09f21e9..000000000
--- a/frontend/src/components/basic/dialog/IntegrationAccessTokenDialog.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-import { Fragment, useState } from "react";
-import { Dialog, Transition } from "@headlessui/react";
-
-import Button from "../buttons/Button";
-import InputField from "../InputField";
-
-interface IntegrationOption {
- clientId: string;
- clientSlug?: string; // vercel-integration specific
- docsLink: string;
- image: string;
- isAvailable: boolean;
- name: string;
- slug: string;
- type: string;
-}
-
-type Props = {
- isOpen: boolean;
- closeModal: () => void;
- selectedIntegrationOption: IntegrationOption | null
- handleIntegrationOption: (arg:{
- integrationOption: IntegrationOption,
- accessToken?: string;
-})=>void;
-};
-
-const IntegrationAccessTokenDialog = ({
- isOpen,
- closeModal,
- selectedIntegrationOption,
- handleIntegrationOption
-}:Props) => {
- const [accessToken, setAccessToken] = useState('');
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const submit = async () => {
- try {
- if (selectedIntegrationOption && accessToken !== '') {
- handleIntegrationOption({
- integrationOption: selectedIntegrationOption,
- accessToken
- });
- closeModal();
- setAccessToken('');
- }
- } catch (err) {
- console.log(err);
- }
- }
-
- return (
-
-
-
-
-
- );
-}
-
-export default IntegrationAccessTokenDialog;
\ No newline at end of file
diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx
new file mode 100644
index 000000000..43920b65f
--- /dev/null
+++ b/frontend/src/hooks/api/integrationAuth/index.tsx
@@ -0,0 +1,3 @@
+export {
+ useGetIntegrationAuthApps,
+ useGetIntegrationAuthById} from './queries';
\ No newline at end of file
diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx
new file mode 100644
index 000000000..508afc741
--- /dev/null
+++ b/frontend/src/hooks/api/integrationAuth/queries.tsx
@@ -0,0 +1,38 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import {
+ App,
+ IntegrationAuth} from './types';
+
+const integrationAuthKeys = {
+ getIntegrationAuthById: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuth'] as const,
+ getIntegrationAuthApps: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuthApps'] as const,
+}
+
+const fetchIntegrationAuthById = async (integrationAuthId: string) => {
+ const { data } = await apiRequest.get<{ integrationAuth: IntegrationAuth }>(`/api/v1/integration-auth/${integrationAuthId}`);
+ return data.integrationAuth;
+}
+
+const fetchIntegrationAuthApps = async (integrationAuthId: string) => {
+ const { data } = await apiRequest.get<{ apps: App[] }>(`/api/v1/integration-auth/${integrationAuthId}/apps`);
+ return data.apps;
+}
+
+export const useGetIntegrationAuthById = (integrationAuthId: string) => {
+ return useQuery({
+ queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
+ queryFn: () => fetchIntegrationAuthById(integrationAuthId),
+ enabled: true
+ });
+}
+
+export const useGetIntegrationAuthApps = (integrationAuthId: string) => {
+ return useQuery({
+ queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId),
+ queryFn: () => fetchIntegrationAuthApps(integrationAuthId),
+ enabled: true
+ });
+}
\ No newline at end of file
diff --git a/frontend/src/hooks/api/integrationAuth/types.ts b/frontend/src/hooks/api/integrationAuth/types.ts
new file mode 100644
index 000000000..bf193eafe
--- /dev/null
+++ b/frontend/src/hooks/api/integrationAuth/types.ts
@@ -0,0 +1,13 @@
+export type IntegrationAuth = {
+ _id: string;
+ workspace: string;
+ integration: string;
+ teamId?: string;
+ accountId?: string;
+}
+
+export type App = {
+ name: string;
+ appId?: string;
+ owner?: string;
+}
\ No newline at end of file
diff --git a/frontend/src/pages/api/integrations/createIntegration.ts b/frontend/src/pages/api/integrations/createIntegration.ts
index c2da2378a..6b83d3fdc 100644
--- a/frontend/src/pages/api/integrations/createIntegration.ts
+++ b/frontend/src/pages/api/integrations/createIntegration.ts
@@ -4,6 +4,10 @@ interface Props {
integrationAuthId: string;
isActive: boolean;
app: string | null;
+ appId: string | null;
+ sourceEnvironment: string;
+ targetEnvironment: string | null;
+ owner: string | null;
}
/**
* This route creates a new integration based on the integration authorization with id [integrationAuthId]
@@ -14,7 +18,11 @@ interface Props {
const createIntegration = ({
integrationAuthId,
isActive,
- app
+ app,
+ appId,
+ sourceEnvironment,
+ targetEnvironment,
+ owner
}: Props) =>
SecurityClient.fetchCall('/api/v1/integration', {
method: 'POST',
@@ -24,7 +32,11 @@ const createIntegration = ({
body: JSON.stringify({
integrationAuthId,
isActive,
- app
+ app,
+ appId,
+ sourceEnvironment,
+ targetEnvironment,
+ owner
})
}).then(async (res) => {
if (res && res.status === 200) {
diff --git a/frontend/src/pages/azure-key-vault.tsx b/frontend/src/pages/azure-key-vault.tsx
deleted file mode 100644
index 71a3c6db7..000000000
--- a/frontend/src/pages/azure-key-vault.tsx
+++ /dev/null
@@ -1,149 +0,0 @@
-import { useEffect, useState } from 'react';
-import { useRouter } from 'next/router';
-import queryString from 'query-string';
-
-import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
-
-import {
- Button,
- Card,
- CardTitle,
- FormControl,
- Input,
- Select,
- SelectItem
-} from '../components/v2';
-import { useGetWorkspaceById } from '../hooks/api/workspace';
-import AuthorizeIntegration from './api/integrations/authorizeIntegration';
-import createIntegration from './api/integrations/createIntegration';
-
-interface IntegrationAuth {
- _id: string;
- integration: string;
- workspace: string;
- createdAt: string;
- updatedAt: string;
-}
-
-export default function AzureKeyVault() {
- const router = useRouter();
- const workspaceResult = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
-
- const { code, state } = queryString.parse(router.asPath.split('?')[1]);
-
- const [integrationAuth, setIntegrationAuth] = useState(null);
- const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
-
- const [vaultBaseUrl, setVaultBaseUrl] = useState('');
- const [vaultBaseUrlErrorText, setVaultBaseUrlErrorText] = useState('');
-
- const [isLoading, setIsLoading] = useState(false);
-
- useEffect(() => {
- (async () => {
- try {
- if (state !== localStorage.getItem('latestCSRFToken')) return;
- localStorage.removeItem('latestCSRFToken');
-
- setIntegrationAuth(await AuthorizeIntegration({
- workspaceId: localStorage.getItem('projectData.id') as string,
- code: code as string,
- integration: 'azure-key-vault',
- }));
- } catch (error) {
- console.error('Azure Key Vault integration error: ', error);
- }
- })();
- }, []);
-
- useEffect(() => {
- if (workspaceResult && workspaceResult.data) {
- setSelectedSourceEnvironment(workspaceResult.data.environments[0].slug);
- }
- }, [workspaceResult]);
-
- const handleButtonClick = async () => {
- try {
- if (vaultBaseUrl.length === 0) {
- setVaultBaseUrlErrorText('Vault URI cannot be blank');
- return;
- }
-
- if (
- !vaultBaseUrl.startsWith('https://')
- || !vaultBaseUrl.endsWith('vault.azure.net')
- ) {
- setVaultBaseUrlErrorText('Vault URI must be like https://.vault.azure.net');
- return;
- }
-
- if (!integrationAuth?._id) return;
-
- setIsLoading(true);
- await createIntegration({
- integrationAuthId: integrationAuth?._id,
- isActive: true,
- app: vaultBaseUrl
- });
- setIsLoading(false);
-
- router.push(
- `/integrations/${localStorage.getItem('projectData.id')}`
- );
-
- } catch (err) {
- console.error(err);
- }
- }
-
- if (!workspaceResult) return
-
- const { data: w } = workspaceResult;
-
- return (integrationAuth && w && selectedSourceEnvironment) ? (
-
-
- Azure Key Vault Integration
-
-
-
-
- setVaultBaseUrl(e.target.value)}
- />
-
-
-
-
- ) :
-}
-
-AzureKeyVault.requireAuth = true;
-
-export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/[id].tsx b/frontend/src/pages/integrations/[id].tsx
index f065494cd..ac2669de8 100644
--- a/frontend/src/pages/integrations/[id].tsx
+++ b/frontend/src/pages/integrations/[id].tsx
@@ -7,7 +7,6 @@ import { useTranslation } from 'next-i18next';
import frameworkIntegrationOptions from 'public/json/frameworkIntegrations.json';
import ActivateBotDialog from '@app/components/basic/dialog/ActivateBotDialog';
-import IntegrationAccessTokenDialog from '@app/components/basic/dialog/IntegrationAccessTokenDialog';
import CloudIntegrationSection from '@app/components/integrations/CloudIntegrationSection';
import FrameworkIntegrationSection from '@app/components/integrations/FrameworkIntegrationSection';
import IntegrationSection from '@app/components/integrations/IntegrationSection';
@@ -20,12 +19,10 @@ import {
} from '../../components/utilities/cryptography/crypto';
import getBot from '../api/bot/getBot';
import setBotActiveStatus from '../api/bot/setBotActiveStatus';
-import createIntegration from '../api/integrations/createIntegration';
import deleteIntegration from '../api/integrations/DeleteIntegration';
import getIntegrationOptions from '../api/integrations/GetIntegrationOptions';
import getWorkspaceAuthorizations from '../api/integrations/getWorkspaceAuthorizations';
import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations';
-import saveIntegrationAccessToken from '../api/integrations/saveIntegrationAccessToken';
import getAWorkspace from '../api/workspace/getAWorkspace';
import getLatestFileKey from '../api/workspace/getLatestFileKey';
@@ -75,7 +72,6 @@ export default function Integrations() {
// TODO: These will have its type when migratiing towards react-query
const [bot, setBot] = useState(null);
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
- const [isIntegrationAccessTokenDialogOpen, setIntegrationAccessTokenDialogOpen] = useState(false);
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState(null);
const router = useRouter();
@@ -166,87 +162,83 @@ export default function Integrations() {
}
};
- /**
- * Handle integration option authorization 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,
- accessToken
- }: {
- integrationOption: IntegrationOption,
- accessToken?: string;
- }) => {
+ const handleUnauthorizedIntegrationOptionPress = (integrationOption: IntegrationOption) => {
try {
- if (!bot.isActive) {
- await handleBotActivate();
+ // generate CSRF token for OAuth2 code-token exchange integrations
+ const state = crypto.randomBytes(16).toString('hex');
+ localStorage.setItem('latestCSRFToken', state);
+
+ let link = '';
+ switch (integrationOption.slug) {
+ case 'azure-key-vault':
+ link = `https://login.microsoftonline.com/${integrationOption.tenantId}/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
+ break;
+ case 'heroku':
+ link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
+ break;
+ case 'vercel':
+ link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
+ break;
+ case 'netlify':
+ link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
+ break;
+ case 'github':
+ link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
+ break;
+ case 'render':
+ link = `${window.location.origin}/integrations/render/authorize`
+ break;
+ case 'flyio':
+ link = `${window.location.origin}/integrations/flyio/authorize`
+ break;
+ default:
+ break;
}
-
- if (integrationOption.type === 'oauth') {
- // integration is of type OAuth
- // generate CSRF token for OAuth2 code-token exchange integrations
- const state = crypto.randomBytes(16).toString('hex');
- localStorage.setItem('latestCSRFToken', state);
-
- switch (integrationOption.slug) {
- case 'azure-key-vault':
- window.location.assign(
- `https://login.microsoftonline.com/${integrationOption.tenantId}/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/azure-key-vault&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`
- );
- break;
- case 'heroku':
- window.location.assign(
- `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`
- );
- break;
- case 'vercel':
- window.location.assign(
- `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`
- );
- break;
- case 'netlify':
- window.location.assign(
- `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`
- );
- break;
- case 'github':
- window.location.assign(
- `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/github&state=${state}`
- );
- break;
- default:
- break;
- }
- return;
- } if (integrationOption.type === 'pat') {
- // integration is of type personal access token
- const integrationAuth = await saveIntegrationAccessToken({
- workspaceId: localStorage.getItem('projectData.id'),
- integration: integrationOption.slug,
- accessToken: accessToken ?? ''
- });
-
- setIntegrationAuths([...integrationAuths, integrationAuth])
-
- const integration = await createIntegration({
- integrationAuthId: integrationAuth._id,
- isActive: false,
- app: null
- });
-
- setIntegrations([...integrations, integration]);
- return;
+ if (link !== '') {
+ window.location.assign(link);
}
} catch (err) {
console.error(err);
}
- };
+ }
+
+ const handleAuthorizedIntegrationOptionPress = (integrationAuth: IntegrationAuth) => {
+ try {
+ let link = '';
+ switch (integrationAuth.integration) {
+ case 'azure-key-vault':
+ link = `${window.location.origin}/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`;
+ break;
+ case 'heroku':
+ link = `${window.location.origin}/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`;
+ break;
+ case 'vercel':
+ link = `${window.location.origin}/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`;
+ break;
+ case 'netlify':
+ link = `${window.location.origin}/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`;
+ break;
+ case 'github':
+ link = `${window.location.origin}/integrations/github/create?integrationAuthId=${integrationAuth._id}`;
+ break;
+ case 'render':
+ link = `${window.location.origin}/integrations/render/create?integrationAuthId=${integrationAuth._id}`;
+ break;
+ case 'flyio':
+ link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
+ break;
+ default:
+ break;
+ }
+
+ if (link !== '') {
+ window.location.assign(link);
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ }
/**
* Open dialog to activate bot if bot is not active.
@@ -258,39 +250,20 @@ export default function Integrations() {
* @returns
*/
const integrationOptionPress = async (integrationOption: IntegrationOption) => {
- // consider: don't start integration until at [handleIntegrationOption] step
try {
const integrationAuthX = integrationAuths.find((integrationAuth) => integrationAuth.integration === integrationOption.slug);
-
- if (!integrationAuthX) {
- // case: integration has not been authorized before
-
- if (integrationOption.type === 'pat') {
- // case: integration requires user to input their personal access token for that integration
- setIntegrationAccessTokenDialogOpen(true);
- return;
- }
-
- // case: integration does not require user to input their personal access token (i.e. it's an OAuth2 integration)
- handleIntegrationOption({ integrationOption });
- return;
- }
-
+
if (!bot.isActive) {
await handleBotActivate();
}
- // case: integration has been authorized before
- // -> create new integration
-
- if (!['azure-key-vault'].includes(integrationOption.slug)) {
- const integration = await createIntegration({
- integrationAuthId: integrationAuthX._id
- });
- setIntegrations([...integrations, integration]);
- } else {
- handleIntegrationOption({ integrationOption });
+ if (!integrationAuthX) {
+ // case: integration has not been authorized
+ handleUnauthorizedIntegrationOptionPress(integrationOption);
+ return;
}
+
+ handleAuthorizedIntegrationOptionPress(integrationAuthX);
} catch (err) {
console.error(err);
}
@@ -372,12 +345,6 @@ export default function Integrations() {
selectedIntegrationOption={selectedIntegrationOption}
integrationOptionPress={integrationOptionPress}
/>
- setIntegrationAccessTokenDialogOpen(false)}
- selectedIntegrationOption={selectedIntegrationOption}
- handleIntegrationOption={handleIntegrationOption}
- />
{
+ if (workspace) {
+ setSelectedSourceEnvironment(workspace.environments[0].slug);
+ }
+ }, [workspace]);
+
+ const handleButtonClick = async () => {
+ try {
+ if (vaultBaseUrl.length === 0) {
+ setVaultBaseUrlErrorText('Vault URI cannot be blank');
+ return;
+ }
+
+ if (
+ !vaultBaseUrl.startsWith('https://')
+ || !vaultBaseUrl.endsWith('vault.azure.net')
+ ) {
+ setVaultBaseUrlErrorText('Vault URI must be like https://.vault.azure.net');
+ return;
+ }
+
+ if (!integrationAuth?._id) return;
+
+ setIsLoading(true);
+ await createIntegration({
+ integrationAuthId: integrationAuth?._id,
+ isActive: true,
+ app: vaultBaseUrl,
+ appId: null,
+ sourceEnvironment: selectedSourceEnvironment,
+ targetEnvironment: null,
+ owner: null
+ });
+ setIsLoading(false);
+
+ router.push(
+ `/integrations/${localStorage.getItem('projectData.id')}`
+ );
+
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (integrationAuth && workspace && selectedSourceEnvironment) ? (
+
+
+ Azure Key Vault Integration
+
+
+
+
+ setVaultBaseUrl(e.target.value)}
+ />
+
+
+
+
+ ) :
+}
+
+AzureKeyVaultCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/azure-key-vault/oauth2/callback.tsx b/frontend/src/pages/integrations/azure-key-vault/oauth2/callback.tsx
new file mode 100644
index 000000000..14d92d9c2
--- /dev/null
+++ b/frontend/src/pages/integrations/azure-key-vault/oauth2/callback.tsx
@@ -0,0 +1,41 @@
+import { useEffect } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
+import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
+
+export default function AzureKeyVaultOAuth2CallbackPage() {
+ const router = useRouter();
+
+ const { code, state } = queryString.parse(router.asPath.split('?')[1]);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ // validate state
+ if (state !== localStorage.getItem('latestCSRFToken')) return;
+ localStorage.removeItem('latestCSRFToken');
+
+ const integrationAuth = await AuthorizeIntegration({
+ workspaceId: localStorage.getItem('projectData.id') as string,
+ code: code as string,
+ integration: 'azure-key-vault'
+ });
+
+ router.push(
+ `/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`
+ );
+
+ } catch (err) {
+ console.error(err);
+ }
+ })();
+ }, []);
+
+ return
+}
+
+AzureKeyVaultOAuth2CallbackPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/flyio/authorize.tsx b/frontend/src/pages/integrations/flyio/authorize.tsx
new file mode 100644
index 000000000..5b00e562a
--- /dev/null
+++ b/frontend/src/pages/integrations/flyio/authorize.tsx
@@ -0,0 +1,76 @@
+import { useState } from 'react';
+import { useRouter } from 'next/router';
+
+import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
+import {
+ Button,
+ Card,
+ CardTitle,
+ FormControl,
+ Input,
+} from '../../../components/v2';
+import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
+
+export default function FlyioCreateIntegrationPage() {
+ const router = useRouter();
+ const [accessToken, setAccessToken] = useState('');
+ const [accessTokenErrorText, setAccessTokenErrorText] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleButtonClick = async () => {
+ try {
+ setAccessTokenErrorText('');
+ if (accessToken.length === 0) {
+ setAccessTokenErrorText('Access token cannot be blank');
+ return;
+ }
+
+ setIsLoading(true);
+
+ const integrationAuth = await saveIntegrationAccessToken({
+ workspaceId: localStorage.getItem('projectData.id'),
+ integration: 'flyio',
+ accessToken
+ });
+
+ setIsLoading(false);
+
+ router.push(
+ `/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`
+ );
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (
+
+
+ Render Integration
+
+ setAccessToken(e.target.value)}
+ />
+
+
+
+
+ )
+}
+
+FlyioCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/flyio/create.tsx b/frontend/src/pages/integrations/flyio/create.tsx
new file mode 100644
index 000000000..7f9d079ae
--- /dev/null
+++ b/frontend/src/pages/integrations/flyio/create.tsx
@@ -0,0 +1,122 @@
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
+import {
+ Button,
+ Card,
+ CardTitle,
+ FormControl,
+ Select,
+ SelectItem
+} from '../../../components/v2';
+import { useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
+import { useGetWorkspaceById } from '../../../hooks/api/workspace';
+import createIntegration from "../../api/integrations/createIntegration";
+
+export default function FlyioCreateIntegrationPage() {
+ const router = useRouter();
+
+ const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
+
+ const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
+ const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
+ const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
+
+ const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
+ const [targetApp, setTargetApp] = useState('');
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ if (workspace) {
+ setSelectedSourceEnvironment(workspace.environments[0].slug);
+ }
+ }, [workspace]);
+
+ useEffect(() => {
+ // TODO: handle case where apps can be empty
+ if (integrationAuthApps) {
+ setTargetApp(integrationAuthApps[0].name);
+ }
+ }, [integrationAuthApps]);
+
+ const handleButtonClick = async () => {
+ try {
+ if (!integrationAuth?._id) return;
+
+ setIsLoading(true);
+
+ await createIntegration({
+ integrationAuthId: integrationAuth?._id,
+ isActive: true,
+ app: targetApp,
+ appId: null,
+ sourceEnvironment: selectedSourceEnvironment,
+ targetEnvironment: null,
+ owner: null
+ });
+
+ setIsLoading(false);
+
+ router.push(
+ `/integrations/${localStorage.getItem('projectData.id')}`
+ );
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
+
+
+ Fly.io Integration
+
+
+
+
+
+
+
+
+
+ ) :
+}
+
+FlyioCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/github/create.tsx b/frontend/src/pages/integrations/github/create.tsx
new file mode 100644
index 000000000..324ef5715
--- /dev/null
+++ b/frontend/src/pages/integrations/github/create.tsx
@@ -0,0 +1,123 @@
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
+import {
+ Button,
+ Card,
+ CardTitle,
+ FormControl,
+ Select,
+ SelectItem
+} from '../../../components/v2';
+import { useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
+import { useGetWorkspaceById } from '../../../hooks/api/workspace';
+import createIntegration from "../../api/integrations/createIntegration";
+
+export default function GitHubCreateIntegrationPage() {
+ const router = useRouter();
+
+ const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
+
+ const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
+ const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
+ const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
+
+ const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
+ const [owner, setOwner] = useState(null);
+ const [targetApp, setTargetApp] = useState('');
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ if (workspace) {
+ setSelectedSourceEnvironment(workspace.environments[0].slug);
+ }
+ }, [workspace]);
+
+ useEffect(() => {
+ // TODO: handle case where apps can be empty
+ if (integrationAuthApps) {
+ setTargetApp(integrationAuthApps[0].name);
+ setOwner(integrationAuthApps[0]?.owner ?? null);
+ }
+ }, [integrationAuthApps]);
+
+ const handleButtonClick = async () => {
+ try {
+ setIsLoading(true);
+
+ if (!integrationAuth?._id) return;
+
+ await createIntegration({
+ integrationAuthId: integrationAuth?._id,
+ isActive: true,
+ app: targetApp,
+ appId: null,
+ sourceEnvironment: selectedSourceEnvironment,
+ targetEnvironment: null,
+ owner
+ });
+
+ setIsLoading(false);
+ router.push(
+ `/integrations/${localStorage.getItem('projectData.id')}`
+ );
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
+
+
+ GitHub Integration
+
+
+
+
+
+
+
+
+
+ ) :
+}
+
+GitHubCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/github/oauth2/callback.tsx b/frontend/src/pages/integrations/github/oauth2/callback.tsx
new file mode 100644
index 000000000..ab2bb18c8
--- /dev/null
+++ b/frontend/src/pages/integrations/github/oauth2/callback.tsx
@@ -0,0 +1,41 @@
+import { useEffect } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
+import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
+
+export default function GitHubOAuth2CallbackPage() {
+ const router = useRouter();
+
+ const { code, state } = queryString.parse(router.asPath.split('?')[1]);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ // validate state
+ if (state !== localStorage.getItem('latestCSRFToken')) return;
+ localStorage.removeItem('latestCSRFToken');
+
+ const integrationAuth = await AuthorizeIntegration({
+ workspaceId: localStorage.getItem('projectData.id') as string,
+ code: code as string,
+ integration: 'github'
+ });
+
+ router.push(
+ `/integrations/github/create?integrationAuthId=${integrationAuth._id}`
+ );
+
+ } catch (err) {
+ console.error(err);
+ }
+ })();
+ }, []);
+
+ return
+}
+
+GitHubOAuth2CallbackPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/heroku/create.tsx b/frontend/src/pages/integrations/heroku/create.tsx
new file mode 100644
index 000000000..6d3c805d5
--- /dev/null
+++ b/frontend/src/pages/integrations/heroku/create.tsx
@@ -0,0 +1,121 @@
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
+import {
+ Button,
+ Card,
+ CardTitle,
+ FormControl,
+ Select,
+ SelectItem
+} from '../../../components/v2';
+import { useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
+import { useGetWorkspaceById } from '../../../hooks/api/workspace';
+import createIntegration from "../../api/integrations/createIntegration";
+
+export default function HerokuCreateIntegrationPage() {
+ const router = useRouter();
+
+ const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
+
+ const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
+ const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
+ const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
+
+ const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
+ const [targetApp, setTargetApp] = useState('');
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ if (workspace) {
+ setSelectedSourceEnvironment(workspace.environments[0].slug);
+ }
+ }, [workspace]);
+
+ useEffect(() => {
+ // TODO: handle case where apps can be empty
+ if (integrationAuthApps) {
+ setTargetApp(integrationAuthApps[0].name);
+ }
+ }, [integrationAuthApps]);
+
+ const handleButtonClick = async () => {
+ try {
+ setIsLoading(true);
+
+ if (!integrationAuth?._id) return;
+
+ await createIntegration({
+ integrationAuthId: integrationAuth?._id,
+ isActive: true,
+ app: targetApp,
+ appId: null,
+ sourceEnvironment: selectedSourceEnvironment,
+ targetEnvironment: null,
+ owner: null
+ });
+
+ setIsLoading(false);
+ router.push(
+ `/integrations/${localStorage.getItem('projectData.id')}`
+ );
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
+
+
+ Heroku Integration
+
+
+
+
+
+
+
+
+
+ ) :
+}
+
+HerokuCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/heroku/oauth2/callback.tsx b/frontend/src/pages/integrations/heroku/oauth2/callback.tsx
new file mode 100644
index 000000000..c5846170b
--- /dev/null
+++ b/frontend/src/pages/integrations/heroku/oauth2/callback.tsx
@@ -0,0 +1,44 @@
+import { useEffect } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
+import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
+
+export default function HerokuOAuth2CallbackPage() {
+ const router = useRouter();
+
+ const { code, state } = queryString.parse(router.asPath.split('?')[1]);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ // validate state
+ console.log('A');
+ if (state !== localStorage.getItem('latestCSRFToken')) return;
+ localStorage.removeItem('latestCSRFToken');
+ console.log('B');
+ const integrationAuth = await AuthorizeIntegration({
+ workspaceId: localStorage.getItem('projectData.id') as string,
+ code: code as string,
+ integration: 'heroku'
+ });
+
+ console.log('C');
+ router.push(
+ `/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`
+ );
+
+ } catch (err) {
+ console.error(err);
+ }
+ console.log('D');
+ })();
+ }, []);
+
+ return
+}
+
+HerokuOAuth2CallbackPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/netlify/create.tsx b/frontend/src/pages/integrations/netlify/create.tsx
new file mode 100644
index 000000000..6ec6469ab
--- /dev/null
+++ b/frontend/src/pages/integrations/netlify/create.tsx
@@ -0,0 +1,144 @@
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
+import {
+ Button,
+ Card,
+ CardTitle,
+ FormControl,
+ Select,
+ SelectItem
+} from '../../../components/v2';
+import { useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
+import { useGetWorkspaceById } from '../../../hooks/api/workspace';
+import createIntegration from "../../api/integrations/createIntegration";
+
+const netlifyEnvironments = [
+ { name: 'Local development', slug: 'dev' },
+ { name: 'Branch deploys', slug: 'branch-deploy' },
+ { name: 'Deploy previews', slug: 'deploy-preview' },
+ { name: 'Production', slug: 'production' }
+]
+
+export default function NetlifyCreateIntegrationPage() {
+ const router = useRouter();
+
+ const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
+
+ const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
+ const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
+ const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
+
+ const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
+ const [targetApp, setTargetApp] = useState('');
+ const [targetEnvironment, setTargetEnvironment] = useState('');
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ if (workspace) {
+ setSelectedSourceEnvironment(workspace.environments[0].slug);
+ setTargetEnvironment(netlifyEnvironments[0].slug);
+ }
+ }, [workspace]);
+
+ useEffect(() => {
+ // TODO: handle case where apps can be empty
+ if (integrationAuthApps) {
+ console.log(integrationAuthApps)
+ setTargetApp(integrationAuthApps[0].name);
+ }
+ }, [integrationAuthApps]);
+
+ const handleButtonClick = async () => {
+ try {
+ setIsLoading(true);
+
+ if (!integrationAuth?._id) return;
+
+ await createIntegration({
+ integrationAuthId: integrationAuth?._id,
+ isActive: true,
+ app: targetApp,
+ appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null,
+ sourceEnvironment: selectedSourceEnvironment,
+ targetEnvironment,
+ owner: null
+ });
+
+ setIsLoading(false);
+ router.push(
+ `/integrations/${localStorage.getItem('projectData.id')}`
+ );
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp && targetEnvironment) ? (
+
+
+ Netlify Integration
+
+
+
+
+
+
+
+
+
+
+
+
+ ) :
+}
+
+NetlifyCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/netlify/oauth2/callback.tsx b/frontend/src/pages/integrations/netlify/oauth2/callback.tsx
new file mode 100644
index 000000000..db884282a
--- /dev/null
+++ b/frontend/src/pages/integrations/netlify/oauth2/callback.tsx
@@ -0,0 +1,41 @@
+import { useEffect } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
+import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
+
+export default function NetlifyOAuth2CallbackPage() {
+ const router = useRouter();
+
+ const { code, state } = queryString.parse(router.asPath.split('?')[1]);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ // validate state
+ if (state !== localStorage.getItem('latestCSRFToken')) return;
+ localStorage.removeItem('latestCSRFToken');
+
+ const integrationAuth = await AuthorizeIntegration({
+ workspaceId: localStorage.getItem('projectData.id') as string,
+ code: code as string,
+ integration: 'netlify'
+ });
+
+ router.push(
+ `/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`
+ );
+
+ } catch (err) {
+ console.error(err);
+ }
+ })();
+ }, []);
+
+ return
+}
+
+NetlifyOAuth2CallbackPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/render/authorize.tsx b/frontend/src/pages/integrations/render/authorize.tsx
new file mode 100644
index 000000000..bd7a877c3
--- /dev/null
+++ b/frontend/src/pages/integrations/render/authorize.tsx
@@ -0,0 +1,76 @@
+import { useState } from 'react';
+import { useRouter } from 'next/router';
+
+import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
+import {
+ Button,
+ Card,
+ CardTitle,
+ FormControl,
+ Input,
+} from '../../../components/v2';
+import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
+
+export default function RenderCreateIntegrationPage() {
+ const router = useRouter();
+ const [apiKey, setApiKey] = useState('');
+ const [apiKeyErrorText, setApiKeyErrorText] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleButtonClick = async () => {
+ try {
+ setApiKeyErrorText('');
+ if (apiKey.length === 0) {
+ setApiKeyErrorText('API Key cannot be blank');
+ return;
+ }
+
+ setIsLoading(true);
+
+ const integrationAuth = await saveIntegrationAccessToken({
+ workspaceId: localStorage.getItem('projectData.id'),
+ integration: 'render',
+ accessToken: apiKey
+ });
+
+ setIsLoading(false);
+
+ router.push(
+ `/integrations/render/create?integrationAuthId=${integrationAuth._id}`
+ );
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (
+
+
+ Render Integration
+
+ setApiKey(e.target.value)}
+ />
+
+
+
+
+ )
+}
+
+RenderCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/render/create.tsx b/frontend/src/pages/integrations/render/create.tsx
new file mode 100644
index 000000000..2f02408d9
--- /dev/null
+++ b/frontend/src/pages/integrations/render/create.tsx
@@ -0,0 +1,122 @@
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
+import {
+ Button,
+ Card,
+ CardTitle,
+ FormControl,
+ Select,
+ SelectItem
+} from '../../../components/v2';
+import { useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
+import { useGetWorkspaceById } from '../../../hooks/api/workspace';
+import createIntegration from "../../api/integrations/createIntegration";
+
+export default function RenderCreateIntegrationPage() {
+ const router = useRouter();
+
+ const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
+
+ const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
+ const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
+ const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
+
+ const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
+ const [targetApp, setTargetApp] = useState('');
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ if (workspace) {
+ setSelectedSourceEnvironment(workspace.environments[0].slug);
+ }
+ }, [workspace]);
+
+ useEffect(() => {
+ // TODO: handle case where apps can be empty
+ if (integrationAuthApps) {
+ setTargetApp(integrationAuthApps[0].name);
+ }
+ }, [integrationAuthApps]);
+
+ const handleButtonClick = async () => {
+ try {
+ if (!integrationAuth?._id) return;
+
+ setIsLoading(true);
+
+ await createIntegration({
+ integrationAuthId: integrationAuth?._id,
+ isActive: true,
+ app: targetApp,
+ appId: null,
+ sourceEnvironment: selectedSourceEnvironment,
+ targetEnvironment: null,
+ owner: null
+ });
+
+ setIsLoading(false);
+
+ router.push(
+ `/integrations/${localStorage.getItem('projectData.id')}`
+ );
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
+
+
+ Render Integration
+
+
+
+
+
+
+
+
+
+ ) :
+}
+
+RenderCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/vercel/create.tsx b/frontend/src/pages/integrations/vercel/create.tsx
new file mode 100644
index 000000000..5a3249378
--- /dev/null
+++ b/frontend/src/pages/integrations/vercel/create.tsx
@@ -0,0 +1,142 @@
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
+import {
+ Button,
+ Card,
+ CardTitle,
+ FormControl,
+ Select,
+ SelectItem
+} from '../../../components/v2';
+import { useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
+import { useGetWorkspaceById } from '../../../hooks/api/workspace';
+import createIntegration from "../../api/integrations/createIntegration";
+
+const vercelEnvironments = [
+ { name: 'Development', slug: 'development' },
+ { name: 'Preview', slug: 'preview' },
+ { name: 'Production', slug: 'production' }
+]
+
+export default function VercelCreateIntegrationPage() {
+ const router = useRouter();
+
+ const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
+
+ const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
+ const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
+ const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
+
+ const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
+ const [targetApp, setTargetApp] = useState('');
+ const [targetEnvironment, setTargetEnvironemnt] = useState('');
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ if (workspace) {
+ setSelectedSourceEnvironment(workspace.environments[0].slug);
+ }
+ }, [workspace]);
+
+ useEffect(() => {
+ // TODO: handle case where apps can be empty
+ if (integrationAuthApps) {
+ setTargetApp(integrationAuthApps[0].name);
+ setTargetEnvironemnt(vercelEnvironments[0].slug);
+ }
+ }, [integrationAuthApps]);
+
+ const handleButtonClick = async () => {
+ try {
+ if (!integrationAuth?._id) return;
+
+ setIsLoading(true);
+ await createIntegration({
+ integrationAuthId: integrationAuth?._id,
+ isActive: true,
+ app: targetApp,
+ appId: null,
+ sourceEnvironment: selectedSourceEnvironment,
+ targetEnvironment,
+ owner: null
+ });
+
+ setIsLoading(false);
+ router.push(
+ `/integrations/${localStorage.getItem('projectData.id')}`
+ );
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp && targetEnvironment) ? (
+
+
+ Vercel Integration
+
+
+
+
+
+
+
+
+
+
+
+
+ ) :
+}
+
+VercelCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/vercel/oauth2/callback.tsx b/frontend/src/pages/integrations/vercel/oauth2/callback.tsx
new file mode 100644
index 000000000..1acf01b02
--- /dev/null
+++ b/frontend/src/pages/integrations/vercel/oauth2/callback.tsx
@@ -0,0 +1,41 @@
+import { useEffect } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
+import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
+
+export default function VercelOAuth2CallbackPage() {
+ const router = useRouter();
+
+ const { code, state } = queryString.parse(router.asPath.split('?')[1]);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ // validate state
+ if (state !== localStorage.getItem('latestCSRFToken')) return;
+ localStorage.removeItem('latestCSRFToken');
+
+ const integrationAuth = await AuthorizeIntegration({
+ workspaceId: localStorage.getItem('projectData.id') as string,
+ code: code as string,
+ integration: 'vercel'
+ });
+
+ router.push(
+ `/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`
+ );
+
+ } catch (err) {
+ console.error(err);
+ }
+ })();
+ }, []);
+
+ return
+}
+
+VercelOAuth2CallbackPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file