mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-18 04:58:28 +00:00
feat(secret-rotation): implemented frontend ui for secret rotation
This commit is contained in:
+149
@@ -0,0 +1,149 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
import { Modal, ModalContent, Step, Stepper } from "@app/components/v2";
|
||||
import { useCreateSecretRotation } from "@app/hooks/api";
|
||||
import { TSecretRotationProvider } from "@app/hooks/api/types";
|
||||
|
||||
import { useNotificationContext } from "~/components/context/Notifications/NotificationProvider";
|
||||
|
||||
import { GeneralDetailsForm, TFormSchema as TGeneralFormSchema } from "./steps/GeneralDetailsForm";
|
||||
import { RotationInputForm } from "./steps/RotationInputForm";
|
||||
import {
|
||||
RotationOutputForm,
|
||||
TFormSchema as TRotationOutputSchema
|
||||
} from "./steps/RotationOutputForm";
|
||||
|
||||
const WIZARD_STEPS = [
|
||||
{
|
||||
title: "General"
|
||||
},
|
||||
{
|
||||
title: "Inputs"
|
||||
},
|
||||
{
|
||||
title: "Secret Mapping"
|
||||
}
|
||||
];
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
onToggle: (isOpen: boolean) => void;
|
||||
customProvider?: string;
|
||||
workspaceId: string;
|
||||
provider: TSecretRotationProvider;
|
||||
};
|
||||
|
||||
export const CreateRotationForm = ({
|
||||
isOpen,
|
||||
onToggle,
|
||||
provider,
|
||||
workspaceId,
|
||||
customProvider
|
||||
}: Props) => {
|
||||
const [wizardStep, setWizardStep] = useState(0);
|
||||
const wizardData = useRef<{
|
||||
general?: TGeneralFormSchema;
|
||||
input?: Record<string, string>;
|
||||
output?: TRotationOutputSchema;
|
||||
}>({});
|
||||
const { createNotification } = useNotificationContext();
|
||||
|
||||
const { mutateAsync: createSecretRotation } = useCreateSecretRotation();
|
||||
|
||||
const handleFormCancel = () => {
|
||||
onToggle(false);
|
||||
setWizardStep(0);
|
||||
wizardData.current = {};
|
||||
};
|
||||
|
||||
const handleFormSubmit = async () => {
|
||||
if (!wizardData.current.general || !wizardData.current.input || !wizardData.current.output)
|
||||
return;
|
||||
try {
|
||||
await createSecretRotation({
|
||||
workspaceId,
|
||||
provider: provider.name,
|
||||
customProvider,
|
||||
secretPath: wizardData.current.general.secretPath,
|
||||
environment: wizardData.current.general.environment,
|
||||
interval: wizardData.current.general.interval,
|
||||
inputs: wizardData.current.input,
|
||||
outputs: wizardData.current.output
|
||||
});
|
||||
setWizardStep(0);
|
||||
onToggle(false);
|
||||
wizardData.current = {};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create secret rotation"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(state) => {
|
||||
onToggle(state);
|
||||
setWizardStep(0);
|
||||
wizardData.current = {};
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title={`Secret rotation for ${provider.name}`}
|
||||
subTitle="Provide the required inputs needed for the rotation"
|
||||
className="max-w-2xl"
|
||||
>
|
||||
<Stepper activeStep={wizardStep} direction="horizontal" className="mb-4">
|
||||
{WIZARD_STEPS.map(({ title }, index) => (
|
||||
<Step title={title} key={`wizard-stepper-rotation-${index + 1}`} />
|
||||
))}
|
||||
</Stepper>
|
||||
<AnimatePresence exitBeforeEnter>
|
||||
{wizardStep === 0 && (
|
||||
<motion.div
|
||||
key="general-step"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<GeneralDetailsForm
|
||||
onCancel={handleFormCancel}
|
||||
onSubmit={(data) => {
|
||||
wizardData.current.general = data;
|
||||
setWizardStep((state) => state + 1);
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{wizardStep === 1 && (
|
||||
<RotationInputForm
|
||||
onCancel={handleFormCancel}
|
||||
onSubmit={(data) => {
|
||||
wizardData.current.input = data;
|
||||
setWizardStep((state) => state + 1);
|
||||
}}
|
||||
inputSchema={provider.template?.inputs || {}}
|
||||
/>
|
||||
)}
|
||||
{wizardStep === 2 && (
|
||||
<RotationOutputForm
|
||||
environment={wizardData.current.general?.environment || ""}
|
||||
secretPath={wizardData.current.general?.secretPath || "/"}
|
||||
outputSchema={provider.template?.outputs || {}}
|
||||
onCancel={handleFormCancel}
|
||||
onSubmit={async (data) => {
|
||||
wizardData.current.output = data;
|
||||
await handleFormSubmit();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { CreateRotationForm } from "./CreateRotationForm";
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
|
||||
const formSchema = z.object({
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
interval: z.number()
|
||||
});
|
||||
|
||||
export type TFormSchema = z.infer<typeof formSchema>;
|
||||
type Props = {
|
||||
onSubmit: (data: TFormSchema) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
export const GeneralDetailsForm = ({ onSubmit, onCancel }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const environments = currentWorkspace?.environments || [];
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
defaultValue={environments?.[0]?.slug}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<FormControl label="Environment">
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
defaultValue={environments?.[0]?.slug}
|
||||
position="popper"
|
||||
>
|
||||
{environments.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
>
|
||||
{sourceEnvironment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
defaultValue="/"
|
||||
render={({ field }) => (
|
||||
<FormControl className="capitalize" label="Secret path">
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="interval"
|
||||
defaultValue={15}
|
||||
render={({ field }) => (
|
||||
<FormControl className="capitalize" label="Rotation Interval (Days)">
|
||||
<Input
|
||||
{...field}
|
||||
min={1}
|
||||
type="number"
|
||||
onChange={(evt) => field.onChange(parseInt(evt.target.value, 10))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
|
||||
Next
|
||||
</Button>
|
||||
<Button onClick={onCancel} colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, FormControl, SecretInput } from "@app/components/v2";
|
||||
|
||||
type Props = {
|
||||
onSubmit: (data: Record<string, string>) => void;
|
||||
onCancel: () => void;
|
||||
inputSchema: {
|
||||
properties: Record<string, { type: string; helperText?: string; default?: string }>;
|
||||
required: string[];
|
||||
};
|
||||
};
|
||||
|
||||
const formSchema = z.record(z.string().trim().optional());
|
||||
|
||||
export const RotationInputForm = ({ onSubmit, onCancel, inputSchema }: Props) => {
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<Record<string, string>>({
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{Object.keys(inputSchema.properties || {}).map((inputName) => (
|
||||
<Controller
|
||||
control={control}
|
||||
name={inputName}
|
||||
key={`provider-input-${inputName}`}
|
||||
defaultValue={inputSchema.properties[inputName]?.default}
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
className="capitalize"
|
||||
key={`provider-input-${inputName}`}
|
||||
label={inputName.replaceAll("_", " ")}
|
||||
helperText={inputSchema.properties[inputName]?.helperText}
|
||||
>
|
||||
<SecretInput
|
||||
{...field}
|
||||
containerClassName="normal-case text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
||||
required={inputSchema.required.includes(inputName)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<div className="mt-8 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
|
||||
Next
|
||||
</Button>
|
||||
<Button onClick={onCancel} colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetProjectSecrets, useGetUserWsKey } from "@app/hooks/api";
|
||||
|
||||
const formSchema = z.record(z.string());
|
||||
|
||||
export type TFormSchema = z.infer<typeof formSchema>;
|
||||
type Props = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
outputSchema: Record<string, unknown>;
|
||||
onSubmit: (data: TFormSchema) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
export const RotationOutputForm = ({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
environment,
|
||||
secretPath,
|
||||
outputSchema = {}
|
||||
}: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?._id || "";
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
|
||||
const { data: userWsKey } = useGetUserWsKey(workspaceId);
|
||||
const { data: secrets } = useGetProjectSecrets({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
decryptFileKey: userWsKey!
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{Object.keys(outputSchema).map((outputName) => (
|
||||
<Controller
|
||||
key={`provider-output-${outputName}`}
|
||||
control={control}
|
||||
name={outputName}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<FormControl className="uppercase" label={outputName.replaceAll("_", " ")} isRequired>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
>
|
||||
{secrets?.map(({ key, _id }) => (
|
||||
<SelectItem value={_id} key={_id}>
|
||||
{key}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<div className="mt-8 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button onClick={onCancel} colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user