From d9ab38c59078eb082c784703c6328ff4dfc6b512 Mon Sep 17 00:00:00 2001 From: Chukwunonso Frank Date: Tue, 4 Jul 2023 22:52:23 +0100 Subject: [PATCH 1/9] chore: resolve merge conflicts --- .husky/pre-commit | 1 - backend/src/integrations/apps.ts | 47 +++++- backend/src/integrations/sync.ts | 149 ++++++++++++++---- backend/src/models/integration.ts | 9 +- backend/src/models/integrationAuth.ts | 8 +- backend/src/variables/integration.ts | 46 ++++-- .../integrations/northflank/authorize.tsx | 64 ++++++++ .../pages/integrations/northflank/create.tsx | 139 ++++++++++++++++ 8 files changed, 404 insertions(+), 59 deletions(-) create mode 100644 frontend/src/pages/integrations/northflank/authorize.tsx create mode 100644 frontend/src/pages/integrations/northflank/create.tsx diff --git a/.husky/pre-commit b/.husky/pre-commit index e235e5d799..4f18d25215 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,3 @@ - #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index bc8f01a80c..3ac9e77a5e 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -9,17 +9,19 @@ import { INTEGRATION_CHECKLY_API_URL, INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI_API_URL, + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CLOUDFLARE_PAGES_API_URL, INTEGRATION_FLYIO, INTEGRATION_FLYIO_API_URL, INTEGRATION_GITHUB, INTEGRATION_GITLAB, - INTEGRATION_CLOUDFLARE_PAGES, - INTEGRATION_CLOUDFLARE_PAGES_API_URL, INTEGRATION_GITLAB_API_URL, INTEGRATION_HEROKU, INTEGRATION_HEROKU_API_URL, INTEGRATION_NETLIFY, INTEGRATION_NETLIFY_API_URL, + INTEGRATION_NORTHFLANK, + INTEGRATION_NORTHFLANK_API_URL, INTEGRATION_RAILWAY, INTEGRATION_RAILWAY_API_URL, INTEGRATION_RENDER, @@ -29,7 +31,7 @@ import { INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI_API_URL, INTEGRATION_VERCEL, - INTEGRATION_VERCEL_API_URL, + INTEGRATION_VERCEL_API_URL } from "../variables"; interface App { @@ -135,7 +137,12 @@ const getApps = async ({ apps = await getAppsCloudflarePages({ accessToken, accountId: accessId - }) + }); + break; + case INTEGRATION_NORTHFLANK: + apps = await getAppsNorthflank({ + accessToken, + }); break; } @@ -678,5 +685,37 @@ const getAppsCloudflarePages = async ({ }); return apps; } + /* Return list of projects for Northflank integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for Northflank API + * @returns {Object[]} apps - names of Northflank apps + * @returns {String} apps.name - name of Northflank app + */ +const getAppsNorthflank = async ({ accessToken }: { accessToken: string }) => { + const { + data: { + data: { + projects + } + } + } = await standardRequest.get( + `${INTEGRATION_NORTHFLANK_API_URL}/v1/projects`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ); + + const apps = projects.map((a: any) => { + return { + name: a.name, + appId: a.id, + }; + }); + + return apps; +}; export { getApps }; diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 0cc3a94386..a49b2aaea2 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -18,6 +18,8 @@ import { INTEGRATION_CHECKLY_API_URL, INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI_API_URL, + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CLOUDFLARE_PAGES_API_URL, INTEGRATION_FLYIO, INTEGRATION_FLYIO_API_URL, INTEGRATION_GITHUB, @@ -28,14 +30,14 @@ import { INTEGRATION_HEROKU_API_URL, INTEGRATION_NETLIFY, INTEGRATION_NETLIFY_API_URL, + INTEGRATION_NORTHFLANK, + INTEGRATION_NORTHFLANK_API_URL, INTEGRATION_RAILWAY, INTEGRATION_RAILWAY_API_URL, INTEGRATION_RENDER, INTEGRATION_RENDER_API_URL, INTEGRATION_SUPABASE, INTEGRATION_SUPABASE_API_URL, - INTEGRATION_CLOUDFLARE_PAGES, - INTEGRATION_CLOUDFLARE_PAGES_API_URL, INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI_API_URL, INTEGRATION_VERCEL, @@ -168,34 +170,6 @@ const syncSecrets = async ({ accessToken, }); break; - case INTEGRATION_FLYIO: - await syncSecretsFlyio({ - integration, - secrets, - accessToken, - }); - break; - case INTEGRATION_CIRCLECI: - await syncSecretsCircleCI({ - integration, - secrets, - accessToken, - }); - break; - case INTEGRATION_TRAVISCI: - await syncSecretsTravisCI({ - integration, - secrets, - accessToken, - }); - break; - case INTEGRATION_SUPABASE: - await syncSecretsSupabase({ - integration, - secrets, - accessToken, - }); - break; case INTEGRATION_CHECKLY: await syncSecretsCheckly({ integration, @@ -220,6 +194,13 @@ const syncSecrets = async ({ accessToken }); break; + case INTEGRATION_NORTHFLANK: + await syncSecretsNorthflank({ + integration, + secrets, + accessToken + }); + break; } }; @@ -1874,7 +1855,7 @@ const syncSecretsCloudflarePages = async ({ } ) ) - .data.result['deployment_configs'][integration.targetEnvironment]['env_vars']; + .data.result["deployment_configs"][integration.targetEnvironment]["env_vars"]; // copy the secrets object, so we can set deleted keys to null const secretsObj: any = {...secrets}; @@ -1912,5 +1893,111 @@ const syncSecretsCloudflarePages = async ({ } ); } + /* Sync/push [secrets] to Northflank + * @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 Northflank integration + */ +const syncSecretsNorthflank = async ({ + integration, + secrets, + accessToken +}: { + integration: IIntegration; + secrets: any; + accessToken: string; +}) => { + +// secrets: { +// secretGroupID: 'some_id', +// secretGroupName: 'some_name', +// data: {} +// } + + const { + data: { + secrets: getSecretsRes + } + } = await standardRequest.get( + `${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${integration.appId}/secrets`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + const secretGroups = getSecretsRes.map((group: any) => { + return { + id: group.id, + name: group.name + }; + }) + + for await (const group of secretGroups) { + if (group.id === secrets.secretGroupID) { + // add secret to existing group + let { + data: { + secrets: { + variables + } + } + } = await standardRequest.get( + `${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${integration.appId}/secrets/${secrets.secretGroupID}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + variables = { ...secrets.data } + + const modifiedFormatForSecretInjection = { + secrets: { + variables + } + } + + await standardRequest.post( + `${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${integration.appId}/secrets/${secrets.secretGroupID}`, + modifiedFormatForSecretInjection, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } else { + // create new secret group + const modifiedFormatForSecretInjection = { + name: secrets.secretGroupName, + secretType: "environment", + priority: 10, + secrets: { + variables: secrets.data + } + }; + + await standardRequest.post( + `${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${integration.appId}/secrets`, + modifiedFormatForSecretInjection, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + } + + // TODO:: figure out delete business logic for secret groups +}; export { syncSecrets }; diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index d94cc9faa3..f952f83b2c 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -5,18 +5,19 @@ import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_CHECKLY, INTEGRATION_CIRCLECI, + INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_FLYIO, INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_HASHICORP_VAULT, INTEGRATION_HEROKU, INTEGRATION_NETLIFY, + INTEGRATION_NORTHFLANK, INTEGRATION_RAILWAY, INTEGRATION_RENDER, INTEGRATION_SUPABASE, - INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_TRAVISCI, - INTEGRATION_VERCEL, + INTEGRATION_VERCEL } from "../variables"; export interface IIntegration { @@ -52,7 +53,8 @@ export interface IIntegration { | "supabase" | "checkly" | "hashicorp-vault" - | "cloudflare-pages"; + | "cloudflare-pages" + | "northflank"; integrationAuth: Types.ObjectId; } @@ -141,6 +143,7 @@ const integrationSchema = new Schema( INTEGRATION_CHECKLY, INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_NORTHFLANK ], required: true, }, diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index 2d56c2063d..790134151a 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -7,24 +7,25 @@ import { INTEGRATION_AWS_SECRET_MANAGER, INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_CIRCLECI, + INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_FLYIO, INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_HASHICORP_VAULT, INTEGRATION_HEROKU, INTEGRATION_NETLIFY, + INTEGRATION_NORTHFLANK, INTEGRATION_RAILWAY, INTEGRATION_RENDER, INTEGRATION_SUPABASE, - INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_TRAVISCI, - INTEGRATION_VERCEL, + INTEGRATION_VERCEL } from "../variables"; export interface IIntegrationAuth extends Document { _id: Types.ObjectId; workspace: Types.ObjectId; - integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'railway' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'supabase' | 'aws-parameter-store' | 'aws-secret-manager' | 'checkly' | 'cloudflare-pages'; + integration: "heroku" | "vercel" | "netlify" | "github" | "gitlab" | "render" | "railway" | "flyio" | "azure-key-vault" | "circleci" | "travisci" | "supabase" | "aws-parameter-store" | "aws-secret-manager" | "checkly" | "cloudflare-pages" | "northflank"; teamId: string; accountId: string; url: string; @@ -69,6 +70,7 @@ const integrationAuthSchema = new Schema( INTEGRATION_SUPABASE, INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_NORTHFLANK ], required: true, }, diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index c87d2eb976..4ceaa9ed71 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -21,10 +21,11 @@ export const INTEGRATION_RAILWAY = "railway"; export const INTEGRATION_FLYIO = "flyio"; export const INTEGRATION_CIRCLECI = "circleci"; export const INTEGRATION_TRAVISCI = "travisci"; -export const INTEGRATION_SUPABASE = 'supabase'; -export const INTEGRATION_CHECKLY = 'checkly'; -export const INTEGRATION_HASHICORP_VAULT = 'hashicorp-vault'; -export const INTEGRATION_CLOUDFLARE_PAGES = 'cloudflare-pages'; +export const INTEGRATION_SUPABASE = "supabase"; +export const INTEGRATION_CHECKLY = "checkly"; +export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault"; +export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages"; +export const INTEGRATION_NORTHFLANK = "northflank"; export const INTEGRATION_SET = new Set([ INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU, @@ -39,7 +40,8 @@ export const INTEGRATION_SET = new Set([ INTEGRATION_SUPABASE, INTEGRATION_CHECKLY, INTEGRATION_HASHICORP_VAULT, - INTEGRATION_CLOUDFLARE_PAGES + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_NORTHFLANK ]); // integration types @@ -65,9 +67,10 @@ export const INTEGRATION_RAILWAY_API_URL = "https://backboard.railway.app/graphq export const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql"; export const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api"; export const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com"; -export const INTEGRATION_SUPABASE_API_URL = 'https://api.supabase.com'; -export const INTEGRATION_CHECKLY_API_URL = 'https://api.checklyhq.com'; -export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = 'https://api.cloudflare.com'; +export const INTEGRATION_SUPABASE_API_URL = "https://api.supabase.com"; +export const INTEGRATION_CHECKLY_API_URL = "https://api.checklyhq.com"; +export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com"; +export const INTEGRATION_NORTHFLANK_API_URL = "https://api.northflank.com"; export const getIntegrationOptions = async () => { const INTEGRATION_OPTIONS = [ @@ -221,18 +224,27 @@ export const getIntegrationOptions = async () => { slug: "gcp", image: "Google Cloud Platform.png", isAvailable: false, - type: '', - clientId: '', - docsLink: '' + type: "", + clientId: "", + docsLink: "" }, { - name: 'Cloudflare Pages', - slug: 'cloudflare-pages', - image: 'Cloudflare.png', + name: "Cloudflare Pages", + slug: "cloudflare-pages", + image: "Cloudflare.png", isAvailable: true, - type: 'pat', - clientId: '', - docsLink: '' + type: "pat", + clientId: "", + docsLink: "" + }, + { + name: "Northflank", + slug: "northflank", + image: "Northflank.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" } ] diff --git a/frontend/src/pages/integrations/northflank/authorize.tsx b/frontend/src/pages/integrations/northflank/authorize.tsx new file mode 100644 index 0000000000..8e2baa3cb1 --- /dev/null +++ b/frontend/src/pages/integrations/northflank/authorize.tsx @@ -0,0 +1,64 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; + +import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; +import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; + +export default function NorthflankCreateIntegrationPage() { + const router = useRouter(); + const [apiKey, setApiKey] = useState(""); + const [apiKeyErrorText, setApiKeyErrorText] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + const handleButtonClick = async () => { + try { + setApiKeyErrorText(""); + if (apiKey.length === 0) { + setApiKeyErrorText("API Key cannot be blank"); + return; + } + + setIsLoading(true); + + const integrationAuth = await saveIntegrationAccessToken({ + workspaceId: localStorage.getItem("projectData.id"), + integration: "northflank", + accessToken: apiKey, + accessId: null, + url: null, + namespace: null + }); + + setIsLoading(false); + + router.push(`/integrations/northflank/create?integrationAuthId=${integrationAuth._id}`); + } catch (err) { + console.error(err); + } + }; + + return ( +
+ + Northflank Integration + + setApiKey(e.target.value)} /> + + + +
+ ); +} + +NorthflankCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/pages/integrations/northflank/create.tsx b/frontend/src/pages/integrations/northflank/create.tsx new file mode 100644 index 0000000000..12e4b1e952 --- /dev/null +++ b/frontend/src/pages/integrations/northflank/create.tsx @@ -0,0 +1,139 @@ +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +import queryString from "query-string"; + +import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "../../../components/v2"; +import { + useGetIntegrationAuthApps, + useGetIntegrationAuthById +} from "../../../hooks/api/integrationAuth"; +import { useGetWorkspaceById } from "../../../hooks/api/workspace"; +import createIntegration from "../../api/integrations/createIntegration"; + +export default function NorthflankCreateIntegrationPage() { + const router = useRouter(); + + const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); + + const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? ""); + const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); + const { data: integrationAuthApps } = useGetIntegrationAuthApps({ + integrationAuthId: (integrationAuthId as string) ?? "" + }); + + const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); + const [targetApp, setTargetApp] = useState(""); + + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (workspace) { + setSelectedSourceEnvironment(workspace.environments[0].slug); + } + }, [workspace]); + + useEffect(() => { + if (integrationAuthApps) { + if (integrationAuthApps.length > 0) { + setTargetApp(integrationAuthApps[0].name); + } else { + setTargetApp("none"); + } + } + }, [integrationAuthApps]); + + const handleButtonClick = async () => { + try { + if (!integrationAuth?._id) return; + + setIsLoading(true); + + await createIntegration({ + integrationAuthId: integrationAuth?._id, + isActive: true, + app: targetApp, + appId: + integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp) + ?.appId ?? null, + sourceEnvironment: selectedSourceEnvironment, + targetEnvironment: null, + targetEnvironmentId: null, + targetService: null, + targetServiceId: null, + owner: null, + path: null, + region: null + }); + + setIsLoading(false); + + router.push(`/integrations/${localStorage.getItem("projectData.id")}`); + } catch (err) { + console.error(err); + } + }; + + return integrationAuth && + workspace && + selectedSourceEnvironment && + integrationAuthApps && + targetApp ? ( +
+ + Northflank Integration + + + + + + + + +
+ ) : ( +
+ ); +} + +NorthflankCreateIntegrationPage.requireAuth = true; From 91e172fd79b764ec1cf6bdadf107f6156f229048 Mon Sep 17 00:00:00 2001 From: Chukwunonso Frank Date: Sun, 9 Jul 2023 16:18:58 +0100 Subject: [PATCH 2/9] add Northflank specific create.tsx file --- .../pages/integrations/northflank/create.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/integrations/northflank/create.tsx b/frontend/src/pages/integrations/northflank/create.tsx index 12e4b1e952..e16803bb3a 100644 --- a/frontend/src/pages/integrations/northflank/create.tsx +++ b/frontend/src/pages/integrations/northflank/create.tsx @@ -2,7 +2,15 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import queryString from "query-string"; -import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "../../../components/v2"; +import { + Button, + Card, + CardTitle, + FormControl, + Input, + Select, + SelectItem +} from "../../../components/v2"; import { useGetIntegrationAuthApps, useGetIntegrationAuthById @@ -22,6 +30,7 @@ export default function NorthflankCreateIntegrationPage() { }); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); + const [secretPath, setSecretPath] = useState("/"); const [targetApp, setTargetApp] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -62,7 +71,8 @@ export default function NorthflankCreateIntegrationPage() { targetServiceId: null, owner: null, path: null, - region: null + region: null, + secretPath }); setIsLoading(false); @@ -97,6 +107,13 @@ export default function NorthflankCreateIntegrationPage() { ))} + + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + + + + diff --git a/package.json b/package.json index e85b464778..80f12205e6 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ ] }, "devDependencies": { - "@types/picomatch": "^2.3.0", "eslint": "^8.29.0", "husky": "^8.0.2" } From bc9d6253be41859d5702f375a6e1389a13cebcb1 Mon Sep 17 00:00:00 2001 From: Chukwunonso Frank Date: Wed, 26 Jul 2023 21:19:02 +0100 Subject: [PATCH 8/9] change isDisabled criteria for Create Integration button --- frontend/src/pages/integrations/northflank/create.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/integrations/northflank/create.tsx b/frontend/src/pages/integrations/northflank/create.tsx index adead79fe3..cf6de64e55 100644 --- a/frontend/src/pages/integrations/northflank/create.tsx +++ b/frontend/src/pages/integrations/northflank/create.tsx @@ -183,7 +183,7 @@ export default function NorthflankCreateIntegrationPage() { color="mineshaft" className="mt-4" isLoading={isLoading} - isDisabled={secretGroupList === 0} + isDisabled={secretGroupList.length === 0} > Create Integration From 980a578bd5c48eabd57dfe548f89ba1874069173 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 27 Jul 2023 14:52:52 +0700 Subject: [PATCH 9/9] Revise Northflank integration --- .../v1/integrationAuthController.ts | 75 +++++++++++ .../controllers/v1/integrationController.ts | 4 +- backend/src/integrations/apps.ts | 31 +---- backend/src/integrations/sync.ts | 19 ++- backend/src/models/integration.ts | 8 +- backend/src/routes/v1/integration.ts | 1 - backend/src/routes/v1/integrationAuth.ts | 14 ++ .../src/hooks/api/integrationAuth/index.tsx | 4 +- .../src/hooks/api/integrationAuth/queries.tsx | 60 ++++++++- .../src/hooks/api/integrationAuth/types.ts | 5 + .../api/integrations/createIntegration.ts | 3 - .../pages/integrations/northflank/create.tsx | 125 +++++++++--------- 12 files changed, 232 insertions(+), 117 deletions(-) diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index 88968a2727..43d223ad65 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -8,6 +8,7 @@ import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, INTEGRATION_BITBUCKET_API_URL, + INTEGRATION_NORTHFLANK_API_URL, INTEGRATION_RAILWAY_API_URL, INTEGRATION_SET, INTEGRATION_VERCEL_API_URL, @@ -445,6 +446,79 @@ export const getIntegrationAuthBitBucketWorkspaces = async (req: Request, res: R }); }; +/** + * Return list of secret groups for Northflank project with id [appId] + * @param req + * @param res + * @returns + */ +export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res: Response) => { + const appId = req.query.appId as string; + + interface NorthflankSecretGroup { + id: string; + name: string; + description: string; + priority: number; + projectId: string; + } + + interface SecretGroup { + name: string; + groupId: string; + } + + const secretGroups: SecretGroup[] = []; + + if (appId && appId !== "") { + let page = 1; + const perPage = 10; + let hasMorePages = true; + + while(hasMorePages) { + const params = new URLSearchParams({ + page: String(page), + per_page: String(perPage), + filter: "all", + }); + + const { + data: { + data: { + secrets + } + } + } = await standardRequest.get<{ data: { secrets: NorthflankSecretGroup[] }}>( + `${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${appId}/secrets`, + { + params, + headers: { + Authorization: `Bearer ${req.accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ); + + secrets.forEach((a: any) => { + secretGroups.push({ + name: a.name, + groupId: a.id + }); + }); + + if (secrets.length < perPage) { + hasMorePages = false; + } + + page++; + } + } + + return res.status(200).send({ + secretGroups + }); +} + /** * Delete integration authorization with id [integrationAuthId] * @param req @@ -461,3 +535,4 @@ export const deleteIntegrationAuth = async (req: Request, res: Response) => { integrationAuth }); }; + diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts index 431b9f80f2..91b32c805a 100644 --- a/backend/src/controllers/v1/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -27,8 +27,7 @@ export const createIntegration = async (req: Request, res: Response) => { owner, path, region, - secretPath, - secretGroup + secretPath } = req.body; const folders = await Folder.findOne({ @@ -62,7 +61,6 @@ export const createIntegration = async (req: Request, res: Response) => { path, region, secretPath, - secretGroup, integration: req.integrationAuth.integration, integrationAuth: new Types.ObjectId(integrationAuthId) }).save(); diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index d2a262b177..fbbc1b7591 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -877,7 +877,8 @@ const getAppsBitBucket = async ({ }); return apps; } - /* Return list of projects for Northflank integration + +/** Return list of projects for Northflank integration * @param {Object} obj * @param {String} obj.accessToken - access token for Northflank API * @returns {Object[]} apps - names of Northflank apps @@ -903,36 +904,10 @@ const getAppsNorthflank = async ({ accessToken }: { accessToken: string }) => { const apps = projects.map((a: any) => { return { name: a.name, - appId: a.id, - secretGroups: [] + appId: a.id }; }); - for (let i = 0; i < apps.length; i++) { - const appName = apps[i].name; - const { - data: { - data: { - secrets - } - } - } = await standardRequest.get( - `${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${appName}/secrets`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, - } - ); - - const secretGroups = secrets.map((a: any) => { - return a.id - }); - - apps[i].secretGroups = secretGroups - } - return apps; }; diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 715e310250..8839c44497 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -2375,7 +2375,7 @@ const syncSecretsCloud66 = async ({ } }; - /* Sync/push [secrets] to Northflank +/** Sync/push [secrets] to Northflank * @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) @@ -2390,16 +2390,13 @@ const syncSecretsNorthflank = async ({ secrets: any; accessToken: string; }) => { - - const modifiedFormatForSecretInjection = { - secrets: { - variables: secrets - } - } - - await standardRequest.post( - `${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${integration.appId}/secrets/${integration.secretGroup}`, - modifiedFormatForSecretInjection, + await standardRequest.patch( + `${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${integration.appId}/secrets/${integration.targetServiceId}`, + { + secrets: { + variables: secrets + } + }, { headers: { Authorization: `Bearer ${accessToken}`, diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index 8608aa964d..d56c603e5c 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -42,7 +42,6 @@ export interface IIntegration { path: string; region: string; secretPath: string; - secretGroup: string; integration: | "azure-key-vault" | "aws-parameter-store" @@ -175,12 +174,7 @@ const integrationSchema = new Schema( type: String, required: true, default: "/", - }, - secretGroup: { - // northflank-specific service - type: String, - default: null, - }, + } }, { timestamps: true, diff --git a/backend/src/routes/v1/integration.ts b/backend/src/routes/v1/integration.ts index f2685c3f6b..1820f4bb82 100644 --- a/backend/src/routes/v1/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -37,7 +37,6 @@ router.post( body("owner").trim(), body("path").trim(), body("region").trim(), - body("secretGroup").isString().trim(), validateRequest, integrationController.createIntegration ); diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index 673d269d28..4fdc290e7c 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -155,6 +155,20 @@ router.get( integrationAuthController.getIntegrationAuthBitBucketWorkspaces ); +router.get( + "/:integrationAuthId/northflank/secret-groups", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT], + }), + requireIntegrationAuthorizationAuth({ + acceptedRoles: [ADMIN, MEMBER], + }), + param("integrationAuthId").exists().isString(), + query("appId").exists().isString(), + validateRequest, + integrationAuthController.getIntegrationAuthNorthflankSecretGroups +); + router.delete( "/:integrationAuthId", requireAuth({ diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx index a227cdae54..e5e53c808a 100644 --- a/frontend/src/hooks/api/integrationAuth/index.tsx +++ b/frontend/src/hooks/api/integrationAuth/index.tsx @@ -3,8 +3,8 @@ export { useGetIntegrationAuthApps, useGetIntegrationAuthBitBucketWorkspaces, useGetIntegrationAuthById, + useGetIntegrationAuthNorthflankSecretGroups, useGetIntegrationAuthRailwayEnvironments, useGetIntegrationAuthRailwayServices, useGetIntegrationAuthTeams, - useGetIntegrationAuthVercelBranches, -} from "./queries"; + useGetIntegrationAuthVercelBranches} from "./queries"; diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index a5aadd7341..bf9b5c940e 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -3,7 +3,15 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { workspaceKeys } from "../workspace/queries"; -import { App, BitBucketWorkspace, Environment, IntegrationAuth, Service, Team } from "./types"; +import { + App, + BitBucketWorkspace, + Environment, + IntegrationAuth, + NorthflankSecretGroup, + Service, + Team +} from "./types"; const integrationAuthKeys = { getIntegrationAuthById: (integrationAuthId: string) => @@ -19,7 +27,6 @@ const integrationAuthKeys = { integrationAuthId: string; appId: string; }) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const, - getIntegrationAuthRailwayEnvironments: ({ integrationAuthId, appId @@ -36,6 +43,13 @@ const integrationAuthKeys = { }) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const, getIntegrationAuthBitBucketWorkspaces: (integrationAuthId: string) => [{ integrationAuthId }, "integrationAuthBitbucketWorkspaces"] as const, + getIntegrationAuthNorthflankSecretGroups: ({ + integrationAuthId, + appId + }: { + integrationAuthId: string; + appId: string; + }) => [{ integrationAuthId, appId }, "integrationAuthNorthflankSecretGroups"] as const, }; const fetchIntegrationAuthById = async (integrationAuthId: string) => { @@ -148,6 +162,27 @@ const fetchIntegrationAuthBitBucketWorkspaces = async (integrationAuthId: string return workspaces; }; +const fetchIntegrationAuthNorthflankSecretGroups = async ({ + integrationAuthId, + appId +}: { + integrationAuthId: string; + appId: string; +}) => { + const { + data: { secretGroups } + } = await apiRequest.get<{ secretGroups: NorthflankSecretGroup[] }>( + `/api/v1/integration-auth/${integrationAuthId}/northflank/secret-groups`, + { + params: { + appId + } + } + ); + + return secretGroups; +}; + export const useGetIntegrationAuthById = (integrationAuthId: string) => { return useQuery({ queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId), @@ -256,6 +291,27 @@ export const useGetIntegrationAuthBitBucketWorkspaces = (integrationAuthId: stri }); }; +export const useGetIntegrationAuthNorthflankSecretGroups = ({ + integrationAuthId, + appId +}: { + integrationAuthId: string; + appId: string; +}) => { + return useQuery({ + queryKey: integrationAuthKeys.getIntegrationAuthNorthflankSecretGroups({ + integrationAuthId, + appId + }), + queryFn: () => + fetchIntegrationAuthNorthflankSecretGroups({ + integrationAuthId, + appId + }), + enabled: true + }); +}; + export const useDeleteIntegrationAuth = () => { const queryClient = useQueryClient(); diff --git a/frontend/src/hooks/api/integrationAuth/types.ts b/frontend/src/hooks/api/integrationAuth/types.ts index 11851be4dd..1214600e81 100644 --- a/frontend/src/hooks/api/integrationAuth/types.ts +++ b/frontend/src/hooks/api/integrationAuth/types.ts @@ -35,4 +35,9 @@ export type BitBucketWorkspace = { uuid: string; name: string; slug: string; +} + +export type NorthflankSecretGroup = { + name: string; + groupId: string; } \ No newline at end of file diff --git a/frontend/src/pages/api/integrations/createIntegration.ts b/frontend/src/pages/api/integrations/createIntegration.ts index 31a3a4288a..9235e7176c 100644 --- a/frontend/src/pages/api/integrations/createIntegration.ts +++ b/frontend/src/pages/api/integrations/createIntegration.ts @@ -4,7 +4,6 @@ interface Props { integrationAuthId: string; isActive: boolean; secretPath: string; - secretGroup?: string; app: string | null; appId: string | null; sourceEnvironment: string; @@ -36,7 +35,6 @@ const createIntegration = ({ path, region, secretPath, - secretGroup }: Props) => SecurityClient.fetchCall("/api/v1/integration", { method: "POST", @@ -57,7 +55,6 @@ const createIntegration = ({ path, region, secretPath, - secretGroup }) }).then(async (res) => { if (res && res.status === 200) { diff --git a/frontend/src/pages/integrations/northflank/create.tsx b/frontend/src/pages/integrations/northflank/create.tsx index cf6de64e55..4ebcf95ec7 100644 --- a/frontend/src/pages/integrations/northflank/create.tsx +++ b/frontend/src/pages/integrations/northflank/create.tsx @@ -13,7 +13,8 @@ import { } from "../../../components/v2"; import { useGetIntegrationAuthApps, - useGetIntegrationAuthById + useGetIntegrationAuthById, + useGetIntegrationAuthNorthflankSecretGroups } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; import createIntegration from "../../api/integrations/createIntegration"; @@ -21,6 +22,13 @@ import createIntegration from "../../api/integrations/createIntegration"; export default function NorthflankCreateIntegrationPage() { const router = useRouter(); + const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); + const [secretPath, setSecretPath] = useState("/"); + const [targetAppId, setTargetAppId] = useState(""); + const [targetSecretGroupId, setTargetSecretGroupId] = useState(null); + + const [isLoading, setIsLoading] = useState(false); + const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? ""); @@ -28,15 +36,11 @@ export default function NorthflankCreateIntegrationPage() { const { data: integrationAuthApps } = useGetIntegrationAuthApps({ integrationAuthId: (integrationAuthId as string) ?? "" }); - - const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); - const [secretPath, setSecretPath] = useState("/"); - const [targetApp, setTargetApp] = useState(""); - const [secretGroupList, setSecretGroupList] = useState([]); - const [targetSecretGroup, setTargetSecretGroup] = useState(""); - - const [isLoading, setIsLoading] = useState(false); - + const { data: integrationAuthSecretGroups } = useGetIntegrationAuthNorthflankSecretGroups({ + integrationAuthId: (integrationAuthId as string) ?? "", + appId: targetAppId + }); + useEffect(() => { if (workspace) { setSelectedSourceEnvironment(workspace.environments[0].slug); @@ -46,28 +50,28 @@ export default function NorthflankCreateIntegrationPage() { useEffect(() => { if (integrationAuthApps) { if (integrationAuthApps.length > 0) { - setTargetApp(integrationAuthApps[0].name); + // setTargetApp(integrationAuthApps[0].name); + setTargetAppId(integrationAuthApps[0].appId as string); } else { - setTargetApp("none"); + // setTargetApp("none"); + setTargetAppId("none"); } } }, [integrationAuthApps]); - + useEffect(() => { - if (integrationAuthApps) { - if (integrationAuthApps.length > 0) { - const selectedApp = integrationAuthApps?.filter((integrationAuthApp) => integrationAuthApp.name === targetApp); - if (selectedApp.length > 0 && selectedApp[0].secretGroups) { - setSecretGroupList(selectedApp[0].secretGroups); - setTargetSecretGroup(selectedApp[0]?.secretGroups[0]); - } else { - setSecretGroupList([]); - setTargetSecretGroup("none"); - } + if (integrationAuthSecretGroups) { + if (integrationAuthSecretGroups.length > 0) { + // case: project has at least 1 secret group in Northflank + setTargetSecretGroupId(integrationAuthSecretGroups[0].groupId); + } else { + // case: project has no secret groups in Northflank + setTargetSecretGroupId("none"); } } - }, [targetApp]) - + + }, [integrationAuthSecretGroups]); + const handleButtonClick = async () => { try { if (!integrationAuth?._id) return; @@ -77,20 +81,19 @@ export default function NorthflankCreateIntegrationPage() { await createIntegration({ integrationAuthId: integrationAuth?._id, isActive: true, - app: targetApp, - appId: - integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp) - ?.appId ?? null, + app: integrationAuthApps?.find( + (integrationAuthApp) => integrationAuthApp.appId === targetAppId + )?.name ?? null, + appId: targetAppId, sourceEnvironment: selectedSourceEnvironment, targetEnvironment: null, targetEnvironmentId: null, targetService: null, - targetServiceId: null, + targetServiceId: targetSecretGroupId, owner: null, path: null, region: null, - secretPath, - secretGroup: targetSecretGroup + secretPath }); setIsLoading(false); @@ -100,12 +103,12 @@ export default function NorthflankCreateIntegrationPage() { console.error(err); } }; - + return integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && - targetApp ? ( + targetAppId ? (
Northflank Integration @@ -134,15 +137,15 @@ export default function NorthflankCreateIntegrationPage() { - - setTargetSecretGroupId(val)} + className="w-full border border-mineshaft-500" + isDisabled={integrationAuthSecretGroups.length === 0} + > + {integrationAuthSecretGroups.length > 0 ? ( + integrationAuthSecretGroups.map((secretGroup: any) => ( + + {secretGroup.name} + + )) + ) : ( + + No secret groups found - )) - ) : ( - - No secret groups found - - )} - - + )} + + + )}