diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 5a336fa11..a21da56aa 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2332,6 +2332,9 @@ export const AppConnections = { RAILWAY: { apiToken: "The API token used to authenticate with Railway." }, + NORTHFLANK: { + apiToken: "The API token used to authenticate with Northflank." + }, CHECKLY: { apiKey: "The API key used to authenticate with Checkly." }, diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index c799ef0f0..a3250c6a9 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -88,6 +88,10 @@ import { NetlifyConnectionListItemSchema, SanitizedNetlifyConnectionSchema } from "@app/services/app-connection/netlify"; +import { + NorthflankConnectionListItemSchema, + SanitizedNorthflankConnectionSchema +} from "@app/services/app-connection/northflank"; import { OktaConnectionListItemSchema, SanitizedOktaConnectionSchema } from "@app/services/app-connection/okta"; import { PostgresConnectionListItemSchema, @@ -160,6 +164,7 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedSupabaseConnectionSchema.options, ...SanitizedDigitalOceanConnectionSchema.options, ...SanitizedNetlifyConnectionSchema.options, + ...SanitizedNorthflankConnectionSchema.options, ...SanitizedOktaConnectionSchema.options, ...SanitizedAzureADCSConnectionSchema.options, ...SanitizedRedisConnectionSchema.options, @@ -203,6 +208,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ SupabaseConnectionListItemSchema, DigitalOceanConnectionListItemSchema, NetlifyConnectionListItemSchema, + NorthflankConnectionListItemSchema, OktaConnectionListItemSchema, AzureADCSConnectionListItemSchema, RedisConnectionListItemSchema, diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index 2e3da4420..d8bcdce23 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -29,6 +29,7 @@ import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerMySqlConnectionRouter } from "./mysql-connection-router"; import { registerNetlifyConnectionRouter } from "./netlify-connection-router"; +import { registerNorthflankConnectionRouter } from "./northflank-connection-router"; import { registerOktaConnectionRouter } from "./okta-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router"; import { registerRailwayConnectionRouter } from "./railway-connection-router"; @@ -83,6 +84,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { + registerAppConnectionEndpoints({ + app: AppConnection.Northflank, + server, + sanitizedResponseSchema: SanitizedNorthflankConnectionSchema, + createSchema: CreateNorthflankConnectionSchema, + updateSchema: UpdateNorthflankConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + server.route({ + method: "GET", + url: `/:connectionId/projects`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ + projects: z + .object({ + name: z.string(), + id: z.string() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + const projects = await server.services.appConnection.northflank.listProjects(connectionId, req.permission); + return { projects }; + } + }); +}; + diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 54b70c7d3..1e731ed77 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -38,7 +38,8 @@ export enum AppConnection { Netlify = "netlify", Okta = "okta", Redis = "redis", - LaravelForge = "laravel-forge" + LaravelForge = "laravel-forge", + Northflank = "northflank" } export enum AWSRegion { diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 464f718ce..8d091c2ee 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -113,6 +113,11 @@ import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums"; import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns"; import { getNetlifyConnectionListItem, validateNetlifyConnectionCredentials } from "./netlify"; +import { + getNorthflankConnectionListItem, + NorthflankConnectionMethod, + validateNorthflankConnectionCredentials +} from "./northflank"; import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway"; @@ -203,6 +208,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => { getSupabaseConnectionListItem(), getDigitalOceanConnectionListItem(), getNetlifyConnectionListItem(), + getNorthflankConnectionListItem(), getOktaConnectionListItem(), getRedisConnectionListItem() ] @@ -332,8 +338,9 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Northflank]: validateNorthflankConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator }; @@ -374,6 +381,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case BitbucketConnectionMethod.ApiToken: case ZabbixConnectionMethod.ApiToken: case DigitalOceanConnectionMethod.ApiToken: + case NorthflankConnectionMethod.ApiToken: case OktaConnectionMethod.ApiToken: case LaravelForgeConnectionMethod.ApiToken: return "API Token"; @@ -470,6 +478,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Supabase]: platformManagedCredentialsNotSupported, [AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported, [AppConnection.Netlify]: platformManagedCredentialsNotSupported, + [AppConnection.Northflank]: platformManagedCredentialsNotSupported, [AppConnection.Okta]: platformManagedCredentialsNotSupported, [AppConnection.Redis]: platformManagedCredentialsNotSupported, [AppConnection.LaravelForge]: platformManagedCredentialsNotSupported diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index c01d9d1b4..c684765bd 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -40,7 +40,8 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.DigitalOcean]: "DigitalOcean App Platform", [AppConnection.Netlify]: "Netlify", [AppConnection.Okta]: "Okta", - [AppConnection.Redis]: "Redis" + [AppConnection.Redis]: "Redis", + [AppConnection.Northflank]: "Northflank" }; export const APP_CONNECTION_PLAN_MAP: Record = { @@ -83,5 +84,6 @@ export const APP_CONNECTION_PLAN_MAP: Record { + return { + name: "Northflank" as const, + app: AppConnection.Northflank as const, + methods: Object.values(NorthflankConnectionMethod) + }; +}; + +export const validateNorthflankConnectionCredentials = async (config: TNorthflankConnectionConfig) => { + const { credentials } = config; + + try { + await request.get(`${NORTHFLANK_API_URL}/v1/projects`, { + headers: { + Authorization: `Bearer ${credentials.apiToken}`, + Accept: "application/json" + } + }); + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to validate Northflank credentials: ${error.message || "Unknown error"}` + }); + } + + throw new BadRequestError({ + message: `Failed to validate Northflank credentials - verify API token is correct` + }); + } + + return credentials; +}; + +export const listProjects = async (appConnection: TNorthflankConnection): Promise => { + const { credentials } = appConnection; + + try { + const { + data: { + data: { projects } + } + } = await request.get<{ data: { projects: TNorthflankProject[] } }>( + `${NORTHFLANK_API_URL}/v1/projects`, + { + headers: { + Authorization: `Bearer ${credentials.apiToken}`, + Accept: "application/json" + } + } + ); + + return projects; + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to list Northflank projects: ${error.message || "Unknown error"}` + }); + } + + throw new BadRequestError({ + message: "Unable to list Northflank projects", + error + }); + } +}; + diff --git a/backend/src/services/app-connection/northflank/northflank-connection-schemas.ts b/backend/src/services/app-connection/northflank/northflank-connection-schemas.ts new file mode 100644 index 000000000..3b61773a3 --- /dev/null +++ b/backend/src/services/app-connection/northflank/northflank-connection-schemas.ts @@ -0,0 +1,65 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { NorthflankConnectionMethod } from "./northflank-connection-enums"; + +export const NorthflankConnectionApiTokenCredentialsSchema = z.object({ + apiToken: z + .string() + .trim() + .min(1, "API Token required") + .describe(AppConnections.CREDENTIALS.NORTHFLANK.apiToken) +}); + +const BaseNorthflankConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.Northflank) +}); + +export const NorthflankConnectionSchema = BaseNorthflankConnectionSchema.extend({ + method: z.literal(NorthflankConnectionMethod.ApiToken), + credentials: NorthflankConnectionApiTokenCredentialsSchema +}); + +export const SanitizedNorthflankConnectionSchema = z.discriminatedUnion("method", [ + BaseNorthflankConnectionSchema.extend({ + method: z.literal(NorthflankConnectionMethod.ApiToken), + credentials: NorthflankConnectionApiTokenCredentialsSchema.pick({}) + }) +]); + +export const ValidateNorthflankConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z.literal(NorthflankConnectionMethod.ApiToken).describe( + AppConnections.CREATE(AppConnection.Northflank).method + ), + credentials: NorthflankConnectionApiTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Northflank).credentials + ) + }) +]); + +export const CreateNorthflankConnectionSchema = ValidateNorthflankConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Northflank) +); + +export const UpdateNorthflankConnectionSchema = z + .object({ + credentials: NorthflankConnectionApiTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Northflank).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Northflank)); + +export const NorthflankConnectionListItemSchema = z.object({ + name: z.literal("Northflank"), + app: z.literal(AppConnection.Northflank), + methods: z.nativeEnum(NorthflankConnectionMethod).array() +}); + diff --git a/backend/src/services/app-connection/northflank/northflank-connection-service.ts b/backend/src/services/app-connection/northflank/northflank-connection-service.ts new file mode 100644 index 000000000..45ce6bb5a --- /dev/null +++ b/backend/src/services/app-connection/northflank/northflank-connection-service.ts @@ -0,0 +1,31 @@ +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"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const northflankConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listProjects = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Northflank, connectionId, actor); + try { + const projects = await getNorthflankProjects(appConnection); + + return projects; + } catch (error) { + logger.error(error, "Failed to establish connection with Northflank"); + return []; + } + }; + + return { + listProjects + }; +}; + diff --git a/backend/src/services/app-connection/northflank/northflank-connection-types.ts b/backend/src/services/app-connection/northflank/northflank-connection-types.ts new file mode 100644 index 000000000..4d29d48ed --- /dev/null +++ b/backend/src/services/app-connection/northflank/northflank-connection-types.ts @@ -0,0 +1,31 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateNorthflankConnectionSchema, + NorthflankConnectionSchema, + ValidateNorthflankConnectionCredentialsSchema +} from "./northflank-connection-schemas"; + +export type TNorthflankConnection = z.infer; + +export type TNorthflankConnectionInput = z.infer & { + app: AppConnection.Northflank; +}; + +export type TValidateNorthflankConnectionCredentialsSchema = typeof ValidateNorthflankConnectionCredentialsSchema; + +export type TNorthflankConnectionConfig = DiscriminativePick< + TNorthflankConnection, + "method" | "app" | "credentials" +> & { + orgId: string; +}; + +export type TNorthflankProject = { + id: string; + name: string; +}; + diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 235b3db3a..f04247684 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-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/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 0ec8aede0..8110cced9 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -32,6 +32,7 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.Checkly]: "Checkly", [SecretSync.DigitalOceanAppPlatform]: "Digital Ocean App Platform", [SecretSync.Netlify]: "Netlify", + [SecretSync.Northflank]: "Northflank", [SecretSync.Bitbucket]: "Bitbucket", [SecretSync.LaravelForge]: "Laravel Forge" }; @@ -66,6 +67,7 @@ export const SECRET_SYNC_CONNECTION_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 }; @@ -100,6 +102,7 @@ export const SECRET_SYNC_PLAN_MAP: Record = { [SecretSync.Checkly]: SecretSyncPlanType.Regular, [SecretSync.DigitalOceanAppPlatform]: SecretSyncPlanType.Regular, [SecretSync.Netlify]: SecretSyncPlanType.Regular, + [SecretSync.Northflank]: SecretSyncPlanType.Regular, [SecretSync.Bitbucket]: SecretSyncPlanType.Regular, [SecretSync.LaravelForge]: SecretSyncPlanType.Regular }; @@ -143,6 +146,7 @@ export const SECRET_SYNC_SKIP_FIELDS_MAP: Record = { [SecretSync.Checkly]: ["groupName", "accountName"], [SecretSync.DigitalOceanAppPlatform]: ["appName"], [SecretSync.Netlify]: ["accountName", "siteName"], + [SecretSync.Northflank]: [], [SecretSync.Bitbucket]: [], [SecretSync.LaravelForge]: [] }; @@ -203,6 +207,7 @@ export const DESTINATION_DUPLICATE_CHECK_MAP: Record + Check out the configuration docs for [Northflank Connections](/integrations/app-connections/northflank) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/northflank/delete.mdx b/docs/api-reference/endpoints/app-connections/northflank/delete.mdx new file mode 100644 index 000000000..1c3518ea5 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/northflank/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/northflank/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/northflank/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/northflank/get-by-id.mdx new file mode 100644 index 000000000..e6e24e39e --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/northflank/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/northflank/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/northflank/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/northflank/get-by-name.mdx new file mode 100644 index 000000000..e3ca69b31 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/northflank/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/northflank/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/northflank/list.mdx b/docs/api-reference/endpoints/app-connections/northflank/list.mdx new file mode 100644 index 000000000..fbaf08ea6 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/northflank/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/northflank" +--- diff --git a/docs/api-reference/endpoints/app-connections/northflank/update.mdx b/docs/api-reference/endpoints/app-connections/northflank/update.mdx new file mode 100644 index 000000000..4554bd714 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/northflank/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/northflank/{connectionId}" +--- diff --git a/docs/docs.json b/docs/docs.json index 7f25805d8..615f65998 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -130,6 +130,7 @@ "integrations/app-connections/mssql", "integrations/app-connections/mysql", "integrations/app-connections/netlify", + "integrations/app-connections/northflank", "integrations/app-connections/oci", "integrations/app-connections/okta", "integrations/app-connections/oracledb", @@ -1841,6 +1842,18 @@ "api-reference/endpoints/app-connections/netlify/delete" ] }, + { + "group": "Northflank", + "pages": [ + "api-reference/endpoints/app-connections/northflank/list", + "api-reference/endpoints/app-connections/northflank/available", + "api-reference/endpoints/app-connections/northflank/get-by-id", + "api-reference/endpoints/app-connections/northflank/get-by-name", + "api-reference/endpoints/app-connections/northflank/create", + "api-reference/endpoints/app-connections/northflank/update", + "api-reference/endpoints/app-connections/northflank/delete" + ] + }, { "group": "OCI", "pages": [ diff --git a/docs/images/app-connections/northflank/northflank-app-connection-form.png b/docs/images/app-connections/northflank/northflank-app-connection-form.png new file mode 100644 index 000000000..aa5853234 Binary files /dev/null and b/docs/images/app-connections/northflank/northflank-app-connection-form.png differ diff --git a/docs/images/app-connections/northflank/northflank-app-connection-generated.png b/docs/images/app-connections/northflank/northflank-app-connection-generated.png new file mode 100644 index 000000000..4a239a073 Binary files /dev/null and b/docs/images/app-connections/northflank/northflank-app-connection-generated.png differ diff --git a/docs/images/app-connections/northflank/northflank-app-connection-option.png b/docs/images/app-connections/northflank/northflank-app-connection-option.png new file mode 100644 index 000000000..1a90dc98f Binary files /dev/null and b/docs/images/app-connections/northflank/northflank-app-connection-option.png differ diff --git a/docs/images/app-connections/northflank/step-1.png b/docs/images/app-connections/northflank/step-1.png new file mode 100644 index 000000000..fae04fe2f Binary files /dev/null and b/docs/images/app-connections/northflank/step-1.png differ diff --git a/docs/images/app-connections/northflank/step-2.png b/docs/images/app-connections/northflank/step-2.png new file mode 100644 index 000000000..dc8aa59a8 Binary files /dev/null and b/docs/images/app-connections/northflank/step-2.png differ diff --git a/docs/images/app-connections/northflank/step-3.png b/docs/images/app-connections/northflank/step-3.png new file mode 100644 index 000000000..f85cffacd Binary files /dev/null and b/docs/images/app-connections/northflank/step-3.png differ diff --git a/docs/images/app-connections/northflank/step-4.png b/docs/images/app-connections/northflank/step-4.png new file mode 100644 index 000000000..1fd72d954 Binary files /dev/null and b/docs/images/app-connections/northflank/step-4.png differ diff --git a/docs/images/app-connections/northflank/step-5.png b/docs/images/app-connections/northflank/step-5.png new file mode 100644 index 000000000..793530ccf Binary files /dev/null and b/docs/images/app-connections/northflank/step-5.png differ diff --git a/docs/images/app-connections/northflank/step-6.png b/docs/images/app-connections/northflank/step-6.png new file mode 100644 index 000000000..2153f94c3 Binary files /dev/null and b/docs/images/app-connections/northflank/step-6.png differ diff --git a/docs/images/app-connections/northflank/step-7.png b/docs/images/app-connections/northflank/step-7.png new file mode 100644 index 000000000..bfbbb15c4 Binary files /dev/null and b/docs/images/app-connections/northflank/step-7.png differ diff --git a/docs/integrations/app-connections/northflank.mdx b/docs/integrations/app-connections/northflank.mdx new file mode 100644 index 000000000..e4fa803f4 --- /dev/null +++ b/docs/integrations/app-connections/northflank.mdx @@ -0,0 +1,123 @@ +--- +title: "Northflank Connection" +description: "Learn how to configure a Northflank Connection for Infisical." +--- + +Infisical supports the use of [API Tokens](https://northflank.com/docs/v1/api/use-the-api) to connect with Northflank. + + + Infisical recommends creating a specific API role for the app connection and only giving access to projects that will use the integration. + + +## Create a Northflank API Token + + + + Navigate to your team page and click **Create token**. + + ![Create API Role](/images/app-connections/northflank/step-1.png) + + Click in **Create API role**. + + ![Create API Role](/images/app-connections/northflank/step-2.png) + + Select all the projects you want this role to have access, or leave this unchecked if you want to give access to all project. + + ![Create API Role](/images/app-connections/northflank/step-3.png) + + Add the **Projects** -> **Manage** -> **Read** permission. + + ![Create API Role](/images/app-connections/northflank/step-4.png) + + Scroll to the bottom and save the API role. + + + Click on the **API** -> **Tokens** menu on the left and then in the **Create API token** button. + + ![Create API Token](/images/app-connections/northflank/step-5.png) + + Give a name to the API token and click the **Use role** button for the new API role you just created. + + ![Create API Token](/images/app-connections/northflank/step-6.png) + + Click the **View API token** icon to view and copy your token. + + ![Create API Token](/images/app-connections/northflank/step-7.png) + + + +## Create a Northflank Connection in Infisical + + + + + + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. + + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Click **+ Add Connection** and choose **Northflank Connection** from the list of integrations. + + ![Select Northflank Connection](/images/app-connections/northflank/northflank-app-connection-option.png) + + + Complete the form by providing: + - A descriptive name for the connection + - An optional description + - The API Token from the previous step + + ![Northflank Connection Modal](/images/app-connections/northflank/northflank-app-connection-form.png) + + + After submitting the form, your **Northflank Connection** will be successfully created and ready to use with your Infisical project. + + ![Northflank Connection Created](/images/app-connections/northflank/northflank-app-connection-generated.png) + + + + + + To create a Northflank Connection via API, send a request to the [Create Northflank Connection](/api-reference/endpoints/app-connections/northflank/create) endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url http://localhost:8080/api/v1/app-connections/northflank \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-northflank-connection", + "method": "api-token", + "projectId": "abcdef12-3456-7890-abcd-ef1234567890", + "credentials": { + "apiToken": "[API TOKEN]" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", + "name": "my-northflank-connection", + "description": null, + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", + "version": 1, + "orgId": "abcdef12-3456-7890-abcd-ef1234567890", + "createdAt": "2025-01-23T10:15:00.000Z", + "updatedAt": "2025-01-23T10:15:00.000Z", + "isPlatformManagedCredentials": false, + "credentialsHash": "d41d8cd98f00b204e9800998ecf8427e", + "gatewayId":null, + "projectId":"abcdef12-3456-7890-abcd-ef1234567890" + "app": "northflank", + "method": "api-token", + "credentials": {} + } + } + ``` + + \ No newline at end of file diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index bdb26c796..b3a8553a9 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -50,6 +50,7 @@ import { DigitalOceanConnectionMethod } from "@app/hooks/api/appConnections/type import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection"; import { LaravelForgeConnectionMethod } from "@app/hooks/api/appConnections/types/laravel-forge-connection"; import { NetlifyConnectionMethod } from "@app/hooks/api/appConnections/types/netlify-connection"; +import { NorthflankConnectionMethod } from "@app/hooks/api/appConnections/types/northflank-connection"; import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection"; import { RailwayConnectionMethod } from "@app/hooks/api/appConnections/types/railway-connection"; import { RenderConnectionMethod } from "@app/hooks/api/appConnections/types/render-connection"; @@ -121,6 +122,7 @@ export const APP_CONNECTION_MAP: Record< name: "Netlify", image: "Netlify.png" }, + [AppConnection.Northflank]: { name: "Northflank", image: "Northflank.png" }, [AppConnection.Okta]: { name: "Okta", image: "Okta.png" }, [AppConnection.Redis]: { name: "Redis", image: "Redis.png" }, [AppConnection.LaravelForge]: { @@ -162,6 +164,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case BitbucketConnectionMethod.ApiToken: case ZabbixConnectionMethod.ApiToken: case DigitalOceanConnectionMethod.ApiToken: + case NorthflankConnectionMethod.ApiToken: case OktaConnectionMethod.ApiToken: case LaravelForgeConnectionMethod.ApiToken: return { name: "API Token", icon: faKey }; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 66fed8a5d..4af1dbb27 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -36,6 +36,7 @@ export enum AppConnection { Supabase = "supabase", DigitalOcean = "digital-ocean", Netlify = "netlify", + Northflank = "northflank", Okta = "okta", Redis = "redis", LaravelForge = "laravel-forge" diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index fd840d5de..d82ad90ec 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -27,6 +27,7 @@ import { TLdapConnection } from "./ldap-connection"; import { TMsSqlConnection } from "./mssql-connection"; import { TMySqlConnection } from "./mysql-connection"; import { TNetlifyConnection } from "./netlify-connection"; +import { TNorthflankConnection } from "./northflank-connection"; import { TOCIConnection } from "./oci-connection"; import { TOktaConnection } from "./okta-connection"; import { TOracleDBConnection } from "./oracledb-connection"; @@ -66,6 +67,8 @@ export * from "./laravel-forge-connection"; export * from "./ldap-connection"; export * from "./mssql-connection"; export * from "./mysql-connection"; +export * from "./netlify-connection"; +export * from "./northflank-connection"; export * from "./oci-connection"; export * from "./okta-connection"; export * from "./oracledb-connection"; @@ -119,6 +122,7 @@ export type TAppConnection = | TSupabaseConnection | TDigitalOceanConnection | TNetlifyConnection + | TNorthflankConnection | TOktaConnection | TRedisConnection; diff --git a/frontend/src/hooks/api/appConnections/types/northflank-connection.ts b/frontend/src/hooks/api/appConnections/types/northflank-connection.ts new file mode 100644 index 000000000..402159117 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/northflank-connection.ts @@ -0,0 +1,14 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum NorthflankConnectionMethod { + ApiToken = "api-token" +} + +export type TNorthflankConnection = TRootAppConnection & { app: AppConnection.Northflank } & { + method: NorthflankConnectionMethod.ApiToken; + credentials: { + apiToken: string; + }; +}; + diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index a44c4746b..d63965977 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -36,6 +36,7 @@ import { LdapConnectionForm } from "./LdapConnectionForm"; import { MsSqlConnectionForm } from "./MsSqlConnectionForm"; import { MySqlConnectionForm } from "./MySqlConnectionForm"; import { NetlifyConnectionForm } from "./NetlifyConnectionForm"; +import { NorthflankConnectionForm } from "./NorthflankConnectionForm"; import { OCIConnectionForm } from "./OCIConnectionForm"; import { OktaConnectionForm } from "./OktaConnectionForm"; import { OracleDBConnectionForm } from "./OracleDBConnectionForm"; @@ -169,6 +170,8 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => { return ; case AppConnection.Netlify: return ; + case AppConnection.Northflank: + return ; case AppConnection.Okta: return ; case AppConnection.Redis: @@ -326,6 +329,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.DigitalOcean: return ; + case AppConnection.Northflank: + return ; case AppConnection.Okta: return ; case AppConnection.Redis: diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/NorthflankConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/NorthflankConnectionForm.tsx new file mode 100644 index 000000000..5befaa645 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/NorthflankConnectionForm.tsx @@ -0,0 +1,136 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { + TNorthflankConnection, + NorthflankConnectionMethod +} from "@app/hooks/api/appConnections/types/northflank-connection"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TNorthflankConnection; + onSubmit: (formData: FormData) => void; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.Northflank) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(NorthflankConnectionMethod.ApiToken), + credentials: z.object({ + apiToken: z.string().trim().min(1, "API Token required") + }) + }) +]); + +type FormData = z.infer; + +export const NorthflankConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.Northflank, + method: NorthflankConnectionMethod.ApiToken + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; + diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx index 8cd28ea91..320abf2df 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx @@ -72,6 +72,7 @@ export const AppConnectionsSelect = ({ onSelect, projectType }: Props) => { return (