fix: refactor and handle modify

This commit is contained in:
Meet
2024-10-02 20:51:02 +05:30
parent 27afad583b
commit e0199084ad
5 changed files with 434 additions and 444 deletions

View File

@@ -5,11 +5,10 @@ import { render } from "mustache";
import { customAlphabet } from "nanoid"; import { customAlphabet } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { LdapSchema, TDynamicProviderFns } from "./models"; import { LdapSchema, TDynamicProviderFns } from "./models";
import { BadRequestError } from "@app/lib/errors";
const ldif = require("ldif"); const ldif = require("ldif");
const generatePassword = () => { const generatePassword = () => {
@@ -17,8 +16,15 @@ const generatePassword = () => {
return customAlphabet(charset, 64)(); return customAlphabet(charset, 64)();
}; };
const encodePassword = (password?: string) => {
const quotedPassword = `"${password}"`;
const utf16lePassword = Buffer.from(quotedPassword, "utf16le");
const base64Password = utf16lePassword.toString("base64");
return base64Password;
}
const generateUsername = () => { const generateUsername = () => {
return alphaNumericNanoId(32); return alphaNumericNanoId(20);
}; };
const generateLDIF = ({ const generateLDIF = ({
@@ -30,9 +36,11 @@ const generateLDIF = ({
password?: string; password?: string;
ldifTemplate: string; ldifTemplate: string;
}): string => { }): string => {
const data = { const data = {
Username: username, Username: username,
Password: password Password: password,
EncodedPassword: encodePassword(password)
}; };
const ldif = render(ldifTemplate, data); const ldif = render(ldifTemplate, data);
@@ -47,7 +55,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
}; };
const getClient = async (providerInputs: z.infer<typeof LdapSchema>): Promise<ldapjs.Client> => { const getClient = async (providerInputs: z.infer<typeof LdapSchema>): Promise<ldapjs.Client> => {
return new Promise((resolve) => { return new Promise((resolve, reject) => {
const client = ldapjs.createClient({ const client = ldapjs.createClient({
url: providerInputs.url, url: providerInputs.url,
tlsOptions: { tlsOptions: {
@@ -57,18 +65,17 @@ export const LdapProvider = (): TDynamicProviderFns => {
reconnect: true, reconnect: true,
bindDN: providerInputs.binddn, bindDN: providerInputs.binddn,
bindCredentials: providerInputs.bindpass, bindCredentials: providerInputs.bindpass,
log: logger
}); });
client.on("error", (err) => { client.on("error", (err) => {
client.unbind(); client.unbind();
throw new Error(err.message); reject(new BadRequestError({ message: err.message }));
}); });
client.bind(providerInputs.binddn, providerInputs.bindpass, (err) => { client.bind(providerInputs.binddn, providerInputs.bindpass, (err) => {
if (err) { if (err) {
client.unbind(); client.unbind();
throw new Error(err.message); reject(new BadRequestError({ message: err.message }));
} else { } else {
resolve(client); resolve(client);
} }
@@ -100,10 +107,10 @@ export const LdapProvider = (): TDynamicProviderFns => {
attributes[attrName] = Array.isArray(attrValue) ? attrValue : [attrValue]; attributes[attrName] = Array.isArray(attrValue) ? attrValue : [attrValue];
}); });
response_dn = await new Promise((resolve) => { response_dn = await new Promise((resolve, reject) => {
client.add(dn, attributes, (err) => { client.add(dn, attributes, (err) => {
if (err) { if (err) {
throw new Error(err.message); reject(new BadRequestError({ message: err.message }));
} else { } else {
resolve(dn); resolve(dn);
} }
@@ -117,28 +124,27 @@ export const LdapProvider = (): TDynamicProviderFns => {
new ldapjs.Change({ new ldapjs.Change({
operation: change.operation || "replace", operation: change.operation || "replace",
modification: { modification: {
[change.attribute.attribute]: Array.isArray(change.value.value) type: change.attribute.attribute,
? change.value.value values: change.values.map((value: any) => value.value)
: [change.value.value]
} }
}) })
); );
}); });
response_dn = await new Promise((resolve) => { response_dn = await new Promise((resolve, reject) => {
client.modify(dn, changes, (err) => { client.modify(dn, changes, (err) => {
if (err) { if (err) {
throw new Error(err.message); reject(new BadRequestError({ message: err.message }));
} else { } else {
resolve(dn); resolve(dn);
} }
}); });
}); });
} else if (entry.type === "delete") { } else if (entry.type === "delete") {
response_dn = await new Promise((resolve) => { response_dn = await new Promise((resolve, reject) => {
client.del(dn, (err) => { client.del(dn, (err) => {
if (err) { if (err) {
throw new Error(err.message); reject(new BadRequestError({ message: err.message }));
} else { } else {
resolve(dn); resolve(dn);
} }
@@ -168,11 +174,11 @@ export const LdapProvider = (): TDynamicProviderFns => {
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } }; return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
} catch (err) { } catch (err) {
const rollbackLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rollbackLdif }); if (providerInputs.rollbackLdif) {
const rollbackLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rollbackLdif });
await executeLdif(client, rollbackLdif); await executeLdif(client, rollbackLdif);
}
throw new Error((err as Error).message); throw new BadRequestError({ message: (err as Error).message });
} }
}; };

View File

@@ -180,9 +180,9 @@ export const LdapSchema = z.object({
bindpass: z.string().trim().min(1), bindpass: z.string().trim().min(1),
ca: z.string().optional(), ca: z.string().optional(),
creationLdif: z.string().trim().min(1), creationLdif: z.string().min(1),
revocationLdif: z.string().trim().min(1), revocationLdif: z.string().min(1),
rollbackLdif: z.string().trim().min(1) rollbackLdif: z.string().optional()
}); });
export enum DynamicSecretProviders { export enum DynamicSecretProviders {

View File

@@ -199,7 +199,7 @@ export type TDynamicSecretProvider =
ca?: string | undefined; ca?: string | undefined;
creationLdif: string; creationLdif: string;
revocationLdif: string; revocationLdif: string;
rollbackLdif: string; rollbackLdif?: string;
}; };
}; };
; ;

View File

@@ -10,274 +10,270 @@ import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
const formSchema = z.object({ const formSchema = z.object({
provider: z.object({ provider: z.object({
url: z.string().trim().min(1), url: z.string().trim().min(1),
binddn: z.string().trim().min(1), binddn: z.string().trim().min(1),
bindpass: z.string().trim().min(1), bindpass: z.string().trim().min(1),
ca: z.string().optional(), ca: z.string().optional(),
creationLdif: z.string().trim().min(1), creationLdif: z.string().min(1),
revocationLdif: z.string().trim().min(1), revocationLdif: z.string().min(1),
rollbackLdif: z.string().trim().min(1), rollbackLdif: z.string().optional()
}), }),
defaultTTL: z.string().superRefine((val, ctx) => { defaultTTL: z.string().superRefine((val, ctx) => {
const valMs = ms(val); const valMs = ms(val);
if (valMs < 60 * 1000) if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
// a day // a day
if (valMs > 24 * 60 * 60 * 1000) if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
maxTTL: z
.string()
.optional()
.superRefine((val, ctx) => {
if (!val) return;
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
// a day
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}), }),
maxTTL: z name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
.string()
.optional()
.superRefine((val, ctx) => {
if (!val) return;
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
// a day
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
}); });
type TForm = z.infer<typeof formSchema>; type TForm = z.infer<typeof formSchema>;
type Props = { type Props = {
onCompleted: () => void; onCompleted: () => void;
onCancel: () => void; onCancel: () => void;
secretPath: string; secretPath: string;
projectSlug: string; projectSlug: string;
environment: string; environment: string;
}; };
export const LdapInputForm = ( export const LdapInputForm = ({
{ onCompleted,
onCompleted, onCancel,
onCancel, secretPath,
secretPath, projectSlug,
environment
}: Props) => {
const {
control,
formState: { isSubmitting },
handleSubmit
} = useForm<TForm>({
resolver: zodResolver(formSchema),
defaultValues: {
provider: {
url: "",
binddn: "",
bindpass: "",
ca: "",
creationLdif: "",
revocationLdif: "",
rollbackLdif: ""
}
}
});
const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isLoading) return;
try {
await createDynamicSecret.mutateAsync({
provider: { type: DynamicSecretProviders.Ldap, inputs: provider },
maxTTL,
name,
path: secretPath,
defaultTTL,
projectSlug, projectSlug,
environment, environmentSlug: environment
}: Props });
) => { onCompleted();
const { } catch (err) {
control, createNotification({
formState: { isSubmitting }, type: "error",
handleSubmit text: "Failed to create dynamic secret"
} = useForm<TForm>({ });
resolver: zodResolver(formSchema), }
defaultValues: { };
provider: {
url: "",
binddn: "",
bindpass: "",
ca: "",
creationLdif: "",
revocationLdif: "",
rollbackLdif: ""
},
}
});
const createDynamicSecret = useCreateDynamicSecret(); return (
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
<div>
<div className="flex items-center space-x-2">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="dynamic-secret" />
</FormControl>
)}
/>
</div>
<div className="w-32">
<Controller
control={control}
name="defaultTTL"
defaultValue="1h"
render={({ field, fieldState: { error } }) => (
<FormControl
label={<TtlFormLabel label="Default TTL" />}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div className="w-32">
<Controller
control={control}
name="maxTTL"
defaultValue="24h"
render={({ field, fieldState: { error } }) => (
<FormControl
label={<TtlFormLabel label="Max TTL" />}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
</div>
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration
</div>
<div className="flex flex-col">
<div className="flex items-center space-x-2">
<div className="flex-grow">
<Controller
control={control}
name="provider.url"
render={({ field, fieldState: { error } }) => (
<FormControl
label="URL"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { <Controller
// wait till previous request is finished control={control}
if (createDynamicSecret.isLoading) return; name="provider.binddn"
try { render={({ field, fieldState: { error } }) => (
await createDynamicSecret.mutateAsync({ <FormControl
provider: { type: DynamicSecretProviders.Ldap, inputs: provider }, label="Bind DN"
maxTTL, isError={Boolean(error?.message)}
name, errorText={error?.message}
path: secretPath, >
defaultTTL, <Input {...field} />
projectSlug, </FormControl>
environmentSlug: environment )}
}); />
onCompleted();
} catch (err) {
createNotification({
type: "error",
text: "Failed to create dynamic secret"
});
}
};
return ( <Controller
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off"> control={control}
<div> name="provider.bindpass"
<div className="flex items-center space-x-2"> render={({ field, fieldState: { error } }) => (
<div className="flex-grow"> <FormControl
<Controller label="Bind Password"
control={control} isError={Boolean(error?.message)}
defaultValue="" errorText={error?.message}
name="name" >
render={({ field, fieldState: { error } }) => ( <Input {...field} type="password" />
<FormControl </FormControl>
label="Secret Name" )}
isError={Boolean(error)} />
errorText={error?.message}
>
<Input {...field} placeholder="dynamic-secret" />
</FormControl>
)}
/>
</div>
<div className="w-32">
<Controller
control={control}
name="defaultTTL"
defaultValue="1h"
render={({ field, fieldState: { error } }) => (
<FormControl
label={<TtlFormLabel label="Default TTL" />}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div className="w-32">
<Controller
control={control}
name="maxTTL"
defaultValue="24h"
render={({ field, fieldState: { error } }) => (
<FormControl
label={<TtlFormLabel label="Max TTL" />}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
</div>
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration
</div>
<div className="flex flex-col">
<div className="flex items-center space-x-2">
<div className="flex-grow">
<Controller
control={control}
name="provider.url"
render={({ field, fieldState: { error } }) => (
<FormControl
label="URL"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<Controller <Controller
control={control} control={control}
name="provider.binddn" name="provider.ca"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Bind DN" label="CA"
isError={Boolean(error?.message)} isError={Boolean(error?.message)}
errorText={error?.message} errorText={error?.message}
> >
<Input {...field} /> <TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl> </FormControl>
)} )}
/> />
<Controller <Controller
control={control} control={control}
name="provider.bindpass" name="provider.creationLdif"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Bind Password" label="Creation LDIF"
isError={Boolean(error?.message)} isError={Boolean(error?.message)}
errorText={error?.message} errorText={error?.message}
> >
<Input {...field} /> <TextArea {...field} />
</FormControl> </FormControl>
)} )}
/> />
<Controller <Controller
control={control} control={control}
name="provider.ca" name="provider.revocationLdif"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="CA" label="Revocation LDIF"
isError={Boolean(error?.message)} isError={Boolean(error?.message)}
errorText={error?.message} errorText={error?.message}
> >
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." /> <TextArea {...field} />
</FormControl> </FormControl>
)} )}
/> />
<Controller <Controller
control={control} control={control}
name="provider.creationLdif" name="provider.rollbackLdif"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Creation LDIF" label="Rollback LDIF"
isError={Boolean(error?.message)} isError={Boolean(error?.message)}
errorText={error?.message} errorText={error?.message}
> >
<TextArea {...field} /> <TextArea {...field} />
</FormControl> </FormControl>
)} )}
/> />
</div>
<Controller </div>
control={control} </div>
name="provider.revocationLdif" </div>
render={({ field, fieldState: { error } }) => ( </div>
<FormControl <div className="mt-4 flex items-center space-x-4">
label="Revocation LDIF" <Button type="submit" isLoading={isSubmitting}>
isError={Boolean(error?.message)} Submit
errorText={error?.message} </Button>
> <Button variant="outline_bg" onClick={onCancel}>
<TextArea {...field} /> Cancel
</FormControl> </Button>
)} </div>
/> </form>
);
<Controller
control={control}
name="provider.rollbackLdif"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Rollback LDIF"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<TextArea {...field} />
</FormControl>
)}
/>
</div>
</div>
</div>
</div>
</div>
<div className="mt-4 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}>
Submit
</Button>
<Button variant="outline_bg" onClick={onCancel}>
Cancel
</Button>
</div>
</form>
)
}; };

View File

@@ -12,13 +12,13 @@ import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
const formSchema = z.object({ const formSchema = z.object({
inputs: z inputs: z
.object({ .object({
url: z.string().trim().min(1), url: z.string().trim().min(1),
binddn: z.string().trim().min(1), binddn: z.string().trim().min(1),
bindpass: z.string().trim().min(1), bindpass: z.string().trim().min(1),
ca: z.string().optional(), ca: z.string().optional(),
creationLdif: z.string().trim().min(1), creationLdif: z.string().min(1),
revocationLdif: z.string().trim().min(1), revocationLdif: z.string().min(1),
rollbackLdif: z.string().trim().min(1), rollbackLdif: z.string().optional()
}) })
.partial(), .partial(),
defaultTTL: z.string().superRefine((val, ctx) => { defaultTTL: z.string().superRefine((val, ctx) => {
@@ -58,62 +58,62 @@ type Props = {
}; };
export const EditDynamicSecretLdapForm = ({ export const EditDynamicSecretLdapForm = ({
onClose, onClose,
dynamicSecret, dynamicSecret,
environment, environment,
secretPath, secretPath,
projectSlug projectSlug
}: Props) => { }: Props) => {
const { const {
control, control,
formState: { isSubmitting }, formState: { isSubmitting },
handleSubmit handleSubmit
} = useForm<TForm>({ } = useForm<TForm>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
values: { values: {
defaultTTL: dynamicSecret.defaultTTL, defaultTTL: dynamicSecret.defaultTTL,
maxTTL: dynamicSecret.maxTTL, maxTTL: dynamicSecret.maxTTL,
newName: dynamicSecret.name, newName: dynamicSecret.name,
inputs: { inputs: {
...(dynamicSecret.inputs as TForm["inputs"]) ...(dynamicSecret.inputs as TForm["inputs"])
}
}
});
const updateDynamicSecret = useUpdateDynamicSecret();
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
// wait till previous request is finished
if (updateDynamicSecret.isLoading) return;
try {
await updateDynamicSecret.mutateAsync({
name: dynamicSecret.name,
path: secretPath,
projectSlug,
environmentSlug: environment,
data: {
maxTTL: maxTTL || undefined,
defaultTTL,
inputs,
newName: newName === dynamicSecret.name ? undefined : newName
} }
} });
}); onClose();
createNotification({
type: "success",
text: "Successfully updated dynamic secret"
});
} catch (err) {
createNotification({
type: "error",
text: "Failed to update dynamic secret"
});
}
};
const updateDynamicSecret = useUpdateDynamicSecret(); return (
<div>
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => { <form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
// wait till previous request is finished
if (updateDynamicSecret.isLoading) return;
try {
await updateDynamicSecret.mutateAsync({
name: dynamicSecret.name,
path: secretPath,
projectSlug,
environmentSlug: environment,
data: {
maxTTL: maxTTL || undefined,
defaultTTL,
inputs,
newName: newName === dynamicSecret.name ? undefined : newName
}
});
onClose();
createNotification({
type: "success",
text: "Successfully updated dynamic secret"
});
} catch (err) {
createNotification({
type: "error",
text: "Failed to update dynamic secret"
});
}
};
return (
<div>
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className="flex-grow"> <div className="flex-grow">
<Controller <Controller
@@ -162,107 +162,95 @@ export const EditDynamicSecretLdapForm = ({
</div> </div>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<Controller <Controller
control={control} control={control}
name="inputs.url" name="inputs.url"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl label="URL" isError={Boolean(error)} errorText={error?.message}>
label="URL" <Input {...field} />
isError={Boolean(error)} </FormControl>
errorText={error?.message} )}
> />
<Input {...field} /> <Controller
</FormControl> control={control}
)} name="inputs.binddn"
/> render={({ field, fieldState: { error } }) => (
<Controller <FormControl label="Bind DN" isError={Boolean(error)} errorText={error?.message}>
control={control} <Input {...field} />
name="inputs.binddn" </FormControl>
render={({ field, fieldState: { error } }) => ( )}
<FormControl />
label="Bind DN" <Controller
isError={Boolean(error)} control={control}
errorText={error?.message} name="inputs.bindpass"
> render={({ field, fieldState: { error } }) => (
<Input {...field} /> <FormControl
</FormControl> label="Bind Password"
)} isError={Boolean(error)}
/> errorText={error?.message}
<Controller >
control={control} <Input {...field} type="password" />
name="inputs.bindpass" </FormControl>
render={({ field, fieldState: { error } }) => ( )}
<FormControl />
label="Bind Password" <Controller
isError={Boolean(error)} control={control}
errorText={error?.message} name="inputs.ca"
> render={({ field, fieldState: { error } }) => (
<Input {...field} /> <FormControl label="CA" isError={Boolean(error)} errorText={error?.message}>
</FormControl> <TextArea {...field} />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
name="inputs.ca" control={control}
render={({ field, fieldState: { error } }) => ( name="inputs.creationLdif"
<FormControl render={({ field, fieldState: { error } }) => (
label="CA" <FormControl
isError={Boolean(error)} label="Creation LDIF"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<TextArea {...field} /> >
</FormControl> <TextArea {...field} />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
name="inputs.creationLdif" control={control}
render={({ field, fieldState: { error } }) => ( name="inputs.revocationLdif"
<FormControl render={({ field, fieldState: { error } }) => (
label="Creation LDIF" <FormControl
isError={Boolean(error)} label="Revocation LDIF"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<TextArea {...field} /> >
</FormControl> <TextArea {...field} />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
name="inputs.revocationLdif" control={control}
render={({ field, fieldState: { error } }) => ( name="inputs.rollbackLdif"
<FormControl render={({ field, fieldState: { error } }) => (
label="Revocation LDIF" <FormControl
isError={Boolean(error)} label="Rollback LDIF"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<TextArea {...field} /> >
</FormControl> <TextArea {...field} />
)} </FormControl>
/> )}
<Controller />
control={control} </div>
name="inputs.rollbackLdif" <div className="mt-4 flex items-center space-x-4">
render={({ field, fieldState: { error } }) => ( <Button type="submit" isLoading={isSubmitting}>
<FormControl Submit
label="Rollback LDIF" </Button>
isError={Boolean(error)} <Button variant="outline_bg" onClick={onClose}>
errorText={error?.message} Cancel
> </Button>
<TextArea {...field} /> </div>
</FormControl>
)}
/>
</div>
<div className="mt-4 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}>
Submit
</Button>
<Button variant="outline_bg" onClick={onClose}>
Cancel
</Button>
</div>
</form> </form>
</div> </div>
) );
} };