diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index cd6cb263f..e1aaf08e2 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -25,6 +25,8 @@ import { INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_GITLAB_API_URL, + INTEGRATION_HASURA_CLOUD, + INTEGRATION_HASURA_CLOUD_API_URL, INTEGRATION_HEROKU, INTEGRATION_HEROKU_API_URL, INTEGRATION_LARAVELFORGE, @@ -47,7 +49,7 @@ import { INTEGRATION_VERCEL, INTEGRATION_VERCEL_API_URL, INTEGRATION_WINDMILL, - INTEGRATION_WINDMILL_API_URL, + INTEGRATION_WINDMILL_API_URL } from "../variables"; import { IIntegrationAuth } from "../models"; import { Octokit } from "@octokit/rest"; @@ -73,7 +75,7 @@ const getApps = async ({ accessToken, accessId, teamId, - workspaceSlug, + workspaceSlug }: { integrationAuth: IIntegrationAuth; accessToken: string; @@ -85,7 +87,7 @@ const getApps = async ({ switch (integrationAuth.integration) { case INTEGRATION_GCP_SECRET_MANAGER: apps = await getAppsGCPSecretManager({ - accessToken, + accessToken }); break; case INTEGRATION_AZURE_KEY_VAULT: @@ -99,50 +101,50 @@ const getApps = async ({ break; case INTEGRATION_HEROKU: apps = await getAppsHeroku({ - accessToken, + accessToken }); break; case INTEGRATION_VERCEL: apps = await getAppsVercel({ integrationAuth, - accessToken, + accessToken }); break; case INTEGRATION_NETLIFY: apps = await getAppsNetlify({ - accessToken, + accessToken }); break; case INTEGRATION_GITHUB: apps = await getAppsGithub({ - accessToken, + accessToken }); break; case INTEGRATION_GITLAB: apps = await getAppsGitlab({ integrationAuth, accessToken, - teamId, + teamId }); break; case INTEGRATION_RENDER: apps = await getAppsRender({ - accessToken, + accessToken }); break; case INTEGRATION_RAILWAY: apps = await getAppsRailway({ - accessToken, + accessToken }); break; case INTEGRATION_FLYIO: apps = await getAppsFlyio({ - accessToken, + accessToken }); break; case INTEGRATION_CIRCLECI: apps = await getAppsCircleCI({ - accessToken, + accessToken }); break; case INTEGRATION_LARAVELFORGE: @@ -154,28 +156,28 @@ const getApps = async ({ case INTEGRATION_TERRAFORM_CLOUD: apps = await getAppsTerraformCloud({ accessToken, - workspacesId: accessId, + workspacesId: accessId }); break; case INTEGRATION_TRAVISCI: apps = await getAppsTravisCI({ - accessToken, + accessToken }); break; case INTEGRATION_TEAMCITY: apps = await getAppsTeamCity({ integrationAuth, - accessToken, + accessToken }); break; case INTEGRATION_SUPABASE: apps = await getAppsSupabase({ - accessToken, + accessToken }); break; case INTEGRATION_CHECKLY: apps = await getAppsCheckly({ - accessToken, + accessToken }); break; case INTEGRATION_CLOUDFLARE_PAGES: @@ -186,7 +188,7 @@ const getApps = async ({ break; case INTEGRATION_NORTHFLANK: apps = await getAppsNorthflank({ - accessToken, + accessToken }); break; case INTEGRATION_BITBUCKET: @@ -197,7 +199,7 @@ const getApps = async ({ break; case INTEGRATION_CODEFRESH: apps = await getAppsCodefresh({ - accessToken, + accessToken }); break; case INTEGRATION_WINDMILL: @@ -206,13 +208,19 @@ const getApps = async ({ }); break; case INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM: - apps = await getAppsDigitalOceanAppPlatform({ - accessToken + apps = await getAppsDigitalOceanAppPlatform({ + accessToken }); break; case INTEGRATION_CLOUD_66: apps = await getAppsCloud66({ - accessToken, + accessToken + }); + break; + + case INTEGRATION_HASURA_CLOUD: + apps = await getAppsHasuraCloud({ + accessToken }); break; } @@ -229,73 +237,79 @@ const getApps = async ({ * @returns {String} apps.appId - id of GCP project */ const getAppsGCPSecretManager = async ({ accessToken }: { accessToken: string }) => { - interface GCPApp { projectNumber: string; projectId: string; - lifecycleState: "ACTIVE" | "LIFECYCLE_STATE_UNSPECIFIED" | "DELETE_REQUESTED" | "DELETE_IN_PROGRESS"; + lifecycleState: + | "ACTIVE" + | "LIFECYCLE_STATE_UNSPECIFIED" + | "DELETE_REQUESTED" + | "DELETE_IN_PROGRESS"; name: string; createTime: string; parent: { type: "organization" | "folder" | "project"; id: string; - } + }; } - + interface GCPGetProjectsRes { projects: GCPApp[]; nextPageToken?: string; } - + interface GCPGetServiceRes { name: string; parent: string; - state: "ENABLED" | "DISABLED" | "STATE_UNSPECIFIED" + state: "ENABLED" | "DISABLED" | "STATE_UNSPECIFIED"; } let gcpApps: GCPApp[] = []; const apps: App[] = []; - + const pageSize = 100; let pageToken: string | undefined; let hasMorePages = true; - + while (hasMorePages) { const params = new URLSearchParams({ pageSize: String(pageSize), ...(pageToken ? { pageToken } : {}) }); - const res: GCPGetProjectsRes = (await standardRequest.get(`${INTEGRATION_GCP_API_URL}/v1/projects`, { + const res: GCPGetProjectsRes = ( + await standardRequest.get(`${INTEGRATION_GCP_API_URL}/v1/projects`, { params, headers: { - "Authorization": `Bearer ${accessToken}`, + Authorization: `Bearer ${accessToken}`, "Accept-Encoding": "application/json" } }) - ) - .data; - + ).data; + gcpApps = gcpApps.concat(res.projects); if (!res.nextPageToken) { hasMorePages = false; } - + pageToken = res.nextPageToken; } - + for await (const gcpApp of gcpApps) { try { - const res: GCPGetServiceRes = (await standardRequest.get( - `${INTEGRATION_GCP_SERVICE_USAGE_URL}/v1/projects/${gcpApp.projectId}/services/${INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME}`, { - headers: { - "Authorization": `Bearer ${accessToken}`, - "Accept-Encoding": "application/json" + const res: GCPGetServiceRes = ( + await standardRequest.get( + `${INTEGRATION_GCP_SERVICE_USAGE_URL}/v1/projects/${gcpApp.projectId}/services/${INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } } - } - )).data; - + ) + ).data; + if (res.state === "ENABLED") { apps.push({ name: gcpApp.name, @@ -322,13 +336,13 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { await standardRequest.get(`${INTEGRATION_HEROKU_API_URL}/apps`, { headers: { Accept: "application/vnd.heroku+json; version=3", - Authorization: `Bearer ${accessToken}`, - }, + Authorization: `Bearer ${accessToken}` + } }) ).data; const apps = res.map((a: any) => ({ - name: a.name, + name: a.name })); return apps; @@ -343,7 +357,7 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { */ const getAppsVercel = async ({ integrationAuth, - accessToken, + accessToken }: { integrationAuth: IIntegrationAuth; accessToken: string; @@ -352,21 +366,21 @@ const getAppsVercel = async ({ await standardRequest.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, { headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", + "Accept-Encoding": "application/json" }, ...(integrationAuth?.teamId ? { - params: { - teamId: integrationAuth.teamId, - }, - } - : {}), + params: { + teamId: integrationAuth.teamId + } + } + : {}) }) ).data; const apps = res.projects.map((a: any) => ({ name: a.name, - appId: a.id, + appId: a.id })); return apps; @@ -390,24 +404,21 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => { const params = new URLSearchParams({ page: String(page), per_page: String(perPage), - filter: "all", + filter: "all" }); - const { data } = await standardRequest.get( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, - { - params, - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + const { data } = await standardRequest.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); + }); data.map((a: any) => { apps.push({ name: a.name, - appId: a.site_id, + appId: a.site_id }); }); @@ -441,7 +452,7 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => { } const octokit = new Octokit({ - auth: accessToken, + auth: accessToken }); const getAllRepos = async () => { @@ -455,7 +466,7 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => { "GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}", { per_page, - page, + page } ); @@ -478,7 +489,7 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => { return { appId: a.id, name: a.name, - owner: a.owner.login, + owner: a.owner.login }; }); @@ -499,14 +510,14 @@ const getAppsRender = async ({ accessToken }: { accessToken: string }) => { headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json", - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } }) ).data; const apps = res.map((a: any) => ({ name: a.service.name, - appId: a.service.id, + appId: a.service.id })); return apps; @@ -540,27 +551,27 @@ const getAppsRailway = async ({ accessToken }: { accessToken: string }) => { const { data: { data: { - projects: { edges }, - }, - }, + projects: { edges } + } + } } = await standardRequest.post( INTEGRATION_RAILWAY_API_URL, { query, - variables, + variables }, { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); const apps = edges.map((e: any) => ({ name: e.node.name, - appId: e.node.id, + appId: e.node.id })); return apps; @@ -575,7 +586,7 @@ const getAppsRailway = async ({ accessToken }: { accessToken: string }) => { * @returns {String} apps.name - name of Laravel Forge sites * @returns {String} apps.appId - id of Laravel Forge sites */ -const getAppsLaravelForge = async ({ +const getAppsLaravelForge = async ({ accessToken, serverId }: { @@ -583,18 +594,21 @@ const getAppsLaravelForge = async ({ serverId?: string; }) => { const res = ( - await standardRequest.get(`${INTEGRATION_LARAVELFORGE_API_URL}/api/v1/servers/${serverId}/sites`, { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json", - "Content-Type": "application/json", - }, - }) + await standardRequest.get( + `${INTEGRATION_LARAVELFORGE_API_URL}/api/v1/servers/${serverId}/sites`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "Content-Type": "application/json" + } + } + ) ).data.sites; const apps = res.map((a: any) => ({ name: a.name, - appId: a.id, + appId: a.id })); return apps; @@ -613,7 +627,7 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => { name: string; hostname: string; } - + const query = ` query($role: String) { apps(type: "container", first: 400, role: $role) { @@ -632,15 +646,15 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => { { query, variables: { - role: null, - }, + role: null + } }, { headers: { Authorization: "Bearer " + accessToken, Accept: "application/json", - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ) ).data.data.apps.nodes; @@ -665,14 +679,14 @@ const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => { await standardRequest.get(`${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`, { headers: { "Circle-Token": accessToken, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } }) ).data; const apps = res?.map((a: any) => { return { - name: a?.reponame, + name: a?.reponame }; }); @@ -684,15 +698,15 @@ const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => { await standardRequest.get(`${INTEGRATION_TRAVISCI_API_URL}/repos`, { headers: { Authorization: `token ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } }) ).data; const apps = res?.map((a: any) => { return { name: a?.slug?.split("/")[1], - appId: a?.id, + appId: a?.id }; }); @@ -707,7 +721,7 @@ const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => { * @returns {Object[]} apps - names and ids of Terraform Cloud projects * @returns {String} apps.name - name of Terraform Cloud projects */ -const getAppsTerraformCloud = async ({ +const getAppsTerraformCloud = async ({ accessToken, workspacesId }: { @@ -715,27 +729,29 @@ const getAppsTerraformCloud = async ({ workspacesId?: string; }) => { const res = ( - await standardRequest.get(`${INTEGRATION_TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${workspacesId}`, { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json", - }, - }) + await standardRequest.get( + `${INTEGRATION_TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${workspacesId}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ) ).data.data; - const apps = [] + const apps = []; const appsObj = { - name: res?.attributes.name, - appId: res?.id, + name: res?.attributes.name, + appId: res?.id }; - apps.push(appsObj) + apps.push(appsObj); return apps; }; - /** * Return list of repositories for GitLab integration * @param {Object} obj @@ -746,14 +762,16 @@ const getAppsTerraformCloud = async ({ const getAppsGitlab = async ({ integrationAuth, accessToken, - teamId, + teamId }: { integrationAuth: IIntegrationAuth; accessToken: string; teamId?: string; }) => { - const gitLabApiUrl = integrationAuth.url ? `${integrationAuth.url}/api` : INTEGRATION_GITLAB_API_URL; - + const gitLabApiUrl = integrationAuth.url + ? `${integrationAuth.url}/api` + : INTEGRATION_GITLAB_API_URL; + const apps: App[] = []; let page = 1; @@ -766,24 +784,21 @@ const getAppsGitlab = async ({ while (hasMorePages) { const params = new URLSearchParams({ page: String(page), - per_page: String(perPage), + per_page: String(perPage) }); - const { data } = await standardRequest.get( - `${gitLabApiUrl}/v4/groups/${teamId}/projects`, - { - params, - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + const { data } = await standardRequest.get(`${gitLabApiUrl}/v4/groups/${teamId}/projects`, { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); + }); data.map((a: any) => { apps.push({ name: a.name, - appId: a.id, + appId: a.id }); }); @@ -800,32 +815,29 @@ const getAppsGitlab = async ({ await standardRequest.get(`${gitLabApiUrl}/v4/user`, { headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } }) ).data; while (hasMorePages) { const params = new URLSearchParams({ page: String(page), - per_page: String(perPage), + per_page: String(perPage) }); - const { data } = await standardRequest.get( - `${gitLabApiUrl}/v4/users/${id}/projects`, - { - params, - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + const { data } = await standardRequest.get(`${gitLabApiUrl}/v4/users/${id}/projects`, { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); + }); data.map((a: any) => { apps.push({ name: a.name, - appId: a.id, + appId: a.id }); }); @@ -847,9 +859,9 @@ const getAppsGitlab = async ({ * @returns {Object[]} apps - names and ids of TeamCity projects * @returns {String} apps.name - name of TeamCity projects */ -const getAppsTeamCity = async ({ +const getAppsTeamCity = async ({ integrationAuth, - accessToken, + accessToken }: { integrationAuth: IIntegrationAuth; accessToken: string; @@ -858,15 +870,15 @@ const getAppsTeamCity = async ({ await standardRequest.get(`${integrationAuth.url}/app/rest/projects`, { headers: { Authorization: `Bearer ${accessToken}`, - Accept: "application/json", - }, + Accept: "application/json" + } }) ).data.project.slice(1); - + const apps = res.map((a: any) => { return { name: a.name, - appId: a.id, + appId: a.id }; }); @@ -881,20 +893,17 @@ const getAppsTeamCity = async ({ * @returns {String} apps.name - name of Supabase app */ const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => { - const { data } = await standardRequest.get( - `${INTEGRATION_SUPABASE_API_URL}/v1/projects`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + const { data } = await standardRequest.get(`${INTEGRATION_SUPABASE_API_URL}/v1/projects`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); + }); const apps = data.map((a: any) => { return { name: a.name, - appId: a.id, + appId: a.id }; }); @@ -909,20 +918,17 @@ const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => { * @returns {String} apps.name - name of Checkly account */ const getAppsCheckly = async ({ accessToken }: { accessToken: string }) => { - const { data } = await standardRequest.get( - `${INTEGRATION_CHECKLY_API_URL}/v1/accounts`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept": "application/json", - }, + const { data } = await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/accounts`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" } - ); + }); const apps = data.map((a: any) => { return { name: a.name, - appId: a.id, + appId: a.id }; }); @@ -948,19 +954,19 @@ const getAppsCloudflarePages = async ({ { headers: { Authorization: `Bearer ${accessToken}`, - "Accept": "application/json", - }, + Accept: "application/json" + } } ); const apps = data.result.map((a: any) => { return { name: a.name, - appId: a.id, + appId: a.id }; }); return apps; -} +}; /** * Return list of repositories for the BitBucket integration based on provided BitBucket workspace @@ -970,9 +976,9 @@ const getAppsCloudflarePages = async ({ * @returns {Object[]} apps - BitBucket repositories * @returns {String} apps.name - name of BitBucket repository */ -const getAppsBitBucket = async ({ +const getAppsBitBucket = async ({ accessToken, - workspaceSlug, + workspaceSlug }: { accessToken: string; workspaceSlug?: string; @@ -996,45 +1002,42 @@ const getAppsBitBucket = async ({ } if (!workspaceSlug) { - return [] + return []; } - + const repositories: Repository[] = []; let hasNextPage = true; - let repositoriesUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}` + let repositoriesUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}`; while (hasNextPage) { - const { data }: { data: RepositoriesResponse } = await standardRequest.get( - repositoriesUrl, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept": "application/json", - }, - } - ); + const { data }: { data: RepositoriesResponse } = await standardRequest.get(repositoriesUrl, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + }); if (data?.values.length > 0) { data.values.forEach((repository) => { - repositories.push(repository) - }) + repositories.push(repository); + }); } if (data.next) { - repositoriesUrl = data.next + repositoriesUrl = data.next; } else { - hasNextPage = false + hasNextPage = false; } } const apps = repositories.map((repository) => { - return { - name: repository.name, - appId: repository.uuid, - }; + return { + name: repository.name, + appId: repository.uuid + }; }); return apps; -} +}; /** Return list of projects for Northflank integration * @param {Object} obj @@ -1045,19 +1048,14 @@ const getAppsBitBucket = async ({ const getAppsNorthflank = async ({ accessToken }: { accessToken: string }) => { const { data: { - data: { - projects - } + data: { projects } } - } = await standardRequest.get( - `${INTEGRATION_NORTHFLANK_API_URL}/v1/projects`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + } = await standardRequest.get(`${INTEGRATION_NORTHFLANK_API_URL}/v1/projects`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); + }); const apps = projects.map((a: any) => { return { @@ -1076,27 +1074,22 @@ const getAppsNorthflank = async ({ accessToken }: { accessToken: string }) => { * @returns {Object[]} apps - names of Supabase apps * @returns {String} apps.name - name of Supabase app */ -const getAppsCodefresh = async ({ - accessToken, -}: { - accessToken: string; -}) => { +const getAppsCodefresh = async ({ accessToken }: { accessToken: string }) => { const res = ( await standardRequest.get(`${INTEGRATION_CODEFRESH_API_URL}/projects`, { headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } }) ).data; const apps = res.projects.map((a: any) => ({ name: a.projectName, - appId: a.id, + appId: a.id })); return apps; - }; /** @@ -1107,16 +1100,13 @@ const getAppsCodefresh = async ({ * @returns {String} apps.name - name of Windmill workspace */ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { - const { data } = await standardRequest.get( - `${INTEGRATION_WINDMILL_API_URL}/workspaces/list`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + const { data } = await standardRequest.get(`${INTEGRATION_WINDMILL_API_URL}/workspaces/list`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); - + }); + // check for write access of secrets in windmill workspaces const writeAccessCheck = data.map(async (app: any) => { try { @@ -1134,8 +1124,8 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { { headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); @@ -1150,30 +1140,30 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { { headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); - + // is write access is allowed then delete the created secrets from workspace if (writeUser && writeFolder) { await standardRequest.delete( - `${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/delete/${userPath}`, + `${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/delete/${userPath}`, { headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); await standardRequest.delete( - `${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/delete/${folderPath}`, + `${INTEGRATION_WINDMILL_API_URL}/w/${app.id}/variables/delete/${folderPath}`, { headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, + "Accept-Encoding": "application/json" + } } ); @@ -1188,16 +1178,16 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { const appsWriteResponses = await Promise.all(writeAccessCheck); const appsWithWriteAccess = appsWriteResponses.filter((appRes: any) => !appRes.error); - + const apps = appsWithWriteAccess.map((a: any) => { return { name: a.name, - appId: a.id, + appId: a.id }; }); - + return apps; -} +}; /** * Return list of applications for DigitalOcean App Platform integration @@ -1225,7 +1215,7 @@ const getAppsDigitalOceanAppPlatform = async ({ accessToken }: { accessToken: st value: string; scope: string; } - + const res = ( await standardRequest.get(`${INTEGRATION_DIGITAL_OCEAN_API_URL}/v2/apps`, { headers: { @@ -1239,8 +1229,28 @@ const getAppsDigitalOceanAppPlatform = async ({ accessToken }: { accessToken: st name: a.spec.name, appId: a.id })); -} - +}; + +const getAppsHasuraCloud = async ({ accessToken }: { accessToken: string }) => { + const res = await standardRequest.post( + INTEGRATION_HASURA_CLOUD_API_URL, + { + query: "query MyQuery { projects { name tenant { id } } }" + }, + { + headers: { + Authorization: `pat ${accessToken}`, + "Content-Type": "application/json" + } + } + ); + + const data = (res?.data?.data?.projects ?? []).map( + ({ name, tenant: { id: appId } }: { name: string; tenant: { id: string } }) => ({ name, appId }) + ); + return data; +}; + /** * Return list of applications for Cloud66 integration * @param {Object} obj @@ -1290,7 +1300,7 @@ const getAppsCloud66 = async ({ accessToken }: { accessToken: string }) => { "Accept-Encoding": "application/json" } }) - ).data.response as Cloud66Apps[] + ).data.response as Cloud66Apps[]; const apps = stacks.map((app) => ({ name: app.name, diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index fe5885c9e..5f7d53345 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -32,6 +32,8 @@ import { INTEGRATION_GITLAB, INTEGRATION_GITLAB_API_URL, INTEGRATION_HASHICORP_VAULT, + INTEGRATION_HASURA_CLOUD, + INTEGRATION_HASURA_CLOUD_API_URL, INTEGRATION_HEROKU, INTEGRATION_HEROKU_API_URL, INTEGRATION_LARAVELFORGE, @@ -63,6 +65,10 @@ import { Octokit } from "@octokit/rest"; import _ from "lodash"; import sodium from "libsodium-wrappers"; import { standardRequest } from "../config/request"; +import { + ZGetTenantEnv, + ZUpdateTenantEnv +} from "../validation/hasuraCloudIntegration"; const getSecretKeyValuePair = ( secrets: Record @@ -95,7 +101,7 @@ const syncSecrets = async ({ secrets: Record; accessId: string | null; accessToken: string; - appendices?: { prefix: string, suffix: string }; + appendices?: { prefix: string; suffix: string }; }) => { switch (integration.integration) { case INTEGRATION_GCP_SECRET_MANAGER: @@ -306,6 +312,14 @@ const syncSecrets = async ({ accessToken }); break; + + case INTEGRATION_HASURA_CLOUD: + await syncSecretsHasuraCloud({ + integration, + secrets, + accessToken + }); + break; } }; @@ -963,8 +977,9 @@ const syncSecretsVercel = async ({ : {}), ...(integration?.path ? { - gitBranch: integration?.path - } : {}) + gitBranch: integration?.path + } + : {}) }; const vercelSecrets: VercelSecret[] = ( @@ -992,7 +1007,7 @@ const syncSecretsVercel = async ({ return true; }); - + const res: { [key: string]: VercelSecret } = {}; for await (const vercelSecret of vercelSecrets) { @@ -1352,7 +1367,7 @@ const syncSecretsGitHub = async ({ integration: IIntegration; secrets: Record; accessToken: string; - appendices?: { prefix: string, suffix: string }; + appendices?: { prefix: string; suffix: string }; }) => { interface GitHubRepoKey { key_id: string; @@ -1395,14 +1410,23 @@ const syncSecretsGitHub = async ({ {} ); - encryptedSecrets = Object.keys(encryptedSecrets).reduce((result: { - [key: string]: GitHubSecret; - }, key) => { - if ((appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true)) { - result[key] = encryptedSecrets[key]; - } - return result; - }, {}); + encryptedSecrets = Object.keys(encryptedSecrets).reduce( + ( + result: { + [key: string]: GitHubSecret; + }, + key + ) => { + if ( + (appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && + (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true) + ) { + result[key] = encryptedSecrets[key]; + } + return result; + }, + {} + ); Object.keys(encryptedSecrets).map(async (key) => { if (!(key in secrets)) { @@ -2095,7 +2119,7 @@ const syncSecretsCheckly = async ({ integration: IIntegration; secrets: Record; accessToken: string; - appendices?: { prefix: string, suffix: string }; + appendices?: { prefix: string; suffix: string }; }) => { let getSecretsRes = ( await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, { @@ -2113,14 +2137,23 @@ const syncSecretsCheckly = async ({ {} ); - getSecretsRes = Object.keys(getSecretsRes).reduce((result: { - [key: string]: string; - }, key) => { - if ((appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true)) { - result[key] = getSecretsRes[key]; - } - return result; - }, {}); + getSecretsRes = Object.keys(getSecretsRes).reduce( + ( + result: { + [key: string]: string; + }, + key + ) => { + if ( + (appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && + (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true) + ) { + result[key] = getSecretsRes[key]; + } + return result; + }, + {} + ); // add secrets for await (const key of Object.keys(secrets)) { @@ -2195,18 +2228,20 @@ const syncSecretsQovery = async ({ secrets: Record; accessToken: string; }) => { - const getSecretsRes = ( - await standardRequest.get(`${INTEGRATION_QOVERY_API_URL}/${integration.scope}/${integration.appId}/environmentVariable`, { - headers: { - Authorization: `Token ${accessToken}`, - "Accept-Encoding": "application/json" + await standardRequest.get( + `${INTEGRATION_QOVERY_API_URL}/${integration.scope}/${integration.appId}/environmentVariable`, + { + headers: { + Authorization: `Token ${accessToken}`, + "Accept-Encoding": "application/json" + } } - }) + ) ).data.results.reduce( (obj: any, secret: any) => ({ ...obj, - [secret.key]: {"id": secret.id, "value": secret.value} + [secret.key]: { id: secret.id, value: secret.value } }), {} ); @@ -3076,4 +3111,111 @@ const syncSecretsNorthflank = async ({ ); }; +/** Sync/push [secrets] to Hasura Cloud + * @param {Object} obj + * @param {IIntegration} obj.integration - integration 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 Hasura Cloud integration + */ +const syncSecretsHasuraCloud = async ({ + integration, + secrets, + accessToken +}: { + integration: IIntegration; + secrets: Record; + accessToken: string; +}) => { + const res = await standardRequest.post( + INTEGRATION_HASURA_CLOUD_API_URL, + { + query: + "query MyQuery($tenantId: uuid!) { getTenantEnv(tenantId: $tenantId) { hash envVars } }", + variables: { + tenantId: integration.appId + } + }, + { + headers: { + Authorization: `pat ${accessToken}`, + "Content-Type": "application/json" + } + } + ); + + const { + data: { + getTenantEnv: { hash, envVars } + } + } = ZGetTenantEnv.parse(res.data); + + let currentHash = hash; + + const secretsToUpdate = Object.keys(secrets).map((key) => { + return ({ + key, + value: secrets[key].value + }); + }); + + if (secretsToUpdate.length) { + // update secrets + + const addRequest = await standardRequest.post( + INTEGRATION_HASURA_CLOUD_API_URL, + { + query: + "mutation MyQuery($currentHash: String!, $envs: [UpdateEnvObject!]!, $tenantId: uuid!) { updateTenantEnv(currentHash: $currentHash, envs: $envs, tenantId: $tenantId) { hash envVars} }", + variables: { + currentHash, + envs: secretsToUpdate, + tenantId: integration.appId + } + }, + { + headers: { + Authorization: `pat ${accessToken}`, + "Content-Type": "application/json" + } + } + ); + + const addRequestResponse = ZUpdateTenantEnv.safeParse(addRequest.data); + if (addRequestResponse.success) { + currentHash = addRequestResponse.data.data.updateTenantEnv.hash; + } + } + + const secretsToDelete = envVars.environment + ? Object.keys(envVars.environment).filter((key) => !(key in secrets)) + : []; + + if (secretsToDelete.length) { + await standardRequest.post( + INTEGRATION_HASURA_CLOUD_API_URL, + { + query: ` + mutation deleteTenantEnv($id: uuid!, $currentHash: String!, $env: [String!]!) { + deleteTenantEnv(tenantId: $id, currentHash: $currentHash, deleteEnvs: $env) { + hash + envVars + } + } + `, + variables: { + id: integration.appId, + currentHash, + env: secretsToDelete + } + }, + { + headers: { + Authorization: `pat ${accessToken}`, + "Content-Type": "application/json" + } + } + ); + } +}; + export { syncSecrets }; diff --git a/backend/src/models/integration/integration.ts b/backend/src/models/integration/integration.ts index 7b9957393..8c997a28d 100644 --- a/backend/src/models/integration/integration.ts +++ b/backend/src/models/integration/integration.ts @@ -14,6 +14,7 @@ import { INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_HASHICORP_VAULT, + INTEGRATION_HASURA_CLOUD, INTEGRATION_HEROKU, INTEGRATION_LARAVELFORGE, INTEGRATION_NETLIFY, @@ -76,7 +77,8 @@ export interface IIntegration { | "cloud-66" | "northflank" | "windmill" - | "gcp-secret-manager"; + | "gcp-secret-manager" + | "hasura-cloud"; integrationAuth: Types.ObjectId; metadata: Metadata; } @@ -86,67 +88,67 @@ const integrationSchema = new Schema( workspace: { type: Schema.Types.ObjectId, ref: "Workspace", - required: true, + required: true }, environment: { type: String, - required: true, + required: true }, isActive: { type: Boolean, - required: true, + required: true }, url: { // for custom self-hosted integrations (e.g. self-hosted GitHub enterprise) type: String, - default: null, + default: null }, app: { // name of app in provider type: String, - default: null, + default: null }, appId: { // id of app in provider type: String, - default: null, + default: null }, targetEnvironment: { // target environment type: String, - default: null, + default: null }, targetEnvironmentId: { type: String, - default: null, + default: null }, targetService: { // railway-specific service // qovery-specific project type: String, - default: null, + default: null }, targetServiceId: { // railway-specific service // qovery specific project type: String, - default: null, + default: null }, owner: { // github-specific repo owner-login type: String, - default: null, + default: null }, path: { // aws-parameter-store-specific path // (also) vercel preview-branch type: String, - default: null, + default: null }, region: { // aws-parameter-store-specific path type: String, - default: null, + default: null }, scope: { // qovery-specific scope @@ -183,19 +185,20 @@ const integrationSchema = new Schema( INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_CLOUD_66, INTEGRATION_NORTHFLANK, - INTEGRATION_GCP_SECRET_MANAGER + INTEGRATION_GCP_SECRET_MANAGER, + INTEGRATION_HASURA_CLOUD ], - required: true, + required: true }, integrationAuth: { type: Schema.Types.ObjectId, ref: "IntegrationAuth", - required: true, + required: true }, secretPath: { type: String, required: true, - default: "/", + default: "/" }, metadata: { type: Schema.Types.Mixed, @@ -203,8 +206,8 @@ const integrationSchema = new Schema( } }, { - timestamps: true, + timestamps: true } ); -export const Integration = model("Integration", integrationSchema); \ No newline at end of file +export const Integration = model("Integration", integrationSchema); diff --git a/backend/src/models/integrationAuth/integrationAuth.ts b/backend/src/models/integrationAuth/integrationAuth.ts index 312ee09d7..e65c126af 100644 --- a/backend/src/models/integrationAuth/integrationAuth.ts +++ b/backend/src/models/integrationAuth/integrationAuth.ts @@ -1,205 +1,203 @@ import { - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_BASE64, - ENCODING_SCHEME_UTF8, - INTEGRATION_AWS_PARAMETER_STORE, - INTEGRATION_AWS_SECRET_MANAGER, - INTEGRATION_AZURE_KEY_VAULT, - INTEGRATION_BITBUCKET, - INTEGRATION_CIRCLECI, - INTEGRATION_CLOUDFLARE_PAGES, - INTEGRATION_CLOUD_66, - INTEGRATION_CODEFRESH, - INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, - INTEGRATION_FLYIO, - INTEGRATION_GCP_SECRET_MANAGER, - INTEGRATION_GITHUB, - INTEGRATION_GITLAB, - INTEGRATION_HASHICORP_VAULT, - INTEGRATION_HEROKU, - INTEGRATION_LARAVELFORGE, - INTEGRATION_NETLIFY, - INTEGRATION_NORTHFLANK, - INTEGRATION_RAILWAY, - INTEGRATION_RENDER, - INTEGRATION_SUPABASE, - INTEGRATION_TEAMCITY, - INTEGRATION_TERRAFORM_CLOUD, - INTEGRATION_TRAVISCI, - INTEGRATION_VERCEL, - INTEGRATION_WINDMILL - } from "../../variables"; - import { Document, Schema, Types, model } from "mongoose"; - import { IntegrationAuthMetadata } from "./types"; - - export interface IIntegrationAuth extends Document { - _id: Types.ObjectId; - workspace: Types.ObjectId; - integration: - | "heroku" - | "vercel" - | "netlify" - | "github" - | "gitlab" - | "render" - | "railway" - | "flyio" - | "azure-key-vault" - | "laravel-forge" - | "circleci" - | "travisci" - | "supabase" - | "aws-parameter-store" - | "aws-secret-manager" - | "checkly" - | "qovery" - | "cloudflare-pages" - | "codefresh" - | "digital-ocean-app-platform" - | "bitbucket" - | "cloud-66" - | "terraform-cloud" - | "teamcity" - | "northflank" - | "windmill" - | "gcp-secret-manager"; - teamId: string; - accountId: string; - url: string; - namespace: string; - refreshCiphertext?: string; - refreshIV?: string; - refreshTag?: string; - accessIdCiphertext?: string; - accessIdIV?: string; - accessIdTag?: string; - accessCiphertext?: string; - accessIV?: string; - accessTag?: string; - algorithm?: "aes-256-gcm"; - keyEncoding?: "utf8" | "base64"; - accessExpiresAt?: Date; - metadata?: IntegrationAuthMetadata; - } - - const integrationAuthSchema = new Schema( - { - workspace: { - type: Schema.Types.ObjectId, - ref: "Workspace", - required: true, - }, - integration: { - type: String, - enum: [ - INTEGRATION_AZURE_KEY_VAULT, - INTEGRATION_AWS_PARAMETER_STORE, - INTEGRATION_AWS_SECRET_MANAGER, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_GITLAB, - INTEGRATION_RENDER, - INTEGRATION_RAILWAY, - INTEGRATION_FLYIO, - INTEGRATION_CIRCLECI, - INTEGRATION_LARAVELFORGE, - INTEGRATION_TRAVISCI, - INTEGRATION_TEAMCITY, - INTEGRATION_SUPABASE, - INTEGRATION_TERRAFORM_CLOUD, - INTEGRATION_HASHICORP_VAULT, - INTEGRATION_CLOUDFLARE_PAGES, - INTEGRATION_CODEFRESH, - INTEGRATION_WINDMILL, - INTEGRATION_BITBUCKET, - INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, - INTEGRATION_CLOUD_66, - INTEGRATION_NORTHFLANK, - INTEGRATION_GCP_SECRET_MANAGER - ], - required: true, - }, - teamId: { - // vercel-specific integration param - type: String, - }, - url: { - // for any self-hosted integrations (e.g. self-hosted hashicorp-vault) - type: String, - }, - namespace: { - // hashicorp-vault-specific integration param - type: String, - }, - accountId: { - // netlify-specific integration param - type: String, - }, - refreshCiphertext: { - type: String, - select: false, - }, - refreshIV: { - type: String, - select: false, - }, - refreshTag: { - type: String, - select: false, - }, - accessIdCiphertext: { - type: String, - select: false, - }, - accessIdIV: { - type: String, - select: false, - }, - accessIdTag: { - type: String, - select: false, - }, - accessCiphertext: { - type: String, - select: false, - }, - accessIV: { - type: String, - select: false, - }, - accessTag: { - type: String, - select: false, - }, - accessExpiresAt: { - type: Date, - select: false, - }, - algorithm: { // the encryption algorithm used - type: String, - enum: [ALGORITHM_AES_256_GCM], - required: true, - }, - keyEncoding: { - type: String, - enum: [ - ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64, - ], - required: true, - }, - metadata: { - type: Schema.Types.Mixed - } + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_BASE64, + ENCODING_SCHEME_UTF8, + INTEGRATION_AWS_PARAMETER_STORE, + INTEGRATION_AWS_SECRET_MANAGER, + INTEGRATION_AZURE_KEY_VAULT, + INTEGRATION_BITBUCKET, + INTEGRATION_CIRCLECI, + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CLOUD_66, + INTEGRATION_CODEFRESH, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, + INTEGRATION_FLYIO, + INTEGRATION_GCP_SECRET_MANAGER, + INTEGRATION_GITHUB, + INTEGRATION_GITLAB, + INTEGRATION_HASHICORP_VAULT, + INTEGRATION_HASURA_CLOUD, + INTEGRATION_HEROKU, + INTEGRATION_LARAVELFORGE, + INTEGRATION_NETLIFY, + INTEGRATION_NORTHFLANK, + INTEGRATION_RAILWAY, + INTEGRATION_RENDER, + INTEGRATION_SUPABASE, + INTEGRATION_TEAMCITY, + INTEGRATION_TERRAFORM_CLOUD, + INTEGRATION_TRAVISCI, + INTEGRATION_VERCEL, + INTEGRATION_WINDMILL +} from "../../variables"; +import { Document, Schema, Types, model } from "mongoose"; +import { IntegrationAuthMetadata } from "./types"; + +export interface IIntegrationAuth extends Document { + _id: Types.ObjectId; + workspace: Types.ObjectId; + integration: + | "heroku" + | "vercel" + | "netlify" + | "github" + | "gitlab" + | "render" + | "railway" + | "flyio" + | "azure-key-vault" + | "laravel-forge" + | "circleci" + | "travisci" + | "supabase" + | "aws-parameter-store" + | "aws-secret-manager" + | "checkly" + | "qovery" + | "cloudflare-pages" + | "codefresh" + | "digital-ocean-app-platform" + | "bitbucket" + | "cloud-66" + | "terraform-cloud" + | "teamcity" + | "northflank" + | "windmill" + | "gcp-secret-manager" + | "hasura-cloud"; + teamId: string; + accountId: string; + url: string; + namespace: string; + refreshCiphertext?: string; + refreshIV?: string; + refreshTag?: string; + accessIdCiphertext?: string; + accessIdIV?: string; + accessIdTag?: string; + accessCiphertext?: string; + accessIV?: string; + accessTag?: string; + algorithm?: "aes-256-gcm"; + keyEncoding?: "utf8" | "base64"; + accessExpiresAt?: Date; + metadata?: IntegrationAuthMetadata; +} + +const integrationAuthSchema = new Schema( + { + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true }, - { - timestamps: true, + integration: { + type: String, + enum: [ + INTEGRATION_AZURE_KEY_VAULT, + INTEGRATION_AWS_PARAMETER_STORE, + INTEGRATION_AWS_SECRET_MANAGER, + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_GITLAB, + INTEGRATION_RENDER, + INTEGRATION_RAILWAY, + INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, + INTEGRATION_LARAVELFORGE, + INTEGRATION_TRAVISCI, + INTEGRATION_TEAMCITY, + INTEGRATION_SUPABASE, + INTEGRATION_TERRAFORM_CLOUD, + INTEGRATION_HASHICORP_VAULT, + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CODEFRESH, + INTEGRATION_WINDMILL, + INTEGRATION_BITBUCKET, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, + INTEGRATION_CLOUD_66, + INTEGRATION_NORTHFLANK, + INTEGRATION_GCP_SECRET_MANAGER, + INTEGRATION_HASURA_CLOUD + ], + required: true + }, + teamId: { + // vercel-specific integration param + type: String + }, + url: { + // for any self-hosted integrations (e.g. self-hosted hashicorp-vault) + type: String + }, + namespace: { + // hashicorp-vault-specific integration param + type: String + }, + accountId: { + // netlify-specific integration param + type: String + }, + refreshCiphertext: { + type: String, + select: false + }, + refreshIV: { + type: String, + select: false + }, + refreshTag: { + type: String, + select: false + }, + accessIdCiphertext: { + type: String, + select: false + }, + accessIdIV: { + type: String, + select: false + }, + accessIdTag: { + type: String, + select: false + }, + accessCiphertext: { + type: String, + select: false + }, + accessIV: { + type: String, + select: false + }, + accessTag: { + type: String, + select: false + }, + accessExpiresAt: { + type: Date, + select: false + }, + algorithm: { + // the encryption algorithm used + type: String, + enum: [ALGORITHM_AES_256_GCM], + required: true + }, + keyEncoding: { + type: String, + enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64], + required: true + }, + metadata: { + type: Schema.Types.Mixed } - ); - - export const IntegrationAuth = model( - "IntegrationAuth", - integrationAuthSchema - ); \ No newline at end of file + }, + { + timestamps: true + } +); + +export const IntegrationAuth = model("IntegrationAuth", integrationAuthSchema); diff --git a/backend/src/validation/hasuraCloudIntegration.ts b/backend/src/validation/hasuraCloudIntegration.ts new file mode 100644 index 000000000..63b037370 --- /dev/null +++ b/backend/src/validation/hasuraCloudIntegration.ts @@ -0,0 +1,21 @@ +import * as z from "zod"; + +export const ZGetTenantEnv = z.object({ + data: z.object({ + getTenantEnv: z.object({ + hash: z.string(), + envVars: z.object({ + environment: z.record(z.any()).optional() + }) + }) + }) +}); + +export const ZUpdateTenantEnv = z.object({ + data: z.object({ + updateTenantEnv: z.object({ + hash: z.string(), + envVars: z.record(z.any()) + }) + }) +}); diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index e6e20e73c..a70429082 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -1,12 +1,12 @@ import { - getClientIdAzure, - getClientIdBitBucket, - getClientIdGCPSecretManager, - getClientIdGitHub, - getClientIdGitLab, - getClientIdHeroku, - getClientIdNetlify, - getClientSlugVercel + getClientIdAzure, + getClientIdBitBucket, + getClientIdGCPSecretManager, + getClientIdGitHub, + getClientIdGitLab, + getClientIdHeroku, + getClientIdNetlify, + getClientSlugVercel } from "../config"; // integrations @@ -22,7 +22,7 @@ export const INTEGRATION_GITLAB = "gitlab"; export const INTEGRATION_RENDER = "render"; export const INTEGRATION_RAILWAY = "railway"; export const INTEGRATION_FLYIO = "flyio"; -export const INTEGRATION_LARAVELFORGE = "laravel-forge" +export const INTEGRATION_LARAVELFORGE = "laravel-forge"; export const INTEGRATION_CIRCLECI = "circleci"; export const INTEGRATION_TRAVISCI = "travisci"; export const INTEGRATION_TEAMCITY = "teamcity"; @@ -38,32 +38,34 @@ export const INTEGRATION_WINDMILL = "windmill"; export const INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM = "digital-ocean-app-platform"; export const INTEGRATION_CLOUD_66 = "cloud-66"; export const INTEGRATION_NORTHFLANK = "northflank"; +export const INTEGRATION_HASURA_CLOUD = "hasura-cloud"; export const INTEGRATION_SET = new Set([ - INTEGRATION_GCP_SECRET_MANAGER, - INTEGRATION_AZURE_KEY_VAULT, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_GITLAB, - INTEGRATION_RENDER, - INTEGRATION_FLYIO, - INTEGRATION_CIRCLECI, - INTEGRATION_LARAVELFORGE, - INTEGRATION_TRAVISCI, - INTEGRATION_TEAMCITY, - INTEGRATION_SUPABASE, - INTEGRATION_CHECKLY, - INTEGRATION_QOVERY, - INTEGRATION_TERRAFORM_CLOUD, - INTEGRATION_HASHICORP_VAULT, - INTEGRATION_CLOUDFLARE_PAGES, - INTEGRATION_CODEFRESH, - INTEGRATION_WINDMILL, - INTEGRATION_BITBUCKET, - INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, - INTEGRATION_CLOUD_66, - INTEGRATION_NORTHFLANK + INTEGRATION_GCP_SECRET_MANAGER, + INTEGRATION_AZURE_KEY_VAULT, + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_GITLAB, + INTEGRATION_RENDER, + INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, + INTEGRATION_LARAVELFORGE, + INTEGRATION_TRAVISCI, + INTEGRATION_TEAMCITY, + INTEGRATION_SUPABASE, + INTEGRATION_CHECKLY, + INTEGRATION_QOVERY, + INTEGRATION_TERRAFORM_CLOUD, + INTEGRATION_HASHICORP_VAULT, + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CODEFRESH, + INTEGRATION_WINDMILL, + INTEGRATION_BITBUCKET, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, + INTEGRATION_CLOUD_66, + INTEGRATION_NORTHFLANK, + INTEGRATION_HASURA_CLOUD ]); // integration types @@ -71,15 +73,14 @@ export const INTEGRATION_OAUTH2 = "oauth2"; // integration oauth endpoints export const INTEGRATION_GCP_TOKEN_URL = "https://oauth2.googleapis.com/token"; -export const INTEGRATION_AZURE_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; +export const INTEGRATION_AZURE_TOKEN_URL = + "https://login.microsoftonline.com/common/oauth2/v2.0/token"; export const INTEGRATION_HEROKU_TOKEN_URL = "https://id.heroku.com/oauth/token"; -export const INTEGRATION_VERCEL_TOKEN_URL = - "https://api.vercel.com/v2/oauth/access_token"; +export const INTEGRATION_VERCEL_TOKEN_URL = "https://api.vercel.com/v2/oauth/access_token"; export const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token"; -export const INTEGRATION_GITHUB_TOKEN_URL = - "https://github.com/login/oauth/access_token"; +export const INTEGRATION_GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token"; export const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token"; -export const INTEGRATION_BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token" +export const INTEGRATION_BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token"; // integration apps endpoints export const INTEGRATION_GCP_API_URL = "https://cloudresourcemanager.googleapis.com"; @@ -106,268 +107,279 @@ export const INTEGRATION_WINDMILL_API_URL = "https://app.windmill.dev/api"; export const INTEGRATION_DIGITAL_OCEAN_API_URL = "https://api.digitalocean.com"; export const INTEGRATION_CLOUD_66_API_URL = "https://app.cloud66.com/api"; export const INTEGRATION_NORTHFLANK_API_URL = "https://api.northflank.com"; +export const INTEGRATION_HASURA_CLOUD_API_URL = "https://data.pro.hasura.io/v1/graphql"; -export const INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com" +export const INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com"; export const INTEGRATION_GCP_SECRET_MANAGER_URL = `https://${INTEGRATION_GCP_SECRET_MANAGER_SERVICE_NAME}`; export const INTEGRATION_GCP_SERVICE_USAGE_URL = "https://serviceusage.googleapis.com"; -export const INTEGRATION_GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"; +export const INTEGRATION_GCP_CLOUD_PLATFORM_SCOPE = + "https://www.googleapis.com/auth/cloud-platform"; export const getIntegrationOptions = async () => { - const INTEGRATION_OPTIONS = [ - { - name: "Heroku", - slug: "heroku", - image: "Heroku.png", - isAvailable: true, - type: "oauth", - clientId: await getClientIdHeroku(), - docsLink: "", - }, - { - name: "Vercel", - slug: "vercel", - image: "Vercel.png", - isAvailable: true, - type: "oauth", - clientId: "", - clientSlug: await getClientSlugVercel(), - docsLink: "", - }, - { - name: "Netlify", - slug: "netlify", - image: "Netlify.png", - isAvailable: true, - type: "oauth", - clientId: await getClientIdNetlify(), - docsLink: "", - }, - { - name: "GitHub", - slug: "github", - image: "GitHub.png", - isAvailable: true, - type: "oauth", - clientId: await getClientIdGitHub(), - docsLink: "", - }, - { - name: "Render", - slug: "render", - image: "Render.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "Railway", - slug: "railway", - image: "Railway.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "Fly.io", - slug: "flyio", - image: "Flyio.svg", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "AWS Parameter Store", - slug: "aws-parameter-store", - image: "Amazon Web Services.png", - isAvailable: true, - type: "custom", - clientId: "", - docsLink: "", - }, - { - name: "Laravel Forge", - slug: "laravel-forge", - image: "Laravel Forge.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "AWS Secrets Manager", - slug: "aws-secret-manager", - image: "Amazon Web Services.png", - isAvailable: true, - type: "custom", - clientId: "", - docsLink: "", - }, - { - name: "Azure Key Vault", - slug: "azure-key-vault", - image: "Microsoft Azure.png", - isAvailable: true, - type: "oauth", - clientId: await getClientIdAzure(), - docsLink: "", - }, - { - name: "Circle CI", - slug: "circleci", - image: "Circle CI.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "GitLab", - slug: "gitlab", - image: "GitLab.png", - isAvailable: true, - type: "custom", - clientId: await getClientIdGitLab(), - docsLink: "", - }, - { - name: "Terraform Cloud", - slug: "terraform-cloud", - image: "Terraform Cloud.png", - isAvailable: true, - type: "pat", - cliendId: "", - docsLink: "", - }, - { - name: "Travis CI", - slug: "travisci", - image: "Travis CI.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "TeamCity", - slug: "teamcity", - image: "TeamCity.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "Supabase", - slug: "supabase", - image: "Supabase.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "Checkly", - slug: "checkly", - image: "Checkly.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "Qovery", - slug: "qovery", - image: "Qovery.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "HashiCorp Vault", - slug: "hashicorp-vault", - image: "Vault.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "GCP Secret Manager", - slug: "gcp-secret-manager", - image: "Google Cloud Platform.png", - isAvailable: true, - type: "oauth", - clientId: await getClientIdGCPSecretManager(), - docsLink: "" - }, - { - name: "Cloudflare Pages", - slug: "cloudflare-pages", - image: "Cloudflare.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "" - }, - { - name: "BitBucket", - slug: "bitbucket", - image: "BitBucket.png", - isAvailable: true, - type: "oauth", - clientId: await getClientIdBitBucket(), - docsLink: "" - }, - { - name: "Codefresh", - slug: "codefresh", - image: "Codefresh.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "Windmill", - slug: "windmill", - image: "Windmill.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "Digital Ocean App Platform", - slug: "digital-ocean-app-platform", - image: "Digital Ocean.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "Cloud 66", - slug: "cloud-66", - image: "Cloud 66.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "", - }, - { - name: "Northflank", - slug: "northflank", - image: "Northflank.png", - isAvailable: true, - type: "pat", - clientId: "", - docsLink: "" - }, - ] - - return INTEGRATION_OPTIONS; -} + const INTEGRATION_OPTIONS = [ + { + name: "Heroku", + slug: "heroku", + image: "Heroku.png", + isAvailable: true, + type: "oauth", + clientId: await getClientIdHeroku(), + docsLink: "" + }, + { + name: "Vercel", + slug: "vercel", + image: "Vercel.png", + isAvailable: true, + type: "oauth", + clientId: "", + clientSlug: await getClientSlugVercel(), + docsLink: "" + }, + { + name: "Netlify", + slug: "netlify", + image: "Netlify.png", + isAvailable: true, + type: "oauth", + clientId: await getClientIdNetlify(), + docsLink: "" + }, + { + name: "GitHub", + slug: "github", + image: "GitHub.png", + isAvailable: true, + type: "oauth", + clientId: await getClientIdGitHub(), + docsLink: "" + }, + { + name: "Render", + slug: "render", + image: "Render.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Railway", + slug: "railway", + image: "Railway.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Fly.io", + slug: "flyio", + image: "Flyio.svg", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "AWS Parameter Store", + slug: "aws-parameter-store", + image: "Amazon Web Services.png", + isAvailable: true, + type: "custom", + clientId: "", + docsLink: "" + }, + { + name: "Laravel Forge", + slug: "laravel-forge", + image: "Laravel Forge.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "AWS Secrets Manager", + slug: "aws-secret-manager", + image: "Amazon Web Services.png", + isAvailable: true, + type: "custom", + clientId: "", + docsLink: "" + }, + { + name: "Azure Key Vault", + slug: "azure-key-vault", + image: "Microsoft Azure.png", + isAvailable: true, + type: "oauth", + clientId: await getClientIdAzure(), + docsLink: "" + }, + { + name: "Circle CI", + slug: "circleci", + image: "Circle CI.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "GitLab", + slug: "gitlab", + image: "GitLab.png", + isAvailable: true, + type: "custom", + clientId: await getClientIdGitLab(), + docsLink: "" + }, + { + name: "Terraform Cloud", + slug: "terraform-cloud", + image: "Terraform Cloud.png", + isAvailable: true, + type: "pat", + cliendId: "", + docsLink: "" + }, + { + name: "Travis CI", + slug: "travisci", + image: "Travis CI.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "TeamCity", + slug: "teamcity", + image: "TeamCity.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Supabase", + slug: "supabase", + image: "Supabase.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Checkly", + slug: "checkly", + image: "Checkly.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Qovery", + slug: "qovery", + image: "Qovery.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "HashiCorp Vault", + slug: "hashicorp-vault", + image: "Vault.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "GCP Secret Manager", + slug: "gcp-secret-manager", + image: "Google Cloud Platform.png", + isAvailable: true, + type: "oauth", + clientId: await getClientIdGCPSecretManager(), + docsLink: "" + }, + { + name: "Cloudflare Pages", + slug: "cloudflare-pages", + image: "Cloudflare.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "BitBucket", + slug: "bitbucket", + image: "BitBucket.png", + isAvailable: true, + type: "oauth", + clientId: await getClientIdBitBucket(), + docsLink: "" + }, + { + name: "Codefresh", + slug: "codefresh", + image: "Codefresh.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Windmill", + slug: "windmill", + image: "Windmill.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Digital Ocean App Platform", + slug: "digital-ocean-app-platform", + image: "Digital Ocean.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Cloud 66", + slug: "cloud-66", + image: "Cloud 66.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Northflank", + slug: "northflank", + image: "Northflank.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Hasura Cloud", + slug: "hasura-cloud", + image: "Hasura.svg", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + } + ]; + + return INTEGRATION_OPTIONS; +}; diff --git a/docs/images/integrations/hasura-cloud/integrations-hasura-cloud-auth.png b/docs/images/integrations/hasura-cloud/integrations-hasura-cloud-auth.png new file mode 100644 index 000000000..585f9fcb3 Binary files /dev/null and b/docs/images/integrations/hasura-cloud/integrations-hasura-cloud-auth.png differ diff --git a/docs/images/integrations/hasura-cloud/integrations-hasura-cloud-create.png b/docs/images/integrations/hasura-cloud/integrations-hasura-cloud-create.png new file mode 100644 index 000000000..eca04f08b Binary files /dev/null and b/docs/images/integrations/hasura-cloud/integrations-hasura-cloud-create.png differ diff --git a/docs/images/integrations/hasura-cloud/integrations-hasura-cloud-tokens.png b/docs/images/integrations/hasura-cloud/integrations-hasura-cloud-tokens.png new file mode 100644 index 000000000..6c45c0b19 Binary files /dev/null and b/docs/images/integrations/hasura-cloud/integrations-hasura-cloud-tokens.png differ diff --git a/docs/images/integrations/hasura-cloud/integrations-hasura-cloud.png b/docs/images/integrations/hasura-cloud/integrations-hasura-cloud.png new file mode 100644 index 000000000..855cef3ce Binary files /dev/null and b/docs/images/integrations/hasura-cloud/integrations-hasura-cloud.png differ diff --git a/docs/integrations/cloud/hasura-cloud.mdx b/docs/integrations/cloud/hasura-cloud.mdx new file mode 100644 index 000000000..770852742 --- /dev/null +++ b/docs/integrations/cloud/hasura-cloud.mdx @@ -0,0 +1,36 @@ +--- +title: "Hasura Cloud" +description: "How to sync secrets from Infisical to Hasura Cloud" +--- + +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) + +## Navigate to your project's integrations tab + +![integrations](../../images/integrations.png) + +## Enter your Hasura Cloud Access Token + +Obtain a Hasura Cloud Access Token in My Account > Access Tokens + +![integrations hasura cloud tokens](../../images/integrations/hasura-cloud/integrations-hasura-cloud-tokens.png) + +Press on the Hasura Cloud tile and input your Hasura Cloud access token to grant Infisical access to your Hasura Cloud account. + +![integrations hasura cloud authorization](../../images/integrations/hasura-cloud/integrations-hasura-cloud-auth.png) + + + If this is your project's first cloud integration, then you'll have to grant + Infisical access to your project's environment variables. Although this step + breaks E2EE, it's necessary for Infisical to sync the environment variables to + the cloud platform. + + +## Start integration + +Select which Infisical environment secrets you want to sync to which Hasura Cloud project and press create integration to start syncing secrets to Hasura Cloud. + +![integrations hasura cloud](../../images/integrations/hasura-cloud/integrations-hasura-cloud-create.png) +![integrations hasura cloud](../../images/integrations/hasura-cloud/integrations-hasura-cloud.png) diff --git a/docs/mint.json b/docs/mint.json index dbe122531..92876198d 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -189,9 +189,7 @@ }, { "group": "Integrations", - "pages": [ - "integrations/overview" - ] + "pages": ["integrations/overview"] }, { "group": "Infrastructure Integrations", @@ -221,9 +219,7 @@ }, { "group": "Digital Ocean", - "pages": [ - "integrations/cloud/digital-ocean-app-platform" - ] + "pages": ["integrations/cloud/digital-ocean-app-platform"] }, "integrations/cloud/heroku", "integrations/cloud/vercel", @@ -234,6 +230,7 @@ "integrations/cloud/laravel-forge", "integrations/cloud/supabase", "integrations/cloud/northflank", + "integrations/cloud/hasura-cloud", "integrations/cloud/terraform-cloud", "integrations/cloud/teamcity", "integrations/cloud/cloudflare-pages", @@ -277,9 +274,7 @@ }, { "group": "Build Tool Integrations", - "pages": [ - "integrations/build-tools/gradle" - ] + "pages": ["integrations/build-tools/gradle"] }, { "group": "Overview", diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index 9e1bfb99d..b40c55d55 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -6,74 +6,75 @@ const integrationSlugNameMapping: Mapping = { "azure-key-vault": "Azure Key Vault", "aws-parameter-store": "AWS Parameter Store", "aws-secret-manager": "AWS Secrets Manager", - "heroku": "Heroku", - "vercel": "Vercel", - "netlify": "Netlify", - "github": "GitHub", - "gitlab": "GitLab", - "render": "Render", + heroku: "Heroku", + vercel: "Vercel", + netlify: "Netlify", + github: "GitHub", + gitlab: "GitLab", + render: "Render", "laravel-forge": "Laravel Forge", - "railway": "Railway", - "flyio": "Fly.io", - "circleci": "CircleCI", - "travisci": "TravisCI", - "supabase": "Supabase", - "checkly": "Checkly", - "qovery": "Qovery", + railway: "Railway", + flyio: "Fly.io", + circleci: "CircleCI", + travisci: "TravisCI", + supabase: "Supabase", + checkly: "Checkly", + qovery: "Qovery", "terraform-cloud": "Terraform Cloud", - "teamcity": "TeamCity", + teamcity: "TeamCity", "hashicorp-vault": "Vault", "cloudflare-pages": "Cloudflare Pages", - "codefresh": "Codefresh", + codefresh: "Codefresh", "digital-ocean-app-platform": "Digital Ocean App Platform", - "bitbucket": "BitBucket", + bitbucket: "BitBucket", "cloud-66": "Cloud 66", - "northflank": "Northflank", - "windmill": "Windmill", - "gcp-secret-manager": "GCP Secret Manager" -} + northflank: "Northflank", + windmill: "Windmill", + "gcp-secret-manager": "GCP Secret Manager", + "hasura-cloud": "Hasura Cloud" +}; const envMapping: Mapping = { Development: "dev", Staging: "staging", Production: "prod", - Testing: "test", + Testing: "test" }; const reverseEnvMapping: Mapping = { dev: "Development", staging: "Staging", prod: "Production", - test: "Testing", + test: "Testing" }; const contextNetlifyMapping: Mapping = { - "dev": "Local development", + dev: "Local development", "branch-deploy": "Branch deploys", "deploy-preview": "Deploy Previews", - "production": "Production" -} + production: "Production" +}; const reverseContextNetlifyMapping: Mapping = { "Local development": "dev", "Branch deploys": "branch-deploy", "Deploy Previews": "deploy-preview", - "Production": "production" -} + Production: "production" +}; const plansDev: Mapping = { - "starter": "prod_Mb4ATFT5QAHoPM", - "team": "prod_NEpD2WMXUS2eDn", - "professional": "prod_Mb4CetZ2jE7jdl", - "enterprise": "licence_key_required" -} + starter: "prod_Mb4ATFT5QAHoPM", + team: "prod_NEpD2WMXUS2eDn", + professional: "prod_Mb4CetZ2jE7jdl", + enterprise: "licence_key_required" +}; const plansProd: Mapping = { - "starter": "prod_Mb8oR5XNwyFTul", - "team": "prod_NEp7fAB3UJWK6A", - "professional": "prod_Mb8pUIpA0OUi5N", - "enterprise": "licence_key_required" -} + starter: "prod_Mb8oR5XNwyFTul", + team: "prod_NEp7fAB3UJWK6A", + professional: "prod_Mb8pUIpA0OUi5N", + enterprise: "licence_key_required" +}; const plans = plansProd || plansDev; @@ -83,4 +84,5 @@ export { integrationSlugNameMapping, plans, reverseContextNetlifyMapping, - reverseEnvMapping} + reverseEnvMapping +}; diff --git a/frontend/public/images/integrations/Hasura.svg b/frontend/public/images/integrations/Hasura.svg new file mode 100644 index 000000000..732f821cb --- /dev/null +++ b/frontend/public/images/integrations/Hasura.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/pages/integrations/flyio/authorize.tsx b/frontend/src/pages/integrations/flyio/authorize.tsx index 277e62167..fe6d8022e 100644 --- a/frontend/src/pages/integrations/flyio/authorize.tsx +++ b/frontend/src/pages/integrations/flyio/authorize.tsx @@ -41,7 +41,6 @@ export default function FlyioAuthorizeIntegrationPage() { const onFormSubmit = async ({ accessToken }: FormData) => { - console.log("onFormSubmit accessToken: ", accessToken); try { setIsLoading(true); diff --git a/frontend/src/pages/integrations/hasura-cloud/authorize.tsx b/frontend/src/pages/integrations/hasura-cloud/authorize.tsx new file mode 100644 index 000000000..d2cff62ba --- /dev/null +++ b/frontend/src/pages/integrations/hasura-cloud/authorize.tsx @@ -0,0 +1,116 @@ +import { useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import Head from "next/head"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; +import { useSaveIntegrationAccessToken } from "@app/hooks/api"; + +const schema = yup.object({ + accessToken: yup.string().trim().required("Hasura Cloud Access Token is required") +}); + +type FormData = yup.InferType; + +const APP_NAME = "Hasura Cloud"; +export default function HasuraCloudAuthorizeIntegrationPage() { + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + const { mutateAsync } = useSaveIntegrationAccessToken(); + + const { control, handleSubmit } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + accessToken: "" + } + }); + + const onFormSubmit = async ({ accessToken }: FormData) => { + try { + setIsLoading(true); + + const integrationAuth = await mutateAsync({ + workspaceId: localStorage.getItem("projectData.id"), + integration: "hasura-cloud", + accessToken + }); + + setIsLoading(false); + router.push(`/integrations/hasura-cloud/create?integrationAuthId=${integrationAuth._id}`); + } catch (err) { + setIsLoading(false); + console.error(err); + } + }; + return ( +
+ + Authorize {APP_NAME} Integration + + + + +
+
+ {`${APP_NAME} +
+ {APP_NAME} Integration + + +
+ + Docs + +
+
+ +
+
+
+ ( + + + + )} + /> + + +
+
+ ); +} + +HasuraCloudAuthorizeIntegrationPage.requireAuth = true; diff --git a/frontend/src/pages/integrations/hasura-cloud/create.tsx b/frontend/src/pages/integrations/hasura-cloud/create.tsx new file mode 100644 index 000000000..d104f6df7 --- /dev/null +++ b/frontend/src/pages/integrations/hasura-cloud/create.tsx @@ -0,0 +1,228 @@ +import { Controller, useForm } from "react-hook-form"; +import Head from "next/head"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { yupResolver } from "@hookform/resolvers/yup"; +import queryString from "query-string"; +import * as yup from "yup"; + +import { + Button, + Card, + CardTitle, + FormControl, + Input, + Select, + SelectItem +} from "@app/components/v2"; +import { useCreateIntegration } from "@app/hooks/api"; +import { + useGetIntegrationAuthApps, + useGetIntegrationAuthById +} from "@app/hooks/api/integrationAuth"; +import { useGetWorkspaceById } from "@app/hooks/api/workspace"; + +const schema = yup.object({ + secretPath: yup.string().trim().required("Secret path is required"), + sourceEnvironment: yup.string().trim().required("Project environment is required"), + appId: yup.string().trim().required("Hasura Cloud project is required") +}); + +type FormData = yup.InferType; + +const APP_NAME = "Hasura Cloud"; +export default function HasuraCloudCreateIntegrationPage() { + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + resolver: yupResolver(schema) + }); + const router = useRouter(); + const { mutateAsync } = useCreateIntegration(); + const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); + + const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? ""); + const { data: integrationAuth, isLoading: isIntegrationAuthLoading } = useGetIntegrationAuthById( + (integrationAuthId as string) ?? "" + ); + + const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } = + useGetIntegrationAuthApps({ + integrationAuthId: (integrationAuthId as string) ?? "" + }); + + const onFormSubmit = async ({ secretPath, sourceEnvironment, appId }: FormData) => { + try { + if (!integrationAuth?._id) return; + + const app = integrationAuthApps?.find((data) => data.appId === appId); + await mutateAsync({ + integrationAuthId: integrationAuth?._id, + isActive: true, + sourceEnvironment, + secretPath, + appId, + app: app?.name + }); + + router.push(`/integrations/${localStorage.getItem("projectData.id")}`); + } catch (err) { + console.error(err); + } + }; + + return integrationAuth && workspace && integrationAuthApps ? ( +
+ + Set Up {APP_NAME} Integration + + + + +
+
+ {`${APP_NAME} +
+ {APP_NAME} Integration + + +
+ + Docs + +
+
+ +
+
+ +
+ ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + + +
+
+ ) : ( +
+ + Set Up {APP_NAME} Integration + + + {isIntegrationAuthLoading || isIntegrationAuthAppsLoading ? ( + infisical loading indicator + ) : ( +
+ +

+ Something went wrong. Please contact{" "} + + support@infisical.com + {" "} + if the issue persists. +

+
+ )} +
+ ); +} + +HasuraCloudCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx index c9fa6c585..5e92ec4ee 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx @@ -1,6 +1,6 @@ import crypto from "crypto"; -import { TCloudIntegration,UserWsKeyPair } from "@app/hooks/api/types"; +import { TCloudIntegration, UserWsKeyPair } from "@app/hooks/api/types"; import { decryptAssymmetric, @@ -32,11 +32,10 @@ export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) = export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => { try { - // 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 "gcp-secret-manager": @@ -123,6 +122,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => case "teamcity": link = `${window.location.origin}/integrations/teamcity/authorize`; break; + case "hasura-cloud": + link = `${window.location.origin}/integrations/hasura-cloud/authorize`; + break; default: break; } @@ -130,7 +132,6 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => if (link !== "") { window.location.assign(link); } - } catch (err) { console.error(err); }