diff --git a/backend/package-lock.json b/backend/package-lock.json
index 6703e198f9..189b1eccd6 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -10,13 +10,13 @@
"license": "ISC",
"dependencies": {
"@godaddy/terminus": "^4.11.2",
- "@sentry/node": "^7.21.1",
"@octokit/rest": "^19.0.5",
- "@sentry/tracing": "^7.21.1",
+ "@sentry/node": "^7.14.0",
+ "@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1",
- "axios": "^1.2.0",
"@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0",
+ "axios": "^1.1.3",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2",
"builder-pattern": "^2.2.0",
@@ -32,9 +32,9 @@
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
- "mongoose": "^6.7.3",
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
+ "mongoose": "^6.7.2",
"nodemailer": "^6.8.0",
"posthog-node": "^2.2.2",
"query-string": "^7.1.3",
@@ -2838,19 +2838,6 @@
"@maxmind/geoip2-node": "^3.4.0"
}
},
- "node_modules/@sentry/core": {
- "version": "7.21.1",
- "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.21.1.tgz",
- "integrity": "sha512-Og5wEEsy24fNvT/T7IKjcV4EvVK5ryY2kxbJzKY6GU2eX+i+aBl+n/vp7U0Es351C/AlTkS+0NOUsp2TQQFxZA==",
- "dependencies": {
- "@sentry/types": "7.21.1",
- "@sentry/utils": "7.21.1",
- "tslib": "^1.9.3"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -2905,27 +2892,10 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
- "node_modules/@sentry/node": {
- "version": "7.19.0",
- "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.19.0.tgz",
- "integrity": "sha512-yG7Tx32WqOkEHVotFLrumCcT9qlaSDTkFNZ+yLSvZXx74ifsE781DzBA9W7K7bBdYO3op+p2YdsOKzf3nPpAyQ==",
- "dependencies": {
- "@sentry/core": "7.19.0",
- "@sentry/types": "7.19.0",
- "@sentry/utils": "7.19.0",
- "cookie": "^0.4.1",
- "https-proxy-agent": "^5.0.0",
- "lru_map": "^0.3.3",
- "tslib": "^1.9.3"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@sentry/node/node_modules/@sentry/core": {
- "version": "7.19.0",
- "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.19.0.tgz",
- "integrity": "sha512-YF9cTBcAnO4R44092BJi5Wa2/EO02xn2ziCtmNgAVTN2LD31a/YVGxGBt/FDr4Y6yeuVehaqijVVvtpSmXrGJw==",
+ "node_modules/@sentry/core": {
+ "version": "7.21.1",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.21.1.tgz",
+ "integrity": "sha512-Og5wEEsy24fNvT/T7IKjcV4EvVK5ryY2kxbJzKY6GU2eX+i+aBl+n/vp7U0Es351C/AlTkS+0NOUsp2TQQFxZA==",
"dependencies": {
"@sentry/types": "7.21.1",
"@sentry/utils": "7.21.1",
@@ -2986,26 +2956,6 @@
"node": ">=8"
}
},
- "node_modules/@sentry/types": {
- "version": "7.21.1",
- "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.21.1.tgz",
- "integrity": "sha512-3/IKnd52Ol21amQvI+kz+WB76s8/LR5YvFJzMgIoI2S8d82smIr253zGijRXxHPEif8kMLX4Yt+36VzrLxg6+A==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@sentry/utils": {
- "version": "7.21.1",
- "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.21.1.tgz",
- "integrity": "sha512-F0W0AAi8tgtTx6ApZRI2S9HbXEA9ENX1phTZgdNNWcMFm1BNbc21XEwLqwXBNjub5nlA6CE8xnjXRgdZKx4kzQ==",
- "dependencies": {
- "@sentry/types": "7.21.1",
- "tslib": "^1.9.3"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/@sinclair/typebox": {
"version": "0.24.51",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",
@@ -14306,16 +14256,6 @@
"@maxmind/geoip2-node": "^3.4.0"
}
},
- "@sentry/core": {
- "version": "7.21.1",
- "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.21.1.tgz",
- "integrity": "sha512-Og5wEEsy24fNvT/T7IKjcV4EvVK5ryY2kxbJzKY6GU2eX+i+aBl+n/vp7U0Es351C/AlTkS+0NOUsp2TQQFxZA==",
- "requires": {
- "@sentry/types": "7.21.1",
- "@sentry/utils": "7.21.1",
- "tslib": "^1.9.3"
- }
- },
"@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -14370,6 +14310,16 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
+ "@sentry/core": {
+ "version": "7.21.1",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.21.1.tgz",
+ "integrity": "sha512-Og5wEEsy24fNvT/T7IKjcV4EvVK5ryY2kxbJzKY6GU2eX+i+aBl+n/vp7U0Es351C/AlTkS+0NOUsp2TQQFxZA==",
+ "requires": {
+ "@sentry/types": "7.21.1",
+ "@sentry/utils": "7.21.1",
+ "tslib": "^1.9.3"
+ }
+ },
"@sentry/node": {
"version": "7.21.1",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.21.1.tgz",
@@ -14409,20 +14359,6 @@
"tslib": "^1.9.3"
}
},
- "@sentry/types": {
- "version": "7.21.1",
- "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.21.1.tgz",
- "integrity": "sha512-3/IKnd52Ol21amQvI+kz+WB76s8/LR5YvFJzMgIoI2S8d82smIr253zGijRXxHPEif8kMLX4Yt+36VzrLxg6+A=="
- },
- "@sentry/utils": {
- "version": "7.21.1",
- "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.21.1.tgz",
- "integrity": "sha512-F0W0AAi8tgtTx6ApZRI2S9HbXEA9ENX1phTZgdNNWcMFm1BNbc21XEwLqwXBNjub5nlA6CE8xnjXRgdZKx4kzQ==",
- "requires": {
- "@sentry/types": "7.21.1",
- "tslib": "^1.9.3"
- }
- },
"@sinclair/typebox": {
"version": "0.24.51",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",
diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts
index cedc7e3453..be42647deb 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');
@@ -40,14 +64,16 @@ export const oAuthExchange = async (
throw new Error("Failed to get environments")
}
- const integrationDetails = await IntegrationService.handleOAuthExchange({
+ const integrationAuth = await IntegrationService.handleOAuthExchange({
workspaceId,
integration,
code,
environment: environments[0].slug,
});
- return res.status(200).send(integrationDetails);
+ return res.status(200).send({
+ integrationAuth
+ });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
@@ -79,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
@@ -89,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 2b52f97252..779cb3cb72 100644
--- a/backend/src/controllers/v1/integrationController.ts
+++ b/backend/src/controllers/v1/integrationController.ts
@@ -1,4 +1,5 @@
import { Request, Response } from 'express';
+import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import {
Integration,
@@ -16,20 +17,42 @@ import { eventPushSecrets } from '../../events';
* @returns
*/
export const createIntegration = async (req: Request, res: Response) => {
-
- // TODO: make this more versatile
-
let integration;
try {
+ const {
+ integrationAuthId,
+ 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,
- isActive: false,
- app: null,
- environment: req.integrationAuth.workspace?.environments[0].slug,
+ environment: sourceEnvironment,
+ isActive,
+ app,
+ appId,
+ targetEnvironment,
+ owner,
integration: req.integrationAuth.integration,
- integrationAuth: req.integrationAuth._id
+ integrationAuth: new Types.ObjectId(integrationAuthId)
}).save();
+
+ if (integration) {
+ // trigger event - push secrets
+ EventService.handleEvent({
+ event: eventPushSecrets({
+ workspaceId: integration.workspace.toString()
+ })
+ });
+ }
+
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts
index 595dbbb6f4..17338ee688 100644
--- a/backend/src/helpers/integration.ts
+++ b/backend/src/helpers/integration.ts
@@ -30,6 +30,7 @@ interface Update {
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.integration - name of integration
* @param {String} obj.code - code
+ * @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
*/
const handleOAuthExchangeHelper = async ({
workspaceId,
@@ -42,9 +43,7 @@ const handleOAuthExchangeHelper = async ({
code: string;
environment: string;
}) => {
- let action;
let integrationAuth;
- let newIntegration;
try {
const bot = await Bot.findOne({
workspace: workspaceId,
@@ -99,26 +98,13 @@ 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);
throw new Error('Failed to handle OAuth2 code-token exchange')
}
- return ({
- integrationAuth,
- integration: newIntegration
- });
+ return integrationAuth;
}
/**
* Sync/push environment variables in workspace with id [workspaceId] to
diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts
index ada2b76bde..846c9e7db9 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 4604d744a2..401c7f9a1a 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 d587bd2e5c..90cea3a387 100644
--- a/backend/src/routes/v1/integration.ts
+++ b/backend/src/routes/v1/integration.ts
@@ -10,7 +10,7 @@ import { ADMIN, MEMBER } from '../../variables';
import { body, param } from 'express-validator';
import { integrationController } from '../../controllers/v1';
-router.post( // new: add new integration
+router.post( // new: add new integration for integration auth
'/',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
@@ -19,7 +19,13 @@ router.post( // new: add new integration
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
}),
- body('integrationAuthId').exists().trim(),
+ body('integrationAuthId').exists().isString().trim(),
+ body('app').isString().trim(),
+ body('isActive').exists().isBoolean(),
+ body('appId').trim(),
+ body('sourceEnvironment').trim(),
+ body('targetEnvironment').trim(),
+ body('owner').trim(),
validateRequest,
integrationController.createIntegration
);
diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts
index 3f88aa7e5d..1918140c2b 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/backend/src/services/IntegrationService.ts b/backend/src/services/IntegrationService.ts
index cb452f8c88..5b01954277 100644
--- a/backend/src/services/IntegrationService.ts
+++ b/backend/src/services/IntegrationService.ts
@@ -26,10 +26,7 @@ class IntegrationService {
* @param {String} obj1.environment - workspace environment
* @param {String} obj1.integration - name of integration
* @param {String} obj1.code - code
- * @returns {Object} obj2
- * @returns {IntegrationAuth} obj2.integrationAuth - integration authorization after OAuth2 code-token exchange
- * @returns {Integration} obj2.integration - newly-initialized integration OAuth2 code-token exchange
- * @retrun
+ * @returns {IntegrationAuth} integrationAuth - integration authorization after OAuth2 code-token exchange
*/
static async handleOAuthExchange({
workspaceId,
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 7f52e04aeb..b0317ff7c0 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 db8153a0a9..4e2e3e585a 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 6ac74cdbdc..f25da26d39 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 1f1ee246df..c6e2340588 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 0a09f21e9d..0000000000
--- 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 0000000000..43920b65fa
--- /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 0000000000..508afc7417
--- /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 0000000000..bf193eafe2
--- /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/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx
index 5de95ab059..67d8b9dea3 100644
--- a/frontend/src/hooks/api/workspace/index.tsx
+++ b/frontend/src/hooks/api/workspace/index.tsx
@@ -3,6 +3,7 @@ export {
useDeleteWorkspace,
useDeleteWsEnvironment,
useGetUserWorkspaces,
+ useGetWorkspaceById,
useRenameWorkspace,
useUpdateWsEnvironment
} from './queries';
diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx
index b0566a43d8..efdf4a6a3d 100644
--- a/frontend/src/hooks/api/workspace/queries.tsx
+++ b/frontend/src/hooks/api/workspace/queries.tsx
@@ -11,16 +11,30 @@ import {
Workspace
} from './types';
+
const workspaceKeys = {
+ getWorkspaceById: (workspaceId: string) => [{ workspaceId }, 'workspace'] as const,
getAllUserWorkspace: ['workspaces'] as const
};
+const fetchWorkspaceById = async (workspaceId: string) => {
+ const { data } = await apiRequest.get<{ workspace: Workspace }>(`/api/v1/workspace/${workspaceId}`);
+ return data.workspace;
+}
+
const fetchUserWorkspaces = async () => {
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>('/api/v1/workspace');
-
return data.workspaces;
};
+export const useGetWorkspaceById = (workspaceId: string) => {
+ return useQuery({
+ queryKey: workspaceKeys.getWorkspaceById(workspaceId),
+ queryFn: () => fetchWorkspaceById(workspaceId),
+ enabled: true
+ });
+};
+
export const useGetUserWorkspaces = () =>
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
diff --git a/frontend/src/pages/api/integrations/authorizeIntegration.ts b/frontend/src/pages/api/integrations/authorizeIntegration.ts
index 666499be6d..9c70796b70 100644
--- a/frontend/src/pages/api/integrations/authorizeIntegration.ts
+++ b/frontend/src/pages/api/integrations/authorizeIntegration.ts
@@ -26,7 +26,7 @@ const AuthorizeIntegration = ({ workspaceId, code, integration }: Props) =>
})
}).then(async (res) => {
if (res && res.status === 200) {
- return (res.json());
+ return (await res.json()).integrationAuth;
}
console.log('Failed to authorize the integration');
return undefined;
diff --git a/frontend/src/pages/api/integrations/createIntegration.ts b/frontend/src/pages/api/integrations/createIntegration.ts
index 6b37f1281f..6b83d3fdc1 100644
--- a/frontend/src/pages/api/integrations/createIntegration.ts
+++ b/frontend/src/pages/api/integrations/createIntegration.ts
@@ -1,7 +1,13 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props {
- integrationAuthId: string;
+ 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]
@@ -10,7 +16,13 @@ interface Props {
* @returns
*/
const createIntegration = ({
- integrationAuthId
+ integrationAuthId,
+ isActive,
+ app,
+ appId,
+ sourceEnvironment,
+ targetEnvironment,
+ owner
}: Props) =>
SecurityClient.fetchCall('/api/v1/integration', {
method: 'POST',
@@ -18,7 +30,13 @@ const createIntegration = ({
'Content-Type': 'application/json'
},
body: JSON.stringify({
- integrationAuthId
+ integrationAuthId,
+ isActive,
+ 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 57f8613870..0000000000
--- a/frontend/src/pages/azure-key-vault.tsx
+++ /dev/null
@@ -1,166 +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 AuthorizeIntegration from './api/integrations/authorizeIntegration';
-import updateIntegration from './api/integrations/updateIntegration';
-import getAWorkspace from './api/workspace/getAWorkspace';
-
-interface Integration {
- _id: string;
- isActive: boolean;
- app: string | null;
- appId: string | null;
- createdAt: string;
- updatedAt: string;
- environment: string;
- integration: string;
- targetEnvironment: string;
- workspace: string;
- integrationAuth: string;
-}
-
-export default function AzureKeyVault() {
- const router = useRouter();
-
- // query-string variables
- const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
- const {code} = parsedUrl;
- const {state} = parsedUrl;
-
- const [integration, setIntegration] = useState(null);
- const [environments, setEnvironments] = useState<
- {
- name: string;
- slug: string;
- }[]
- >([]);
- const [environment, setEnvironment] = useState('');
- const [vaultBaseUrl, setVaultBaseUrl] = useState('');
- const [vaultBaseUrlErrorText, setVaultBaseUrlErrorText] = useState('');
- const [isLoading, setIsLoading] = useState(false);
-
- useEffect(() => {
- (async () => {
- try {
- if (state === localStorage.getItem('latestCSRFToken')) {
- localStorage.removeItem('latestCSRFToken');
-
- const integrationDetails = await AuthorizeIntegration({
- workspaceId: localStorage.getItem('projectData.id') as string,
- code: code as string,
- integration: 'azure-key-vault',
- });
-
- setIntegration(integrationDetails.integration);
-
- const workspaceId = localStorage.getItem('projectData.id');
- if (!workspaceId) return;
-
- const workspace = await getAWorkspace(workspaceId);
- setEnvironment(workspace.environments[0].slug);
- setEnvironments(workspace.environments);
-
- }
- } catch (error) {
- console.error('Azure Key Vault integration error: ', error);
- }
- })();
- }, []);
-
- 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 (!integration) return;
-
- setIsLoading(true);
- await updateIntegration({
- integrationId: integration._id,
- isActive: true,
- environment,
- app: vaultBaseUrl,
- appId: null,
- targetEnvironment: null,
- owner: null
- });
- setIsLoading(false);
-
- router.push(
- `/integrations/${localStorage.getItem('projectData.id')}`
- );
-
- } catch (err) {
- console.error(err);
- }
- }
-
- return (integration && environments.length > 0) ? (
-
-
- 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/github.tsx b/frontend/src/pages/github.tsx
deleted file mode 100644
index 37c35b1946..0000000000
--- a/frontend/src/pages/github.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { useEffect } from 'react';
-import { useRouter } from 'next/router';
-import queryString from 'query-string';
-
-import AuthorizeIntegration from './api/integrations/authorizeIntegration';
-
-export default function Github() {
- const router = useRouter();
- const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
- const {code} = parsedUrl;
- const {state} = parsedUrl;
-
- /**
- * 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') as string,
- code: code as string,
- integration: 'github',
- });
- router.push(
- `/integrations/${localStorage.getItem('projectData.id')}`
- );
- }
- } catch (error) {
- console.error('Github integration error: ', error);
- }
- })();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- return ;
-}
-
-Github.requireAuth = true;
diff --git a/frontend/src/pages/heroku.tsx b/frontend/src/pages/heroku.tsx
deleted file mode 100644
index ff4f2263e0..0000000000
--- a/frontend/src/pages/heroku.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { useEffect } from 'react';
-import { useRouter } from 'next/router';
-import queryString from 'query-string';
-
-import AuthorizeIntegration from './api/integrations/authorizeIntegration';
-
-export default function Heroku() {
- const router = useRouter();
- const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
- const {code} = parsedUrl;
- const {state} = parsedUrl;
-
- /**
- * 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') as string,
- code: code as string,
- integration: 'heroku',
- });
- router.push(
- `/integrations/${ localStorage.getItem('projectData.id')}`
- );
- }
- } catch (error) {
- console.error('Heroku integration error: ', error);
- }
- })();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- return ;
-}
-
-Heroku.requireAuth = true;
diff --git a/frontend/src/pages/integrations/[id].tsx b/frontend/src/pages/integrations/[id].tsx
index 688d7c34fc..e6bc784f67 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';
@@ -52,6 +49,7 @@ interface Integration {
}
interface IntegrationOption {
+ tenantId?: string;
clientId: string;
clientSlug?: string; // vercel-integration specific
docsLink: string;
@@ -75,7 +73,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,85 +163,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();
- }
-
- 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);
- // 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 ?? ''
- });
+ 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;
+ }
- setIntegrationAuths([...integrationAuths, integrationAuth])
+ 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;
+ }
- const integration = await createIntegration({
- integrationAuthId: integrationAuth._id
- });
-
- setIntegrations([...integrations, integration]);
- return;
+ if (link !== '') {
+ window.location.assign(link);
}
} catch (err) {
console.error(err);
}
- };
+ }
/**
* Open dialog to activate bot if bot is not active.
@@ -256,39 +251,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);
}
@@ -370,12 +346,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 0000000000..14d92d9c23
--- /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 0000000000..5b00e562a7
--- /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 0000000000..2e185d9fe1
--- /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 { useGetIntegrationAuthApps,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 0000000000..3b39fa2d15
--- /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 { useGetIntegrationAuthApps,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 0000000000..ab2bb18c8f
--- /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 0000000000..46e3794627
--- /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 { useGetIntegrationAuthApps,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 0000000000..321255c9f2
--- /dev/null
+++ b/frontend/src/pages/integrations/heroku/oauth2/callback.tsx
@@ -0,0 +1,40 @@
+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
+ 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: 'heroku'
+ });
+
+ router.push(
+ `/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`
+ );
+
+ } catch (err) {
+ console.error(err);
+ }
+ })();
+ }, []);
+
+ 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 0000000000..a8b6bcd05e
--- /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 { useGetIntegrationAuthApps,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 0000000000..db884282ad
--- /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 0000000000..bd7a877c39
--- /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 0000000000..3429def894
--- /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 { useGetIntegrationAuthApps,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 0000000000..5e0e8ad7e3
--- /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 { useGetIntegrationAuthApps,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 0000000000..1acf01b02f
--- /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
diff --git a/frontend/src/pages/netlify.tsx b/frontend/src/pages/netlify.tsx
deleted file mode 100644
index 532d6044ac..0000000000
--- a/frontend/src/pages/netlify.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { useEffect } from 'react';
-import { useRouter } from 'next/router';
-import queryString from '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;
- const {state} = parsedUrl;
- // 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 (!code) throw new Error('Code not found');
-
- if (state === localStorage.getItem('latestCSRFToken')) {
- localStorage.removeItem('latestCSRFToken');
-
- await AuthorizeIntegration({
- workspaceId: localStorage.getItem('projectData.id') as string,
- code: code as string,
- 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 ;
-}
-
-Netlify.requireAuth = true;
diff --git a/frontend/src/pages/vercel.tsx b/frontend/src/pages/vercel.tsx
deleted file mode 100644
index 8e82457367..0000000000
--- a/frontend/src/pages/vercel.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { useEffect } from 'react';
-import { useRouter } from 'next/router';
-import queryString from '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;
- const {state} = parsedUrl;
-
- /**
- * Here we forward to the default workspace if a user opens this url
- */
- // eslint-disable-next-line react-hooks/exhaustive-deps
- useEffect(() => {
- (async () => {
- try {
- // type check
- if (!code) throw new Error('Code not found');
-
- if (state === localStorage.getItem('latestCSRFToken')) {
- localStorage.removeItem('latestCSRFToken');
-
- await AuthorizeIntegration({
- workspaceId: localStorage.getItem('projectData.id') as string,
- code: code as string,
- 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 ;
-}
-
-Vercel.requireAuth = true;