From 8121db69f2d045a6aa5e050ad7d5aa7d83166e6f Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Fri, 14 Nov 2025 20:26:21 -0300 Subject: [PATCH 001/307] Add MongoDB connection support - Introduced MongoDB connection router and schemas. - Updated app connection enums and maps to include MongoDB. - Implemented validation and connection functions for MongoDB. - Enhanced app connection service to handle MongoDB credentials. --- .../routes/v1/app-connection-routers/index.ts | 2 + .../mongodb-connection-router.ts | 18 ++++ .../app-connection/app-connection-enums.ts | 1 + .../app-connection/app-connection-fns.ts | 7 +- .../app-connection/app-connection-maps.ts | 2 + .../app-connection/app-connection-service.ts | 2 + .../app-connection/app-connection-types.ts | 10 +++ .../services/app-connection/mongodb/index.ts | 4 + .../mongodb/mongodb-connection-enums.ts | 4 + .../mongodb/mongodb-connection-fns.ts | 70 +++++++++++++++ .../mongodb/mongodb-connection-schemas.ts | 89 +++++++++++++++++++ .../mongodb/mongodb-connection-types.ts | 22 +++++ .../src/hooks/api/appConnections/enums.ts | 1 + 13 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 backend/src/server/routes/v1/app-connection-routers/mongodb-connection-router.ts create mode 100644 backend/src/services/app-connection/mongodb/index.ts create mode 100644 backend/src/services/app-connection/mongodb/mongodb-connection-enums.ts create mode 100644 backend/src/services/app-connection/mongodb/mongodb-connection-fns.ts create mode 100644 backend/src/services/app-connection/mongodb/mongodb-connection-schemas.ts create mode 100644 backend/src/services/app-connection/mongodb/mongodb-connection-types.ts 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 aa1d671b62..1c3eade580 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -27,6 +27,7 @@ import { registerHerokuConnectionRouter } from "./heroku-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; import { registerLaravelForgeConnectionRouter } from "./laravel-forge-connection-router"; import { registerLdapConnectionRouter } from "./ldap-connection-router"; +import { registerMongoDBConnectionRouter } from "./mongodb-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerMySqlConnectionRouter } from "./mysql-connection-router"; import { registerNetlifyConnectionRouter } from "./netlify-connection-router"; @@ -88,5 +89,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { + registerAppConnectionEndpoints({ + app: AppConnection.MongoDB, + server, + sanitizedResponseSchema: SanitizedMongoDBConnectionSchema, + createSchema: CreateMongoDBConnectionSchema, + updateSchema: UpdateMongoDBConnectionSchema + }); +}; diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 1c184a436a..5f09d61a8d 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -38,6 +38,7 @@ export enum AppConnection { Netlify = "netlify", Okta = "okta", Redis = "redis", + MongoDB = "mongodb", LaravelForge = "laravel-forge", Chef = "chef", Northflank = "northflank" diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 863fa75f9a..f8e73db806 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -114,6 +114,7 @@ import { validateLaravelForgeConnectionCredentials } from "./laravel-forge"; import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap"; +import { getMongoDBConnectionListItem, MongoDBConnectionMethod, validateMongoDBConnectionCredentials } from "./mongodb"; import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums"; import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns"; @@ -216,6 +217,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => { getNorthflankConnectionListItem(), getOktaConnectionListItem(), getRedisConnectionListItem(), + getMongoDBConnectionListItem(), getChefConnectionListItem() ] .filter((option) => { @@ -348,7 +350,8 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Northflank]: validateNorthflankConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Chef]: validateChefConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator + [AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.MongoDB]: validateMongoDBConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService, gatewayV2Service); @@ -400,6 +403,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case OracleDBConnectionMethod.UsernameAndPassword: case AzureADCSConnectionMethod.UsernamePassword: case RedisConnectionMethod.UsernameAndPassword: + case MongoDBConnectionMethod.UsernameAndPassword: return "Username & Password"; case WindmillConnectionMethod.AccessToken: case HCVaultConnectionMethod.AccessToken: @@ -492,6 +496,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Northflank]: platformManagedCredentialsNotSupported, [AppConnection.Okta]: platformManagedCredentialsNotSupported, [AppConnection.Redis]: platformManagedCredentialsNotSupported, + [AppConnection.MongoDB]: platformManagedCredentialsNotSupported, [AppConnection.LaravelForge]: platformManagedCredentialsNotSupported, [AppConnection.Chef]: platformManagedCredentialsNotSupported }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 5b8cc3fc1c..49b337a2c4 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -41,6 +41,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Netlify]: "Netlify", [AppConnection.Okta]: "Okta", [AppConnection.Redis]: "Redis", + [AppConnection.MongoDB]: "MongoDB", [AppConnection.Chef]: "Chef", [AppConnection.Northflank]: "Northflank" }; @@ -86,6 +87,7 @@ export const APP_CONNECTION_PLAN_MAP: Record { + return { + name: "MongoDB" as const, + app: AppConnection.MongoDB as const, + methods: Object.values(MongoDBConnectionMethod) as [MongoDBConnectionMethod.UsernameAndPassword], + supportsPlatformManagement: false as const + }; +}; + +export const validateMongoDBConnectionCredentials = async (config: TMongoDBConnectionConfig) => { + const [hostIp] = await verifyHostInputValidity(config.credentials.host); + + let client: MongoClient | null = null; + try { + const isSrv = !config.credentials.port; + const uri = isSrv ? `mongodb+srv://${hostIp}` : `mongodb://${hostIp}:${config.credentials.port}`; + + const clientOptions: { + auth?: { username: string; password?: string }; + tls?: boolean; + tlsInsecure?: boolean; + ca?: string; + directConnection?: boolean; + } = { + auth: { + username: config.credentials.username, + password: config.credentials.password + }, + directConnection: !isSrv + }; + + if (config.credentials.sslEnabled || isSrv) { + clientOptions.tls = true; + clientOptions.tlsInsecure = !config.credentials.sslRejectUnauthorized; + if (config.credentials.sslCertificate) { + clientOptions.ca = config.credentials.sslCertificate; + } + } + + client = new MongoClient(uri, clientOptions); + + // Validate connection by running ping command + await client + .db(config.credentials.database) + .command({ ping: 1 }) + .then(() => true); + + if (client) await client.close(); + + return config.credentials; + } catch (err) { + if (err instanceof BadRequestError) { + throw err; + } + throw new BadRequestError({ + message: `Unable to validate connection: ${(err as Error)?.message || "verify credentials"}` + }); + } finally { + if (client) await client.close(); + } +}; diff --git a/backend/src/services/app-connection/mongodb/mongodb-connection-schemas.ts b/backend/src/services/app-connection/mongodb/mongodb-connection-schemas.ts new file mode 100644 index 0000000000..1caba50715 --- /dev/null +++ b/backend/src/services/app-connection/mongodb/mongodb-connection-schemas.ts @@ -0,0 +1,89 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { AppConnection } from "../app-connection-enums"; +import { MongoDBConnectionMethod } from "./mongodb-connection-enums"; + +export const BaseMongoDBUsernameAndPasswordConnectionSchema = z.object({ + host: z.string().toLowerCase().min(1), + port: z.coerce.number(), + username: z.string().min(1), + password: z.string().min(1), + database: z.string().min(1).trim(), + + sslRejectUnauthorized: z.boolean(), + sslEnabled: z.boolean(), + sslCertificate: z + .string() + .trim() + .transform((value) => value || undefined) + .optional() +}); + +export const MongoDBConnectionAccessTokenCredentialsSchema = BaseMongoDBUsernameAndPasswordConnectionSchema; + +const BaseMongoDBConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.MongoDB) }); + +export const MongoDBConnectionSchema = BaseMongoDBConnectionSchema.extend({ + method: z.literal(MongoDBConnectionMethod.UsernameAndPassword), + credentials: MongoDBConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedMongoDBConnectionSchema = z.discriminatedUnion("method", [ + BaseMongoDBConnectionSchema.extend({ + method: z.literal(MongoDBConnectionMethod.UsernameAndPassword), + credentials: MongoDBConnectionAccessTokenCredentialsSchema.pick({ + host: true, + port: true, + username: true, + database: true, + sslEnabled: true, + sslRejectUnauthorized: true, + sslCertificate: true + }) + }) +]); + +export const ValidateMongoDBConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(MongoDBConnectionMethod.UsernameAndPassword) + .describe(AppConnections.CREATE(AppConnection.MongoDB).method), + credentials: MongoDBConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.MongoDB).credentials + ) + }) +]); + +export const CreateMongoDBConnectionSchema = ValidateMongoDBConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.MongoDB, { + supportsPlatformManagedCredentials: false, + supportsGateways: false + }) +); + +export const UpdateMongoDBConnectionSchema = z + .object({ + credentials: MongoDBConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.MongoDB).credentials + ) + }) + .and( + GenericUpdateAppConnectionFieldsSchema(AppConnection.MongoDB, { + supportsPlatformManagedCredentials: false, + supportsGateways: false + }) + ); + +export const MongoDBConnectionListItemSchema = z.object({ + name: z.literal("MongoDB"), + app: z.literal(AppConnection.MongoDB), + methods: z.nativeEnum(MongoDBConnectionMethod).array(), + supportsPlatformManagement: z.literal(false) +}); diff --git a/backend/src/services/app-connection/mongodb/mongodb-connection-types.ts b/backend/src/services/app-connection/mongodb/mongodb-connection-types.ts new file mode 100644 index 0000000000..52212545a4 --- /dev/null +++ b/backend/src/services/app-connection/mongodb/mongodb-connection-types.ts @@ -0,0 +1,22 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateMongoDBConnectionSchema, + MongoDBConnectionSchema, + ValidateMongoDBConnectionCredentialsSchema +} from "./mongodb-connection-schemas"; + +export type TMongoDBConnection = z.infer; + +export type TMongoDBConnectionInput = z.infer & { + app: AppConnection.MongoDB; +}; + +export type TValidateMongoDBConnectionCredentialsSchema = typeof ValidateMongoDBConnectionCredentialsSchema; + +export type TMongoDBConnectionConfig = DiscriminativePick & { + orgId: string; +}; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index fba0cbb4bc..d04b42d4c9 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -39,6 +39,7 @@ export enum AppConnection { Northflank = "northflank", Okta = "okta", Redis = "redis", + MongoDB = "mongodb", LaravelForge = "laravel-forge", Chef = "chef" } From 055a663d24d9da0052d828ab7a6b539307cef738 Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Mon, 17 Nov 2025 09:21:44 -0300 Subject: [PATCH 002/307] Add MongoDB connection form and related updates - Implemented MongoDB connection form with validation and credential handling. - Updated enums and maps to include MongoDB support across backend and frontend. - Enhanced app connection schemas to accommodate MongoDB connection options. - Integrated MongoDB connection handling in relevant components and hooks. --- .../secret-rotation-v2-enums.ts | 3 +- .../secret-rotation-v2-maps.ts | 6 +- .../app-connection-router.ts | 6 + frontend/src/helpers/appConnections.ts | 3 + .../api/appConnections/types/app-options.ts | 7 + .../hooks/api/appConnections/types/index.ts | 3 + .../types/mongodb-connection.ts | 22 ++ .../AppConnectionForm/AppConnectionForm.tsx | 5 + .../MongoDBConnectionForm.tsx | 334 ++++++++++++++++++ 9 files changed, 386 insertions(+), 3 deletions(-) create mode 100644 frontend/src/hooks/api/appConnections/types/mongodb-connection.ts create mode 100644 frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MongoDBConnectionForm.tsx diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts index 661a2399ae..470a638498 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts @@ -8,7 +8,8 @@ export enum SecretRotation { AwsIamUserSecret = "aws-iam-user-secret", LdapPassword = "ldap-password", OktaClientSecret = "okta-client-secret", - RedisCredentials = "redis-credentials" + RedisCredentials = "redis-credentials", + MongoDBCredentials = "mongodb-credentials" } export enum SecretRotationStatus { diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts index 2087fa1957..c2b0714ab0 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts @@ -11,7 +11,8 @@ export const SECRET_ROTATION_NAME_MAP: Record = { [SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret", [SecretRotation.LdapPassword]: "LDAP Password", [SecretRotation.OktaClientSecret]: "Okta Client Secret", - [SecretRotation.RedisCredentials]: "Redis Credentials" + [SecretRotation.RedisCredentials]: "Redis Credentials", + [SecretRotation.MongoDBCredentials]: "MongoDB Credentials" }; export const SECRET_ROTATION_CONNECTION_MAP: Record = { @@ -24,5 +25,6 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record; diff --git a/frontend/src/hooks/api/appConnections/types/mongodb-connection.ts b/frontend/src/hooks/api/appConnections/types/mongodb-connection.ts new file mode 100644 index 0000000000..151c676a54 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/mongodb-connection.ts @@ -0,0 +1,22 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum MongoDBConnectionMethod { + UsernameAndPassword = "username-and-password" +} + +export type TMongoDBConnectionCredentials = { + host: string; + port: number; + username: string; + password: string; + database: string; + sslEnabled: boolean; + sslRejectUnauthorized: boolean; + sslCertificate?: string; +}; + +export type TMongoDBConnection = TRootAppConnection & { app: AppConnection.MongoDB } & { + method: MongoDBConnectionMethod.UsernameAndPassword; + credentials: TMongoDBConnectionCredentials; +}; 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 aca33ffa28..3b1710fdb5 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -34,6 +34,7 @@ import { HerokuConnectionForm } from "./HerokuAppConnectionForm"; import { HumanitecConnectionForm } from "./HumanitecConnectionForm"; import { LaravelForgeConnectionForm } from "./LaravelForgeConnectionForm"; import { LdapConnectionForm } from "./LdapConnectionForm"; +import { MongoDBConnectionForm } from "./MongoDBConnectionForm"; import { MsSqlConnectionForm } from "./MsSqlConnectionForm"; import { MySqlConnectionForm } from "./MySqlConnectionForm"; import { NetlifyConnectionForm } from "./NetlifyConnectionForm"; @@ -170,6 +171,8 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => { return ; case AppConnection.Redis: return ; + case AppConnection.MongoDB: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -326,6 +329,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Redis: return ; + case AppConnection.MongoDB: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MongoDBConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MongoDBConnectionForm.tsx new file mode 100644 index 0000000000..c9b3421cee --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MongoDBConnectionForm.tsx @@ -0,0 +1,334 @@ +import { useEffect, useState } from "react"; +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tab } from "@headlessui/react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem, + Switch, + TextArea, + Tooltip +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { MongoDBConnectionMethod, TMongoDBConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TMongoDBConnection; + onSubmit: (formData: FormData) => Promise; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.MongoDB) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(MongoDBConnectionMethod.UsernameAndPassword), + credentials: z.object({ + host: z.string().trim().min(1, "Host required"), + port: z.coerce.number().default(27017), + username: z.string().trim().min(1, "Username required"), + password: z.string().trim().min(1, "Password required"), + database: z.string().trim().min(1, "Database required"), + sslEnabled: z.boolean().default(false), + sslRejectUnauthorized: z.boolean().default(true), + sslCertificate: z + .string() + .trim() + .transform((value) => value || undefined) + .optional() + }) + }) +]); + +type FormData = z.infer; + +export const MongoDBConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + const [selectedTabIndex, setSelectedTabIndex] = useState(0); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.MongoDB, + method: MongoDBConnectionMethod.UsernameAndPassword, + credentials: { + host: "", + port: 27017, + username: "", + password: "", + database: "", + sslEnabled: false, + sslRejectUnauthorized: true, + sslCertificate: undefined + } + } + }); + + const { + handleSubmit, + watch, + control, + setValue, + formState: { isSubmitting, isDirty } + } = form; + + const host = watch("credentials.host"); + const sslEnabled = watch("credentials.sslEnabled"); + + useEffect(() => { + if (host && host.includes("+srv") && !sslEnabled) { + setValue("credentials.sslEnabled", true, { shouldDirty: true }); + } + }, [host, sslEnabled, setValue]); + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + + + + + `-mb-[0.14rem] px-4 py-2 text-sm font-medium whitespace-nowrap outline-hidden disabled:opacity-60 ${ + selected + ? "border-b-2 border-mineshaft-300 text-mineshaft-200" + : "text-bunker-300" + }` + } + > + Configuration + + + `-mb-[0.14rem] px-4 py-2 text-sm font-medium whitespace-nowrap outline-hidden disabled:opacity-60 ${ + selected + ? "border-b-2 border-mineshaft-300 text-mineshaft-200" + : "text-bunker-300" + }` + } + > + SSL ({sslEnabled ? "Enabled" : "Disabled"}) + + + + +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+
+ ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+
+ + ( + + + Enable SSL + + + )} + /> + ( + +