From 8121db69f2d045a6aa5e050ad7d5aa7d83166e6f Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Fri, 14 Nov 2025 20:26:21 -0300 Subject: [PATCH] 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 aa1d671b6..1c3eade58 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 1c184a436..5f09d61a8 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 863fa75f9..f8e73db80 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 5b8cc3fc1..49b337a2c 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 000000000..1caba5071 --- /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 000000000..52212545a --- /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 fba0cbb4b..d04b42d4c 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" }