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 ( -
- - { - closeModal(); - }}> - -
- -
-
- - - - {`Enter your ${selectedIntegrationOption?.name} API Key`} - -
-

- {`This integration requires you to obtain an API key from ${selectedIntegrationOption?.name ?? ''} and store it with Infisical. `} - You can learn how to do this here. -

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