feat(secret-rotation): implemented frontend ui for secret rotation

This commit is contained in:
Akhil Mohan
2023-11-01 22:21:31 +05:30
parent 82e924baff
commit 97e4338335
24 changed files with 1250 additions and 1 deletions
@@ -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";
@@ -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>
);
};
@@ -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>
);
};
@@ -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>
);
};