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.
This commit is contained in:
Victor Santos
2025-11-17 09:21:44 -03:00
parent 8121db69f2
commit 055a663d24
9 changed files with 386 additions and 3 deletions

View File

@@ -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 {

View File

@@ -11,7 +11,8 @@ export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
[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<SecretRotation, AppConnection> = {
@@ -24,5 +25,6 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS,
[SecretRotation.LdapPassword]: AppConnection.LDAP,
[SecretRotation.OktaClientSecret]: AppConnection.Okta,
[SecretRotation.RedisCredentials]: AppConnection.Redis
[SecretRotation.RedisCredentials]: AppConnection.Redis,
[SecretRotation.MongoDBCredentials]: AppConnection.MongoDB
};

View File

@@ -83,6 +83,10 @@ import {
SanitizedLaravelForgeConnectionSchema
} from "@app/services/app-connection/laravel-forge";
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
import {
MongoDBConnectionListItemSchema,
SanitizedMongoDBConnectionSchema
} from "@app/services/app-connection/mongodb";
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
import { MySqlConnectionListItemSchema, SanitizedMySqlConnectionSchema } from "@app/services/app-connection/mysql";
import {
@@ -169,6 +173,7 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedOktaConnectionSchema.options,
...SanitizedAzureADCSConnectionSchema.options,
...SanitizedRedisConnectionSchema.options,
...SanitizedMongoDBConnectionSchema.options,
...SanitizedLaravelForgeConnectionSchema.options,
...SanitizedChefConnectionSchema.options
]);
@@ -214,6 +219,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
OktaConnectionListItemSchema,
AzureADCSConnectionListItemSchema,
RedisConnectionListItemSchema,
MongoDBConnectionListItemSchema,
LaravelForgeConnectionListItemSchema,
ChefConnectionListItemSchema
]);

View File

@@ -31,6 +31,7 @@ import {
HCVaultConnectionMethod,
HumanitecConnectionMethod,
LdapConnectionMethod,
MongoDBConnectionMethod,
MsSqlConnectionMethod,
MySqlConnectionMethod,
OktaConnectionMethod,
@@ -127,6 +128,7 @@ export const APP_CONNECTION_MAP: Record<
[AppConnection.Northflank]: { name: "Northflank", image: "Northflank.png" },
[AppConnection.Okta]: { name: "Okta", image: "Okta.png" },
[AppConnection.Redis]: { name: "Redis", image: "Redis.png" },
[AppConnection.MongoDB]: { name: "MongoDB", image: "MongoDB.png" },
[AppConnection.LaravelForge]: {
name: "Laravel Forge",
image: "Laravel Forge.png",
@@ -179,6 +181,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
case OracleDBConnectionMethod.UsernameAndPassword:
case AzureADCSConnectionMethod.UsernamePassword:
case RedisConnectionMethod.UsernameAndPassword:
case MongoDBConnectionMethod.UsernameAndPassword:
return { name: "Username & Password", icon: faLock };
case HCVaultConnectionMethod.AccessToken:
case TeamCityConnectionMethod.AccessToken:

View File

@@ -184,6 +184,10 @@ export type TRedisConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Redis;
};
export type TMongoDBConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.MongoDB;
};
export type TAppConnectionOption =
| TAwsConnectionOption
| TGitHubConnectionOption
@@ -225,6 +229,8 @@ export type TAppConnectionOption =
| TOktaConnectionOption
| TAzureAdCsConnectionOption
| TLaravelForgeConnectionOption
| TRedisConnectionOption
| TMongoDBConnectionOption
| TChefConnectionOption;
export type TAppConnectionOptionMap = {
@@ -268,6 +274,7 @@ export type TAppConnectionOptionMap = {
[AppConnection.Okta]: TOktaConnectionOption;
[AppConnection.AzureADCS]: TAzureAdCsConnectionOption;
[AppConnection.Redis]: TRedisConnectionOption;
[AppConnection.MongoDB]: TMongoDBConnectionOption;
[AppConnection.LaravelForge]: TLaravelForgeConnectionOption;
[AppConnection.Chef]: TChefConnectionOption;
};

View File

@@ -25,6 +25,7 @@ import { THerokuConnection } from "./heroku-connection";
import { THumanitecConnection } from "./humanitec-connection";
import { TLaravelForgeConnection } from "./laravel-forge-connection";
import { TLdapConnection } from "./ldap-connection";
import { TMongoDBConnection } from "./mongodb-connection";
import { TMsSqlConnection } from "./mssql-connection";
import { TMySqlConnection } from "./mysql-connection";
import { TNetlifyConnection } from "./netlify-connection";
@@ -67,6 +68,7 @@ export * from "./heroku-connection";
export * from "./humanitec-connection";
export * from "./laravel-forge-connection";
export * from "./ldap-connection";
export * from "./mongodb-connection";
export * from "./mssql-connection";
export * from "./mysql-connection";
export * from "./netlify-connection";
@@ -127,6 +129,7 @@ export type TAppConnection =
| TNorthflankConnection
| TOktaConnection
| TRedisConnection
| TMongoDBConnection
| TChefConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id" | "projectId">;

View File

@@ -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;
};

View File

@@ -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 <OktaConnectionForm onSubmit={onSubmit} />;
case AppConnection.Redis:
return <RedisConnectionForm onSubmit={onSubmit} />;
case AppConnection.MongoDB:
return <MongoDBConnectionForm onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled App ${app}`);
}
@@ -326,6 +329,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <OktaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Redis:
return <RedisConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.MongoDB:
return <MongoDBConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
}

View File

@@ -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<void>;
};
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<typeof formSchema>;
export const MongoDBConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const form = useForm<FormData>({
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 (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.MongoDB].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(MongoDBConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600">
<Tab
className={({ selected }) =>
`-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
</Tab>
<Tab
className={({ selected }) =>
`-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"})
</Tab>
</Tab.List>
<Tab.Panels className="mb-4 rounded-sm border border-mineshaft-600 bg-mineshaft-700/70 p-3 pb-0">
<Tab.Panel>
<div className="mt-[0.675rem] flex items-start gap-2">
<Controller
name="credentials.host"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
className="flex-1"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Host"
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
name="credentials.database"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
className="flex-1"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Database"
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
name="credentials.port"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
className="w-28"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Port"
>
<Input type="number" {...field} />
</FormControl>
)}
/>
</div>
<div className="mb-[0.675rem] flex items-start gap-2">
<Controller
name="credentials.username"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Username"
className="flex-1"
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
name="credentials.password"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Password"
className="flex-1"
>
<SecretInput
containerClassName="text-gray-400 w-full group-focus-within:border-primary-400/50! border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
</div>
</Tab.Panel>
<Tab.Panel>
<Controller
name="credentials.sslEnabled"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error?.message)} errorText={error?.message}>
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="ssl-enabled"
thumbClassName="bg-mineshaft-800"
isChecked={value}
onCheckedChange={onChange}
>
Enable SSL
</Switch>
</FormControl>
)}
/>
<Controller
name="credentials.sslCertificate"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
className={sslEnabled ? "" : "opacity-50"}
label="SSL Certificate"
isOptional
>
<TextArea className="h-14 resize-none!" {...field} isDisabled={!sslEnabled} />
</FormControl>
)}
/>
<Controller
name="credentials.sslRejectUnauthorized"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
className={sslEnabled ? "" : "opacity-50"}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="ssl-reject-unauthorized"
thumbClassName="bg-mineshaft-800"
isChecked={sslEnabled ? value : false}
onCheckedChange={onChange}
isDisabled={!sslEnabled}
>
<p className="w-38">
Reject Unauthorized
<Tooltip
className="max-w-md"
content={
<p>
If enabled, Infisical will only connect to the server if it has a
valid, trusted SSL certificate.
</p>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</p>
</Switch>
</FormControl>
)}
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Database"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};