diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index a21da56aa..d9012c99e 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2607,6 +2607,12 @@ export const SecretSyncs = { siteName: "The name of the Netlify site to sync secrets to.", siteId: "The ID of the Netlify site to sync secrets to.", context: "The Netlify context to sync secrets to." + }, + NORTHFLANK: { + projectId: "The ID of the Northflank project to sync secrets to.", + projectName: "The name of the Northflank project to sync secrets to.", + secretGroupId: "The ID of the Northflank secret group to sync secrets to.", + secretGroupName: "The name of the Northflank secret group to sync secrets to." } } }; diff --git a/backend/src/server/routes/v1/app-connection-routers/northflank-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/northflank-connection-router.ts index 1728a6380..8ca462f3b 100644 --- a/backend/src/server/routes/v1/app-connection-routers/northflank-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/northflank-connection-router.ts @@ -50,4 +50,38 @@ export const registerNorthflankConnectionRouter = async (server: FastifyZodProvi return { projects }; } }); + + server.route({ + method: "GET", + url: `/:connectionId/projects/:projectId/secret-groups`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid(), + projectId: z.string() + }), + response: { + 200: z.object({ + secretGroups: z + .object({ + name: z.string(), + id: z.string() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId, projectId } = req.params; + const secretGroups = await server.services.appConnection.northflank.listSecretGroups( + connectionId, + projectId, + req.permission + ); + return { secretGroups }; + } + }); }; diff --git a/backend/src/services/app-connection/northflank/northflank-connection-fns.ts b/backend/src/services/app-connection/northflank/northflank-connection-fns.ts index 25177bd71..462934800 100644 --- a/backend/src/services/app-connection/northflank/northflank-connection-fns.ts +++ b/backend/src/services/app-connection/northflank/northflank-connection-fns.ts @@ -5,7 +5,12 @@ import { BadRequestError } from "@app/lib/errors"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { NorthflankConnectionMethod } from "./northflank-connection-enums"; -import { TNorthflankConnection, TNorthflankConnectionConfig, TNorthflankProject } from "./northflank-connection-types"; +import { + TNorthflankConnection, + TNorthflankConnectionConfig, + TNorthflankProject, + TNorthflankSecretGroup +} from "./northflank-connection-types"; const NORTHFLANK_API_URL = "https://api.northflank.com"; @@ -71,3 +76,39 @@ export const listProjects = async (appConnection: TNorthflankConnection): Promis }); } }; + +export const listSecretGroups = async ( + appConnection: TNorthflankConnection, + projectId: string +): Promise => { + const { credentials } = appConnection; + + try { + const { + data: { + data: { secrets } + } + } = await request.get<{ data: { secrets: TNorthflankSecretGroup[] } }>( + `${NORTHFLANK_API_URL}/v1/projects/${projectId}/secrets`, + { + headers: { + Authorization: `Bearer ${credentials.apiToken}`, + Accept: "application/json" + } + } + ); + + return secrets; + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to list Northflank secret groups: ${error.message || "Unknown error"}` + }); + } + + throw new BadRequestError({ + message: "Unable to list Northflank secret groups", + error + }); + } +}; diff --git a/backend/src/services/app-connection/northflank/northflank-connection-service.ts b/backend/src/services/app-connection/northflank/northflank-connection-service.ts index bee3cad14..faf248bbc 100644 --- a/backend/src/services/app-connection/northflank/northflank-connection-service.ts +++ b/backend/src/services/app-connection/northflank/northflank-connection-service.ts @@ -2,8 +2,11 @@ import { logger } from "@app/lib/logger"; import { OrgServiceActor } from "@app/lib/types"; import { AppConnection } from "../app-connection-enums"; -import { listProjects as getNorthflankProjects } from "./northflank-connection-fns"; -import { TNorthflankConnection } from "./northflank-connection-types"; +import { + listProjects as getNorthflankProjects, + listSecretGroups as getNorthflankSecretGroups +} from "./northflank-connection-fns"; +import { TNorthflankConnection, TNorthflankSecretGroup } from "./northflank-connection-types"; type TGetAppConnectionFunc = ( app: AppConnection, @@ -24,7 +27,24 @@ export const northflankConnectionService = (getAppConnection: TGetAppConnectionF } }; + const listSecretGroups = async ( + connectionId: string, + projectId: string, + actor: OrgServiceActor + ): Promise => { + const appConnection = await getAppConnection(AppConnection.Northflank, connectionId, actor); + try { + const secretGroups = await getNorthflankSecretGroups(appConnection, projectId); + + return secretGroups; + } catch (error) { + logger.error({ error, connectionId, projectId, actor: actor.type }, "Failed to list Northflank secret groups"); + return []; + } + }; + return { - listProjects + listProjects, + listSecretGroups }; }; diff --git a/backend/src/services/app-connection/northflank/northflank-connection-types.ts b/backend/src/services/app-connection/northflank/northflank-connection-types.ts index 90ee579ef..c007e27d3 100644 --- a/backend/src/services/app-connection/northflank/northflank-connection-types.ts +++ b/backend/src/services/app-connection/northflank/northflank-connection-types.ts @@ -28,3 +28,8 @@ export type TNorthflankProject = { id: string; name: string; }; + +export type TNorthflankSecretGroup = { + id: string; + name: string; +}; diff --git a/backend/src/services/secret-sync/northflank/northflank-sync-fns.ts b/backend/src/services/secret-sync/northflank/northflank-sync-fns.ts index 07a5e2f8c..bfe845c5e 100644 --- a/backend/src/services/secret-sync/northflank/northflank-sync-fns.ts +++ b/backend/src/services/secret-sync/northflank/northflank-sync-fns.ts @@ -1,22 +1,123 @@ -import { logger } from "@app/lib/logger"; -import { TSecretMap, TSecretSyncWithCredentials } from "@app/services/secret-sync/secret-sync-types"; +import { request } from "@app/lib/config/request"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; -export const NorthflankSyncFns = { - syncSecrets: async (secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap): Promise => { - // TODO: Will be implemented in the follow up PR - logger.error({ secretSync, secretMap }, "Northflank secret sync not yet implemented"); - throw new Error("Northflank secret sync not yet implemented"); - }, +import { SecretSyncError } from "../secret-sync-errors"; +import { TNorthflankSyncWithCredentials } from "./northflank-sync-types"; - getSecrets: async (secretSync: TSecretSyncWithCredentials): Promise => { - // TODO: Will be implemented in the follow up PR - logger.error({ secretSync }, "Northflank secret retrieval not yet implemented"); - throw new Error("Northflank secret retrieval not yet implemented"); - }, +const NORTHFLANK_API_URL = "https://api.northflank.com"; - removeSecrets: async (secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap): Promise => { - // TODO: Will be implemented in the follow up PR - logger.error({ secretSync, secretMap }, "Northflank secret removal not yet implemented"); - throw new Error("Northflank secret removal not yet implemented"); +const getNorthflankSecrets = async (secretSync: TNorthflankSyncWithCredentials): Promise> => { + const { + destinationConfig: { projectId, secretGroupId }, + connection: { + credentials: { apiToken } + } + } = secretSync; + + try { + const { + data: { + data: { + secrets: { variables } + } + } + } = await request.get<{ + data: { + secrets: { + variables: Record; + }; + }; + }>(`${NORTHFLANK_API_URL}/v1/projects/${projectId}/secrets/${secretGroupId}/details`, { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + }); + + return variables; + } catch (error: unknown) { + throw new SecretSyncError({ + error, + message: "Failed to fetch Northflank secrets" + }); + } +}; + +const updateNorthflankSecrets = async ( + secretSync: TNorthflankSyncWithCredentials, + variables: Record +): Promise => { + const { + destinationConfig: { projectId, secretGroupId }, + connection: { + credentials: { apiToken } + } + } = secretSync; + + try { + await request.patch( + `${NORTHFLANK_API_URL}/v1/projects/${projectId}/secrets/${secretGroupId}`, + { + secrets: { + variables + } + }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + } + ); + } catch (error: unknown) { + throw new SecretSyncError({ + error, + message: "Failed to update Northflank secrets" + }); + } +}; + +export const NorthflankSyncFns = { + syncSecrets: async (secretSync: TNorthflankSyncWithCredentials, secretMap: TSecretMap): Promise => { + const northflankSecrets = await getNorthflankSecrets(secretSync); + + const updatedVariables: Record = {}; + + for (const [key, value] of Object.entries(northflankSecrets)) { + const shouldKeep = + !secretMap[key] && // this prevents duplicates from infisical secrets, because we add all of them to the updateVariables in the next loop + (secretSync.syncOptions.disableSecretDeletion || + !matchesSchema(key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)); + + if (shouldKeep) { + updatedVariables[key] = value; + } + } + + for (const [key, { value }] of Object.entries(secretMap)) { + updatedVariables[key] = value; + } + + await updateNorthflankSecrets(secretSync, updatedVariables); + }, + + getSecrets: async (secretSync: TNorthflankSyncWithCredentials): Promise => { + const northflankSecrets = await getNorthflankSecrets(secretSync); + return Object.fromEntries(Object.entries(northflankSecrets).map(([key, value]) => [key, { value }])); + }, + + removeSecrets: async (secretSync: TNorthflankSyncWithCredentials, secretMap: TSecretMap): Promise => { + const northflankSecrets = await getNorthflankSecrets(secretSync); + + const updatedVariables: Record = {}; + + for (const [key, value] of Object.entries(northflankSecrets)) { + if (!(key in secretMap)) { + updatedVariables[key] = value; + } + } + + await updateNorthflankSecrets(secretSync, updatedVariables); } }; diff --git a/backend/src/services/secret-sync/northflank/northflank-sync-schemas.ts b/backend/src/services/secret-sync/northflank/northflank-sync-schemas.ts index f88dac34a..55cdeae23 100644 --- a/backend/src/services/secret-sync/northflank/northflank-sync-schemas.ts +++ b/backend/src/services/secret-sync/northflank/northflank-sync-schemas.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { SecretSyncs } from "@app/lib/api-docs"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { @@ -10,8 +11,18 @@ import { import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; const NorthflankSyncDestinationConfigSchema = z.object({ - // TODO: Will be implemented in the follow up secret sync PR - placeholder: z.string().optional() + projectId: z + .string() + .trim() + .min(1, "Project ID is required") + .describe(SecretSyncs.DESTINATION_CONFIG.NORTHFLANK.projectId), + projectName: z.string().trim().optional().describe(SecretSyncs.DESTINATION_CONFIG.NORTHFLANK.projectName), + secretGroupId: z + .string() + .trim() + .min(1, "Secret Group ID is required") + .describe(SecretSyncs.DESTINATION_CONFIG.NORTHFLANK.secretGroupId), + secretGroupName: z.string().trim().optional().describe(SecretSyncs.DESTINATION_CONFIG.NORTHFLANK.secretGroupName) }); const NorthflankSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; diff --git a/docs/api-reference/endpoints/secret-syncs/northflank/create.mdx b/docs/api-reference/endpoints/secret-syncs/northflank/create.mdx new file mode 100644 index 000000000..47ae2f4b4 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/northflank/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/northflank" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/northflank/delete.mdx b/docs/api-reference/endpoints/secret-syncs/northflank/delete.mdx new file mode 100644 index 000000000..12e5c6e44 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/northflank/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/northflank/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/northflank/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/northflank/get-by-id.mdx new file mode 100644 index 000000000..7cad153e8 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/northflank/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/northflank/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/northflank/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/northflank/get-by-name.mdx new file mode 100644 index 000000000..487462dde --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/northflank/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/northflank/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/northflank/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/northflank/import-secrets.mdx new file mode 100644 index 000000000..0294f4dad --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/northflank/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/northflank/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/northflank/list.mdx b/docs/api-reference/endpoints/secret-syncs/northflank/list.mdx new file mode 100644 index 000000000..a1926710f --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/northflank/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/northflank" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/northflank/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/northflank/remove-secrets.mdx new file mode 100644 index 000000000..161ddac54 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/northflank/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/northflank/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/northflank/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/northflank/sync-secrets.mdx new file mode 100644 index 000000000..82ce1a96a --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/northflank/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/northflank/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/northflank/update.mdx b/docs/api-reference/endpoints/secret-syncs/northflank/update.mdx new file mode 100644 index 000000000..2f743e82f --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/northflank/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/northflank/{syncId}" +--- diff --git a/docs/docs.json b/docs/docs.json index 615f65998..d0ae7cfb5 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -555,6 +555,7 @@ "integrations/secret-syncs/humanitec", "integrations/secret-syncs/laravel-forge", "integrations/secret-syncs/netlify", + "integrations/secret-syncs/northflank", "integrations/secret-syncs/oci-vault", "integrations/secret-syncs/railway", "integrations/secret-syncs/render", @@ -2312,6 +2313,20 @@ "api-reference/endpoints/secret-syncs/netlify/remove-secrets" ] }, + { + "group": "Northflank", + "pages": [ + "api-reference/endpoints/secret-syncs/northflank/list", + "api-reference/endpoints/secret-syncs/northflank/get-by-id", + "api-reference/endpoints/secret-syncs/northflank/get-by-name", + "api-reference/endpoints/secret-syncs/northflank/create", + "api-reference/endpoints/secret-syncs/northflank/update", + "api-reference/endpoints/secret-syncs/northflank/delete", + "api-reference/endpoints/secret-syncs/northflank/sync-secrets", + "api-reference/endpoints/secret-syncs/northflank/import-secrets", + "api-reference/endpoints/secret-syncs/northflank/remove-secrets" + ] + }, { "group": "OCI", "pages": [ diff --git a/docs/images/app-connections/northflank/step-4.png b/docs/images/app-connections/northflank/step-4-1.png similarity index 100% rename from docs/images/app-connections/northflank/step-4.png rename to docs/images/app-connections/northflank/step-4-1.png diff --git a/docs/images/app-connections/northflank/step-4-2.png b/docs/images/app-connections/northflank/step-4-2.png new file mode 100644 index 000000000..ba45dae15 Binary files /dev/null and b/docs/images/app-connections/northflank/step-4-2.png differ diff --git a/docs/images/secret-syncs/northflank/configure-destination.png b/docs/images/secret-syncs/northflank/configure-destination.png new file mode 100644 index 000000000..08dcb96bf Binary files /dev/null and b/docs/images/secret-syncs/northflank/configure-destination.png differ diff --git a/docs/images/secret-syncs/northflank/configure-details.png b/docs/images/secret-syncs/northflank/configure-details.png new file mode 100644 index 000000000..edbfa0dac Binary files /dev/null and b/docs/images/secret-syncs/northflank/configure-details.png differ diff --git a/docs/images/secret-syncs/northflank/configure-source.png b/docs/images/secret-syncs/northflank/configure-source.png new file mode 100644 index 000000000..530613f03 Binary files /dev/null and b/docs/images/secret-syncs/northflank/configure-source.png differ diff --git a/docs/images/secret-syncs/northflank/configure-sync-options.png b/docs/images/secret-syncs/northflank/configure-sync-options.png new file mode 100644 index 000000000..6e03b1f5f Binary files /dev/null and b/docs/images/secret-syncs/northflank/configure-sync-options.png differ diff --git a/docs/images/secret-syncs/northflank/review-configuration.png b/docs/images/secret-syncs/northflank/review-configuration.png new file mode 100644 index 000000000..59df97a80 Binary files /dev/null and b/docs/images/secret-syncs/northflank/review-configuration.png differ diff --git a/docs/images/secret-syncs/northflank/select-option.png b/docs/images/secret-syncs/northflank/select-option.png new file mode 100644 index 000000000..0ee9ea2fa Binary files /dev/null and b/docs/images/secret-syncs/northflank/select-option.png differ diff --git a/docs/images/secret-syncs/northflank/sync-created.png b/docs/images/secret-syncs/northflank/sync-created.png new file mode 100644 index 000000000..388d78f4d Binary files /dev/null and b/docs/images/secret-syncs/northflank/sync-created.png differ diff --git a/docs/integrations/app-connections/northflank.mdx b/docs/integrations/app-connections/northflank.mdx index 4e0481154..132a9ddf9 100644 --- a/docs/integrations/app-connections/northflank.mdx +++ b/docs/integrations/app-connections/northflank.mdx @@ -27,7 +27,11 @@ Infisical supports the use of [API Tokens](https://northflank.com/docs/v1/api/us Add the **Projects** -> **Manage** -> **Read** permission. - ![Create API Role](/images/app-connections/northflank/step-4.png) + ![Create API Role](/images/app-connections/northflank/step-4-1.png) + + Add the **Config & Secrets** -> **Secret Groups** -> **List**, **Update** and **Read Values** permissions. + + ![Create API Role](/images/app-connections/northflank/step-4-2.png) Scroll to the bottom and save the API role. diff --git a/docs/integrations/secret-syncs/northflank.mdx b/docs/integrations/secret-syncs/northflank.mdx new file mode 100644 index 000000000..2d0ef0c8d --- /dev/null +++ b/docs/integrations/secret-syncs/northflank.mdx @@ -0,0 +1,160 @@ +--- +title: "Northflank Sync" +description: "Learn how to configure a Northflank Sync for Infisical." +--- + +**Prerequisites:** +- Create a [Northflank Connection](/integrations/app-connections/northflank) + + + + + + Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + + ![Select Northflank](/images/secret-syncs/northflank/select-option.png) + + + Configure the **Source** from where secrets should be retrieved, then click **Next**. + + ![Configure Source](/images/secret-syncs/northflank/configure-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + + Configure the **Destination** to where secrets should be deployed, then click **Next**. + + ![Configure Destination](/images/secret-syncs/northflank/configure-destination.png) + + - **Northflank Connection**: The Northflank Connection to authenticate with. + - **Project**: The Northflank project to sync secrets to. + - **Secret Group**: The Northflank secret group to sync secrets to. + + + Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + + ![Configure Sync Options](/images/secret-syncs/northflank/configure-sync-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Destination Secrets - Prioritize Infisical Values**: Imports any secrets present in the Northflank destination prior to syncing, prioritizing values from Infisical over Northflank when keys conflict. + - **Import Destination Secrets - Prioritize Northflank Values**: Imports any secrets present in the Northflank destination prior to syncing, prioritizing values from Northflank over Infisical when keys conflict. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + + Configure the **Details** of your Northflank Sync, then click **Next**. + + ![Configure Details](/images/secret-syncs/northflank/configure-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + + Review your Northflank Sync configuration, then click **Create Sync**. + + ![Review Configuration](/images/secret-syncs/northflank/review-configuration.png) + + + If enabled, your Northflank Sync will begin syncing your secrets to the destination endpoint. + + ![Sync Created](/images/secret-syncs/northflank/sync-created.png) + + + + + To create a **Northflank Sync**, make an API request to the [Create Northflank Sync](/api-reference/endpoints/secret-syncs/northflank/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/northflank \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-northflank-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isAutoSyncEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination", + "keySchema": "INFISICAL_{{secretKey}}" + }, + "destinationConfig": { + "projectId": "my-project-id", + "secretGroupId": "my-secret-group-id" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-northflank-sync", + "description": "an example sync", + "isAutoSyncEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination", + "keySchema": "INFISICAL_{{secretKey}}", + "disableSecretDeletion": false + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "northflank", + "name": "my-northflank-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "northflank", + "destinationConfig": { + "projectId": "my-project-id", + "secretGroupId": "my-secret-group-id" + } + } + } + ``` + + \ No newline at end of file diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/NorthflankSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/NorthflankSyncFields.tsx new file mode 100644 index 000000000..2392e4394 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/NorthflankSyncFields.tsx @@ -0,0 +1,123 @@ +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; +import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2"; +import { + TNorthflankProject, + TNorthflankSecretGroup, + useNorthflankConnectionListProjects, + useNorthflankConnectionListSecretGroups +} from "@app/hooks/api/appConnections/northflank"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const NorthflankSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.Northflank } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + const projectId = useWatch({ name: "destinationConfig.projectId", control }); + + const { data: projects = [], isPending: isProjectsLoading } = useNorthflankConnectionListProjects( + connectionId, + { + enabled: Boolean(connectionId) + } + ); + + const { data: secretGroups = [], isPending: isSecretGroupsLoading } = + useNorthflankConnectionListSecretGroups(connectionId, projectId, { + enabled: Boolean(connectionId) && Boolean(projectId) + }); + + return ( + <> + { + setValue("destinationConfig.projectId", ""); + setValue("destinationConfig.projectName", ""); + setValue("destinationConfig.secretGroupId", ""); + setValue("destinationConfig.secretGroupName", ""); + }} + /> + ( + +
+ Don't see the project you're looking for?{" "} + +
+ + } + > + p.id === value) ?? null} + onChange={(option) => { + const v = option as SingleValue; + onChange(v?.id ?? null); + setValue("destinationConfig.projectName", v?.name ?? ""); + setValue("destinationConfig.secretGroupId", ""); + setValue("destinationConfig.secretGroupName", ""); + }} + options={projects} + placeholder="Select a project..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> +
+ )} + /> + ( + +
+ Don't see the secret group you're looking for?{" "} + +
+ + } + > + sg.id === value) ?? null} + onChange={(option) => { + const v = option as SingleValue; + onChange(v?.id ?? null); + setValue("destinationConfig.secretGroupName", v?.name ?? ""); + }} + options={secretGroups} + placeholder="Select a secret group..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> +
+ )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 61ba54369..ffad9aa42 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -25,6 +25,7 @@ import { HerokuSyncFields } from "./HerokuSyncFields"; import { HumanitecSyncFields } from "./HumanitecSyncFields"; import { LaravelForgeSyncFields } from "./LaravelForgeSyncFields"; import { NetlifySyncFields } from "./NetlifySyncFields"; +import { NorthflankSyncFields } from "./NorthflankSyncFields"; import { OCIVaultSyncFields } from "./OCIVaultSyncFields"; import { RailwaySyncFields } from "./RailwaySyncFields"; import { RenderSyncFields } from "./RenderSyncFields"; @@ -103,6 +104,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.LaravelForge: return ; + case SecretSync.Northflank: + return ; default: throw new Error(`Unhandled Destination Config Field: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index f66cadaea..a7fb037de 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -68,6 +68,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.Supabase: case SecretSync.DigitalOceanAppPlatform: case SecretSync.Netlify: + case SecretSync.Northflank: case SecretSync.Bitbucket: case SecretSync.LaravelForge: AdditionalSyncOptionsFieldsComponent = null; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/NorthflankSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/NorthflankSyncReviewFields.tsx new file mode 100644 index 000000000..a37597722 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/NorthflankSyncReviewFields.tsx @@ -0,0 +1,20 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const NorthflankSyncReviewFields = () => { + const { watch } = useFormContext(); + const projectName = watch("destinationConfig.projectName"); + const projectId = watch("destinationConfig.projectId"); + const secretGroupName = watch("destinationConfig.secretGroupName"); + const secretGroupId = watch("destinationConfig.secretGroupId"); + + return ( + <> + {projectName || projectId} + {secretGroupName || secretGroupId} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 44d35cd2c..99d638c87 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -37,6 +37,7 @@ import { HerokuSyncReviewFields } from "./HerokuSyncReviewFields"; import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields"; import { LaravelForgeSyncReviewFields } from "./LaravelForgeSyncReviewFields"; import { NetlifySyncReviewFields } from "./NetlifySyncReviewFields"; +import { NorthflankSyncReviewFields } from "./NorthflankSyncReviewFields"; import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields"; import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields"; import { RailwaySyncReviewFields } from "./RailwaySyncReviewFields"; @@ -166,6 +167,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.Netlify: DestinationFieldsComponent = ; break; + case SecretSync.Northflank: + DestinationFieldsComponent = ; + break; case SecretSync.Bitbucket: DestinationFieldsComponent = ; break; diff --git a/frontend/src/components/secret-syncs/forms/schemas/northflank-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/northflank-sync-destination-schema.ts new file mode 100644 index 000000000..1554da63b --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/northflank-sync-destination-schema.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; + +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const NorthflankSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.Northflank), + destinationConfig: z.object({ + projectId: z.string().trim().min(1, "Project ID is required"), + projectName: z.string().trim().optional(), + secretGroupId: z.string().trim().min(1, "Secret Group ID is required"), + secretGroupName: z.string().trim().optional() + }) + }) +); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index 5ebc38184..146c862dc 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -22,6 +22,7 @@ import { HerokuSyncDestinationSchema } from "./heroku-sync-destination-schema"; import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema"; import { LaravelForgeSyncDestinationSchema } from "./laravel-forge-sync-destination-schema"; import { NetlifySyncDestinationSchema } from "./netlify-sync-destination-schema"; +import { NorthflankSyncDestinationSchema } from "./northflank-sync-destination-schema"; import { OCIVaultSyncDestinationSchema } from "./oci-vault-sync-destination-schema"; import { RailwaySyncDestinationSchema } from "./railway-sync-destination-schema"; import { RenderSyncDestinationSchema } from "./render-sync-destination-schema"; @@ -62,6 +63,7 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ ChecklySyncDestinationSchema, DigitalOceanAppPlatformSyncDestinationSchema, NetlifySyncDestinationSchema, + NorthflankSyncDestinationSchema, BitbucketSyncDestinationSchema, LaravelForgeSyncDestinationSchema ]); diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 12cae6ea6..a96af15d4 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -114,6 +114,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.Checkly]: AppConnection.Checkly, [SecretSync.DigitalOceanAppPlatform]: AppConnection.DigitalOcean, [SecretSync.Netlify]: AppConnection.Netlify, + [SecretSync.Northflank]: AppConnection.Northflank, [SecretSync.Bitbucket]: AppConnection.Bitbucket, [SecretSync.LaravelForge]: AppConnection.LaravelForge }; diff --git a/frontend/src/hooks/api/appConnections/northflank/index.ts b/frontend/src/hooks/api/appConnections/northflank/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/northflank/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/northflank/queries.tsx b/frontend/src/hooks/api/appConnections/northflank/queries.tsx new file mode 100644 index 000000000..bf63cd976 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/northflank/queries.tsx @@ -0,0 +1,65 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; +import { appConnectionKeys } from "@app/hooks/api/appConnections"; + +import { TNorthflankProject, TNorthflankSecretGroup } from "./types"; + +const northflankConnectionKeys = { + all: [...appConnectionKeys.all, "northflank"] as const, + listProjects: (connectionId: string) => + [...northflankConnectionKeys.all, "projects", connectionId] as const, + listSecretGroups: (connectionId: string, projectId: string) => + [...northflankConnectionKeys.all, "secret-groups", connectionId, projectId] as const +}; + +export const useNorthflankConnectionListProjects = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TNorthflankProject[], + unknown, + TNorthflankProject[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: northflankConnectionKeys.listProjects(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get<{ projects: TNorthflankProject[] }>( + `/api/v1/app-connections/northflank/${connectionId}/projects` + ); + + return data.projects; + }, + ...options + }); +}; + +export const useNorthflankConnectionListSecretGroups = ( + connectionId: string, + projectId: string, + options?: Omit< + UseQueryOptions< + TNorthflankSecretGroup[], + unknown, + TNorthflankSecretGroup[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: northflankConnectionKeys.listSecretGroups(connectionId, projectId), + queryFn: async () => { + const { data } = await apiRequest.get<{ secretGroups: TNorthflankSecretGroup[] }>( + `/api/v1/app-connections/northflank/${connectionId}/projects/${projectId}/secret-groups` + ); + + return data.secretGroups; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/northflank/types.ts b/frontend/src/hooks/api/appConnections/northflank/types.ts new file mode 100644 index 000000000..061d2179f --- /dev/null +++ b/frontend/src/hooks/api/appConnections/northflank/types.ts @@ -0,0 +1,9 @@ +export type TNorthflankProject = { + id: string; + name: string; +}; + +export type TNorthflankSecretGroup = { + id: string; + name: string; +}; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index efcc04b6d..149759d33 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -28,6 +28,7 @@ export enum SecretSync { Checkly = "checkly", DigitalOceanAppPlatform = "digital-ocean-app-platform", Netlify = "netlify", + Northflank = "northflank", Bitbucket = "bitbucket", LaravelForge = "laravel-forge" } diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index 3cab195bd..bd2e5af4f 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -23,6 +23,7 @@ import { THerokuSync } from "./heroku-sync"; import { THumanitecSync } from "./humanitec-sync"; import { TLaravelForgeSync } from "./laravel-forge-sync"; import { TNetlifySync } from "./netlify-sync"; +import { TNorthflankSync } from "./northflank-sync"; import { TOCIVaultSync } from "./oci-vault-sync"; import { TRailwaySync } from "./railway-sync"; import { TRenderSync } from "./render-sync"; @@ -70,6 +71,7 @@ export type TSecretSync = | TSupabaseSync | TDigitalOceanAppPlatformSync | TNetlifySync + | TNorthflankSync | TBitbucketSync | TLaravelForgeSync; diff --git a/frontend/src/hooks/api/secretSyncs/types/northflank-sync.ts b/frontend/src/hooks/api/secretSyncs/types/northflank-sync.ts new file mode 100644 index 000000000..e1fff68fc --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/northflank-sync.ts @@ -0,0 +1,19 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type TNorthflankSync = TRootSecretSync & { + destination: SecretSync.Northflank; + destinationConfig: { + projectId: string; + projectName?: string; + secretGroupId: string; + secretGroupName?: string; + }; + + connection: { + app: AppConnection.Northflank; + name: string; + id: string; + }; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/NorthflankSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/NorthflankSyncDestinationCol.tsx new file mode 100644 index 000000000..eecd43577 --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/NorthflankSyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { TNorthflankSync } from "@app/hooks/api/secretSyncs/types/northflank-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TNorthflankSync; +}; + +export const NorthflankSyncDestinationCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index 1e4df7351..4ec6d15ff 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -22,6 +22,7 @@ import { HerokuSyncDestinationCol } from "./HerokuSyncDestinationCol"; import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol"; import { LaravelForgeSyncDestinationCol } from "./LaravelForgeSyncDestinationCol"; import { NetlifySyncDestinationCol } from "./NetlifySyncDestinationCol"; +import { NorthflankSyncDestinationCol } from "./NorthflankSyncDestinationCol"; import { OCIVaultSyncDestinationCol } from "./OCIVaultSyncDestinationCol"; import { RailwaySyncDestinationCol } from "./RailwaySyncDestinationCol"; import { RenderSyncDestinationCol } from "./RenderSyncDestinationCol"; @@ -96,6 +97,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.Netlify: return ; + case SecretSync.Northflank: + return ; case SecretSync.Bitbucket: return ; case SecretSync.LaravelForge: diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index c4a3f2a37..48422c41e 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -194,6 +194,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.workspaceSlug; secondaryText = destinationConfig.repositorySlug; break; + case SecretSync.Northflank: + primaryText = destinationConfig.projectName || destinationConfig.projectId; + secondaryText = destinationConfig.secretGroupName || destinationConfig.secretGroupId; + break; case SecretSync.LaravelForge: primaryText = destinationConfig.siteName || destinationConfig.siteId; secondaryText = destinationConfig.orgName || destinationConfig.orgSlug; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/NorthflankSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/NorthflankSyncDestinationSection.tsx new file mode 100644 index 000000000..ec4bbf901 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/NorthflankSyncDestinationSection.tsx @@ -0,0 +1,21 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { TNorthflankSync } from "@app/hooks/api/secretSyncs/types/northflank-sync"; + +type Props = { + secretSync: TNorthflankSync; +}; + +export const NorthflankSyncDestinationSection = ({ secretSync }: Props) => { + const { destinationConfig } = secretSync; + + return ( + <> + + {destinationConfig.projectName || destinationConfig.projectId} + + + {destinationConfig.secretGroupName || destinationConfig.secretGroupId} + + + ); +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx index 78e00d8fa..31bc6d4fd 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -33,6 +33,7 @@ import { HerokuSyncDestinationSection } from "./HerokuSyncDestinationSection"; import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection"; import { LaravelForgeSyncDestinationSection } from "./LaravelForgeSyncDestinationSection"; import { NetlifySyncDestinationSection } from "./NetlifySyncDestinationSection"; +import { NorthflankSyncDestinationSection } from "./NorthflankSyncDestinationSection"; import { OCIVaultSyncDestinationSection } from "./OCIVaultSyncDestinationSection"; import { RailwaySyncDestinationSection } from "./RailwaySyncDestinationSection"; import { RenderSyncDestinationSection } from "./RenderSyncDestinationSection"; @@ -146,6 +147,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.Netlify: DestinationComponents = ; break; + case SecretSync.Northflank: + DestinationComponents = ; + break; case SecretSync.Bitbucket: DestinationComponents = ; break; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index 61d1d8ccb..cf1122d20 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -70,6 +70,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.Checkly: case SecretSync.DigitalOceanAppPlatform: case SecretSync.Netlify: + case SecretSync.Northflank: case SecretSync.Bitbucket: case SecretSync.LaravelForge: AdditionalSyncOptionsComponent = null;