From 2f045be8a4a448f63173c998d195a4c874a065f0 Mon Sep 17 00:00:00 2001 From: Aashish-Upadhyay-101 Date: Tue, 28 Feb 2023 09:37:47 +0545 Subject: [PATCH] missing break statement --- .env.example | 2 + backend/environment.d.ts | 2 + backend/src/config/index.ts | 4 + backend/src/integrations/apps.ts | 65 ++++++++- backend/src/integrations/exchange.ts | 65 ++++++++- backend/src/integrations/revoke.ts | 5 +- backend/src/integrations/sync.ts | 103 ++++++++++++++- backend/src/models/integration.ts | 5 +- backend/src/models/integrationAuth.ts | 4 +- backend/src/variables/index.ts | 6 + backend/src/variables/integration.ts | 19 +++ frontend/public/data/frequentConstants.ts | 1 + .../public/images/integrations/GitLab.png | Bin 0 -> 17927 bytes frontend/src/pages/integrations/[id].tsx | 6 + .../src/pages/integrations/gitlab/create.tsx | 125 ++++++++++++++++++ .../integrations/gitlab/oauth2/callback.tsx | 39 ++++++ 16 files changed, 442 insertions(+), 9 deletions(-) create mode 100644 frontend/public/images/integrations/GitLab.png create mode 100644 frontend/src/pages/integrations/gitlab/create.tsx create mode 100644 frontend/src/pages/integrations/gitlab/oauth2/callback.tsx diff --git a/.env.example b/.env.example index e8f9b747b..c893713f4 100644 --- a/.env.example +++ b/.env.example @@ -48,10 +48,12 @@ CLIENT_ID_HEROKU= CLIENT_ID_VERCEL= CLIENT_ID_NETLIFY= CLIENT_ID_GITHUB= +CLIENT_ID_GITLAB= CLIENT_SECRET_HEROKU= CLIENT_SECRET_VERCEL= CLIENT_SECRET_NETLIFY= CLIENT_SECRET_GITHUB= +CLIENT_SECRET_GITLAB= CLIENT_SLUG_VERCEL= # Sentry (optional) for monitoring errors diff --git a/backend/environment.d.ts b/backend/environment.d.ts index 8012416f6..497902def 100644 --- a/backend/environment.d.ts +++ b/backend/environment.d.ts @@ -22,10 +22,12 @@ declare global { CLIENT_ID_VERCEL: string; CLIENT_ID_NETLIFY: string; CLIENT_ID_GITHUB: string; + CLIENT_ID_GITLAB: string; CLIENT_SECRET_HEROKU: string; CLIENT_SECRET_VERCEL: string; CLIENT_SECRET_NETLIFY: string; CLIENT_SECRET_GITHUB: string; + CLIENT_SECRET_GITLAB: string; CLIENT_SLUG_VERCEL: string; POSTHOG_HOST: string; POSTHOG_PROJECT_API_KEY: string; diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index c0c9988f0..898da805e 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -22,11 +22,13 @@ const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!; const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!; const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!; const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!; +const CLIENT_ID_GITLAB = process.env.CLIENT_ID_GITLAB!; const CLIENT_SECRET_AZURE = process.env.CLIENT_SECRET_AZURE!; const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!; const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!; const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!; const CLIENT_SECRET_GITHUB = process.env.CLIENT_SECRET_GITHUB!; +const CLIENT_SECRET_GITLAB = process.env.CLIENT_SECRET_GITLAB; const CLIENT_SLUG_VERCEL = process.env.CLIENT_SLUG_VERCEL!; const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com'; const POSTHOG_PROJECT_API_KEY = @@ -75,11 +77,13 @@ export { CLIENT_ID_VERCEL, CLIENT_ID_NETLIFY, CLIENT_ID_GITHUB, + CLIENT_ID_GITLAB, CLIENT_SECRET_AZURE, CLIENT_SECRET_HEROKU, CLIENT_SECRET_VERCEL, CLIENT_SECRET_NETLIFY, CLIENT_SECRET_GITHUB, + CLIENT_SECRET_GITLAB, CLIENT_SLUG_VERCEL, POSTHOG_HOST, POSTHOG_PROJECT_API_KEY, diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index f18734805..90889b623 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -10,11 +10,13 @@ import { INTEGRATION_VERCEL, INTEGRATION_NETLIFY, INTEGRATION_GITHUB, + INTEGRATION_GITLAB, INTEGRATION_RENDER, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, INTEGRATION_HEROKU_API_URL, + INTEGRATION_GITLAB_API_URL, INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, @@ -77,6 +79,11 @@ const getApps = async ({ accessToken, }); break; + case INTEGRATION_GITLAB: + apps = await getAppsGitlab({ + accessToken, + }); + break; case INTEGRATION_RENDER: apps = await getAppsRender({ accessToken, @@ -217,9 +224,9 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => { /** * Return list of repositories for Github integration * @param {Object} obj - * @param {String} obj.accessToken - access token for Netlify API - * @returns {Object[]} apps - names of Netlify sites - * @returns {String} apps.name - name of Netlify site + * @param {String} obj.accessToken - access token for Github API + * @returns {Object[]} apps - names of Github sites + * @returns {String} apps.name - name of Github site */ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => { let apps; @@ -406,4 +413,56 @@ const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => { return apps; } +/** + * Return list of repositories for GitLab integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for GitLab API + * @returns {Object[]} apps - names of GitLab sites + * @returns {String} apps.name - name of GitLab site + */ +const getAppsGitlab = async ({ accessToken }: {accessToken: string}) => { + let apps; + console.log(accessToken); + + try { + const { id } = ( + await axios.get( + `${INTEGRATION_GITLAB_API_URL}/v4/user`, + { + headers: { + "Authorization": `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ) + ).data; + + const res = ( + await axios.get( + `${INTEGRATION_GITLAB_API_URL}/v4/users/${id}/projects`, + { + headers: { + "Authorization": `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ) + ).data; + + apps = res?.map((a: any) => { + return { + name: a?.name, + appId: `${a?.id}`, + } + }); + }catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error("Failed to get GitLab repos"); + } + + console.log(apps); + return apps; +} + export { getApps }; diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts index 301259198..f84001512 100644 --- a/backend/src/integrations/exchange.ts +++ b/backend/src/integrations/exchange.ts @@ -6,11 +6,13 @@ import { INTEGRATION_VERCEL, INTEGRATION_NETLIFY, INTEGRATION_GITHUB, + INTEGRATION_GITLAB, INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL, - INTEGRATION_GITHUB_TOKEN_URL + INTEGRATION_GITHUB_TOKEN_URL, + INTEGRATION_GITLAB_TOKEN_URL, } from '../variables'; import { SITE_URL, @@ -18,11 +20,13 @@ import { CLIENT_ID_VERCEL, CLIENT_ID_NETLIFY, CLIENT_ID_GITHUB, + CLIENT_ID_GITLAB, CLIENT_SECRET_AZURE, CLIENT_SECRET_HEROKU, CLIENT_SECRET_VERCEL, CLIENT_SECRET_NETLIFY, - CLIENT_SECRET_GITHUB + CLIENT_SECRET_GITHUB, + CLIENT_SECRET_GITLAB, } from '../config'; interface ExchangeCodeAzureResponse { @@ -66,6 +70,15 @@ interface ExchangeCodeGithubResponse { token_type: string; } +interface ExchangeCodeGitlabResponse { + access_token: string; + token_type: string; + expires_in: string; + refresh_token: string; + scope: string; + created_at: number; +} + /** * Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2 * code-token exchange for integration named [integration] @@ -114,6 +127,10 @@ const exchangeCode = async ({ code }); break; + case INTEGRATION_GITLAB: + obj = await exchangeCodeGitlab({ + code + }); } } catch (err) { Sentry.setUser(null); @@ -341,4 +358,48 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => { }; }; +/** + * Return [accessToken], [accessExpiresAt], and [refreshToken] for Gitlab + * code-token exchange + * @param {Object} obj1 + * @param {Object} obj1.code - code for code-token exchange + * @returns {Object} obj2 + * @returns {String} obj2.accessToken - access token for Github API + * @returns {String} obj2.refreshToken - refresh token for Github API + * @returns {Date} obj2.accessExpiresAt - date of expiration for access token + */ +const exchangeCodeGitlab = async ({ code }: { code: string }) => { + let res: ExchangeCodeGitlabResponse; + + try { + res = ( + await axios.post( + INTEGRATION_GITLAB_TOKEN_URL, + new URLSearchParams({ + grant_type: 'authorization_code', + code: code, + client_id: CLIENT_ID_GITLAB, + client_secret: CLIENT_SECRET_GITLAB, + redirect_uri: `${SITE_URL}/integrations/gitlab/oauth2/callback` + } as any), + { + headers: { + "Accept-Encoding": "application/json", + } + } + ) + ).data; + }catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed OAuth2 code-token exchange with Gitlab'); + } + + return { + accessToken: res.access_token, + refreshToken: null, + accessExpiresAt: null + }; +} + export { exchangeCode }; diff --git a/backend/src/integrations/revoke.ts b/backend/src/integrations/revoke.ts index 25ec70313..188b166a7 100644 --- a/backend/src/integrations/revoke.ts +++ b/backend/src/integrations/revoke.ts @@ -10,7 +10,8 @@ import { INTEGRATION_HEROKU, INTEGRATION_VERCEL, INTEGRATION_NETLIFY, - INTEGRATION_GITHUB + INTEGRATION_GITHUB, + INTEGRATION_GITLAB, } from '../variables'; const revokeAccess = async ({ @@ -32,6 +33,8 @@ const revokeAccess = async ({ break; case INTEGRATION_GITHUB: break; + case INTEGRATION_GITLAB: + break; } deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({ diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 70b60dd9f..a2d48f7aa 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -20,11 +20,13 @@ import { INTEGRATION_VERCEL, INTEGRATION_NETLIFY, INTEGRATION_GITHUB, + INTEGRATION_GITLAB, INTEGRATION_RENDER, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, INTEGRATION_HEROKU_API_URL, + INTEGRATION_GITLAB_API_URL, INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, @@ -57,6 +59,7 @@ const syncSecrets = async ({ accessToken: string; }) => { try { + console.log(integration); // aashish switch (integration.integration) { case INTEGRATION_AZURE_KEY_VAULT: await syncSecretsAzureKeyVault({ @@ -111,6 +114,13 @@ const syncSecrets = async ({ accessToken, }); break; + case INTEGRATION_GITLAB: + await syncSecretsGitLab({ + integration, + secrets, + accessToken, + }); + break; case INTEGRATION_RENDER: await syncSecretsRender({ integration, @@ -1407,7 +1417,7 @@ const syncSecretsTravisCI = async ({ } // delete secret - for (const sec of getSecretsRes) { + for (let sec of getSecretsRes) { if (!(sec.name in secrets)){ await axios.delete( `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${sec.id}?repository_id=${sec.repository_id}`, @@ -1428,4 +1438,95 @@ const syncSecretsTravisCI = async ({ } } +/** + * Sync/push [secrets] to GitLab repo with name [integration.app] + * @param {Object} obj + * @param {IIntegration} obj.integration - integration details + * @param {IIntegrationAuth} obj.integrationAuth - integration auth details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for GitLab integration + */ +const syncSecretsGitLab = async ({ + integration, + secrets, + accessToken, +}: { + integration: IIntegration; + secrets: any; + accessToken: string; +}) => { + try { + // get secrets from gitlab + const getSecretsRes = ( + await axios.get( + `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`, + { + headers: { + "Authorization": `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ) + ).data; + + for (const key of Object.keys(secrets)) { + const existingSecret = getSecretsRes.find((s: any) => s.key == key); + if (!existingSecret) { + await axios.post( + `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`, + { + key: key, + value: secrets[key], + protected: false, + masked: false, + raw: false, + environment_scope:'*' + }, + { + headers: { + "Authorization": `Bearer ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json", + }, + } + ) + }else { + // udpate secret + await axios.put( + `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${existingSecret.key}`, + { + ...existingSecret, + value: secrets[existingSecret.key] + }, + { + headers: { + "Authorization": `Bearer ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json", + }, + } + ) + } + } + + // delete secrets + for (let sec of getSecretsRes) { + if (!(sec.key in secrets)) { + await axios.delete( + `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${sec.key}`, + { + headers: { + "Authorization": `Bearer ${accessToken}`, + }, + } + ); + } + } + }catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error("Failed to sync secrets to GitLab"); + } +} + export { syncSecrets }; diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index 35ef6106b..d5d9b80ce 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -7,6 +7,7 @@ import { INTEGRATION_VERCEL, INTEGRATION_NETLIFY, INTEGRATION_GITHUB, + INTEGRATION_GITLAB, INTEGRATION_RENDER, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, @@ -31,7 +32,8 @@ export interface IIntegration { | 'heroku' | 'vercel' | 'netlify' - | 'github' + | 'github' + | 'gitlab' | 'render' | 'flyio' | 'circleci' @@ -96,6 +98,7 @@ const integrationSchema = new Schema( INTEGRATION_VERCEL, INTEGRATION_NETLIFY, INTEGRATION_GITHUB, + INTEGRATION_GITLAB, INTEGRATION_RENDER, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index c68fdd336..8f4aed663 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -7,6 +7,7 @@ import { INTEGRATION_VERCEL, INTEGRATION_NETLIFY, INTEGRATION_GITHUB, + INTEGRATION_GITLAB, INTEGRATION_RENDER, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, @@ -16,7 +17,7 @@ import { export interface IIntegrationAuth { _id: Types.ObjectId; workspace: Types.ObjectId; - integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'aws-parameter-store' | 'aws-secret-manager'; + integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'aws-parameter-store' | 'aws-secret-manager'; teamId: string; accountId: string; refreshCiphertext?: string; @@ -48,6 +49,7 @@ const integrationAuthSchema = new Schema( INTEGRATION_VERCEL, INTEGRATION_NETLIFY, INTEGRATION_GITHUB, + INTEGRATION_GITLAB, INTEGRATION_RENDER, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index 2be394971..269f0d778 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -13,6 +13,7 @@ import { INTEGRATION_VERCEL, INTEGRATION_NETLIFY, INTEGRATION_GITHUB, + INTEGRATION_GITLAB, INTEGRATION_RENDER, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, @@ -24,7 +25,9 @@ import { INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_GITHUB_TOKEN_URL, + INTEGRATION_GITLAB_TOKEN_URL, INTEGRATION_HEROKU_API_URL, + INTEGRATION_GITLAB_API_URL, INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, @@ -80,6 +83,7 @@ export { INTEGRATION_VERCEL, INTEGRATION_NETLIFY, INTEGRATION_GITHUB, + INTEGRATION_GITLAB, INTEGRATION_RENDER, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, @@ -91,7 +95,9 @@ export { INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_GITHUB_TOKEN_URL, + INTEGRATION_GITLAB_TOKEN_URL, INTEGRATION_HEROKU_API_URL, + INTEGRATION_GITLAB_API_URL, INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index a6e2029e2..77e43e688 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -1,5 +1,6 @@ import { CLIENT_ID_AZURE, + CLIENT_ID_GITLAB, TENANT_ID_AZURE } from '../config'; import { @@ -7,6 +8,7 @@ import { CLIENT_ID_NETLIFY, CLIENT_ID_GITHUB, CLIENT_SLUG_VERCEL, + CLIENT_SECRET_GITLAB, } from "../config"; // integrations @@ -17,6 +19,7 @@ const INTEGRATION_HEROKU = "heroku"; const INTEGRATION_VERCEL = "vercel"; const INTEGRATION_NETLIFY = "netlify"; const INTEGRATION_GITHUB = "github"; +const INTEGRATION_GITLAB = "gitlab"; const INTEGRATION_RENDER = "render"; const INTEGRATION_FLYIO = "flyio"; const INTEGRATION_CIRCLECI = "circleci"; @@ -27,6 +30,7 @@ const INTEGRATION_SET = new Set([ INTEGRATION_VERCEL, INTEGRATION_NETLIFY, INTEGRATION_GITHUB, + INTEGRATION_GITLAB, INTEGRATION_RENDER, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, @@ -44,9 +48,12 @@ const INTEGRATION_VERCEL_TOKEN_URL = const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token"; const INTEGRATION_GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token"; +const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token"; + // integration apps endpoints const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com"; +const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api"; const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com"; const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com"; const INTEGRATION_RENDER_API_URL = "https://api.render.com"; @@ -92,6 +99,15 @@ const INTEGRATION_OPTIONS = [ clientId: CLIENT_ID_GITHUB, docsLink: '' }, + { + name: 'GitLab', + slug: 'gitlab', + image: 'GitLab.png', + isAvailable: true, + type: 'oauth', + clientId: CLIENT_ID_GITLAB, + docsLink: '' + }, { name: 'Render', slug: 'render', @@ -175,6 +191,7 @@ export { INTEGRATION_VERCEL, INTEGRATION_NETLIFY, INTEGRATION_GITHUB, + INTEGRATION_GITLAB, INTEGRATION_RENDER, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, @@ -186,7 +203,9 @@ export { INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_GITHUB_TOKEN_URL, + INTEGRATION_GITLAB_API_URL, INTEGRATION_HEROKU_API_URL, + INTEGRATION_GITLAB_TOKEN_URL, INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index 0a6733b25..8a69465ac 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -10,6 +10,7 @@ const integrationSlugNameMapping: Mapping = { 'vercel': 'Vercel', 'netlify': 'Netlify', 'github': 'GitHub', + 'gitlab': 'GitLab', 'render': 'Render', 'flyio': 'Fly.io', 'circleci': 'CircleCI', diff --git a/frontend/public/images/integrations/GitLab.png b/frontend/public/images/integrations/GitLab.png new file mode 100644 index 0000000000000000000000000000000000000000..ffe9e244a13d0182db0ac27ab040903ae8fa36e6 GIT binary patch literal 17927 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 + 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: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null, + sourceEnvironment: selectedSourceEnvironment, + targetEnvironment: null, + owner, + path: null, + region: null + }); + + setIsLoading(false); + router.push( + `/integrations/${localStorage.getItem('projectData.id')}` + ); + } catch (err) { + console.error(err); + } + } + + return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? ( +
+ + GitLab Integration + + + + + + + + +
+ ) :
+} + +GitLabCreateIntegrationPage.requireAuth = true; + +export const getServerSideProps = getTranslatedServerSideProps(['integrations']); \ No newline at end of file diff --git a/frontend/src/pages/integrations/gitlab/oauth2/callback.tsx b/frontend/src/pages/integrations/gitlab/oauth2/callback.tsx new file mode 100644 index 000000000..0612ee82e --- /dev/null +++ b/frontend/src/pages/integrations/gitlab/oauth2/callback.tsx @@ -0,0 +1,39 @@ +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 GitLabOAuth2CallbackPage() { + 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: 'gitlab' + }); + + router.push( + `/integrations/gitlab/create?integrationAuthId=${integrationAuth._id}` + ); + + } catch (err) { + console.error(err); + } + })(); + }, []); + + return
+} + +GitLabOAuth2CallbackPage.requireAuth = true; + +export const getServerSideProps = getTranslatedServerSideProps(['integrations']); \ No newline at end of file