mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
]);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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">;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user