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