mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
336 lines
10 KiB
TypeScript
336 lines
10 KiB
TypeScript
import { ClipboardEvent } from "react";
|
|
import { Controller, useForm } from "react-hook-form";
|
|
import { subject } from "@casl/ability";
|
|
import { faTriangleExclamation, faWarning } from "@fortawesome/free-solid-svg-icons";
|
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { z } from "zod";
|
|
|
|
import { createNotification } from "@app/components/notifications";
|
|
import {
|
|
Button,
|
|
Checkbox,
|
|
FilterableSelect,
|
|
FormControl,
|
|
FormLabel,
|
|
Input,
|
|
Tooltip
|
|
} from "@app/components/v2";
|
|
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
|
import {
|
|
ProjectPermissionActions,
|
|
ProjectPermissionSub,
|
|
useProjectPermission,
|
|
useWorkspace
|
|
} from "@app/context";
|
|
import { getKeyValue } from "@app/helpers/parseEnvVar";
|
|
import {
|
|
useCreateFolder,
|
|
useCreateSecretV3,
|
|
useGetWsTags,
|
|
useUpdateSecretV3
|
|
} from "@app/hooks/api";
|
|
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
|
|
|
|
const typeSchema = z
|
|
.object({
|
|
key: z.string().trim().min(1, "Key is required"),
|
|
value: z.string().optional(),
|
|
environments: z.record(z.boolean().optional()),
|
|
tags: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).optional()
|
|
})
|
|
.refine((data) => data.key !== undefined, {
|
|
message: "Please enter secret name"
|
|
});
|
|
|
|
type TFormSchema = z.infer<typeof typeSchema>;
|
|
|
|
type Props = {
|
|
secretPath?: string;
|
|
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
|
|
// modal props
|
|
onClose: () => void;
|
|
};
|
|
|
|
export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }: Props) => {
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
control,
|
|
reset,
|
|
watch,
|
|
setValue,
|
|
formState: { isSubmitting, errors }
|
|
} = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) });
|
|
const newSecretKey = watch("key");
|
|
|
|
const { currentWorkspace } = useWorkspace();
|
|
const { permission } = useProjectPermission();
|
|
const canReadTags = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
|
const workspaceId = currentWorkspace?.id || "";
|
|
const environments = currentWorkspace?.environments || [];
|
|
|
|
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
|
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
|
|
const { mutateAsync: createFolder } = useCreateFolder();
|
|
const { data: projectTags, isLoading: isTagsLoading } = useGetWsTags(
|
|
canReadTags ? workspaceId : ""
|
|
);
|
|
|
|
const handleFormSubmit = async ({ key, value, environments: selectedEnv, tags }: TFormSchema) => {
|
|
const environmentsSelected = environments.filter(({ slug }) => selectedEnv[slug]);
|
|
const isEnvironmentsSelected = environmentsSelected.length;
|
|
|
|
if (!isEnvironmentsSelected) {
|
|
createNotification({ type: "error", text: "Select at least one environment" });
|
|
return;
|
|
}
|
|
|
|
const promises = environmentsSelected.map(async (env) => {
|
|
const environment = env.slug;
|
|
// create folder if not existing
|
|
if (secretPath !== "/") {
|
|
// /hello/world -> [hello","world"]
|
|
const pathSegment = secretPath.split("/").filter(Boolean);
|
|
const parentPath = `/${pathSegment.slice(0, -1).join("/")}`;
|
|
const folderName = pathSegment.at(-1);
|
|
const canCreateFolder = permission.can(
|
|
ProjectPermissionActions.Create,
|
|
subject(ProjectPermissionSub.SecretFolders, {
|
|
environment: env.slug,
|
|
secretPath: parentPath
|
|
})
|
|
);
|
|
|
|
if (folderName && parentPath && canCreateFolder) {
|
|
await createFolder({
|
|
projectId: workspaceId,
|
|
path: parentPath,
|
|
environment,
|
|
name: folderName
|
|
});
|
|
}
|
|
}
|
|
|
|
const isEdit = getSecretByKey(environment, key) !== undefined;
|
|
if (isEdit) {
|
|
return {
|
|
...(await updateSecretV3({
|
|
environment,
|
|
workspaceId,
|
|
secretPath,
|
|
secretKey: key,
|
|
secretValue: value || "",
|
|
type: SecretType.Shared,
|
|
tagIds: tags?.map((el) => el.value)
|
|
})),
|
|
environment
|
|
};
|
|
}
|
|
|
|
return {
|
|
...(await createSecretV3({
|
|
environment,
|
|
workspaceId,
|
|
secretPath,
|
|
secretKey: key,
|
|
secretValue: value || "",
|
|
secretComment: "",
|
|
type: SecretType.Shared,
|
|
tagIds: tags?.map((el) => el.value)
|
|
})),
|
|
environment
|
|
};
|
|
});
|
|
|
|
const results = await Promise.allSettled(promises);
|
|
const forApprovalEnvs = results
|
|
.map((result) =>
|
|
result.status === "fulfilled" && "approval" in result.value
|
|
? result.value.environment
|
|
: undefined
|
|
)
|
|
.filter(Boolean) as string[];
|
|
|
|
const updatedEnvs = results
|
|
.map((result) =>
|
|
result.status === "fulfilled" && !("approval" in result.value)
|
|
? result.value.environment
|
|
: undefined
|
|
)
|
|
.filter(Boolean) as string[];
|
|
|
|
if (forApprovalEnvs.length) {
|
|
createNotification({
|
|
type: "info",
|
|
text: `Change request submitted for ${
|
|
forApprovalEnvs.length > 1 ? "environments" : "environment"
|
|
}: ${forApprovalEnvs.join(", ")}`
|
|
});
|
|
}
|
|
|
|
if (updatedEnvs.length) {
|
|
createNotification({
|
|
type: "success",
|
|
text: `Secrets created in ${
|
|
updatedEnvs.length > 1 ? "environments" : "environment"
|
|
}: ${updatedEnvs.join(", ")}`
|
|
});
|
|
}
|
|
|
|
if (!updatedEnvs.length && !forApprovalEnvs.length) {
|
|
createNotification({
|
|
type: "error",
|
|
text: "Failed to create secrets"
|
|
});
|
|
} else {
|
|
onClose();
|
|
reset();
|
|
}
|
|
};
|
|
|
|
const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {
|
|
e.preventDefault();
|
|
const delimitters = [":", "="];
|
|
const pastedContent = e.clipboardData.getData("text");
|
|
const { key, value } = getKeyValue(pastedContent, delimitters);
|
|
|
|
setValue("key", key);
|
|
setValue("value", value);
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
|
<FormControl
|
|
label="Key"
|
|
isRequired
|
|
isError={Boolean(errors?.key)}
|
|
errorText={errors?.key?.message}
|
|
>
|
|
<Input
|
|
{...register("key")}
|
|
placeholder="Type your secret name"
|
|
onPaste={handlePaste}
|
|
autoCapitalization={currentWorkspace?.autoCapitalization}
|
|
/>
|
|
</FormControl>
|
|
<Controller
|
|
control={control}
|
|
name="value"
|
|
render={({ field }) => (
|
|
<FormControl
|
|
label="Value"
|
|
isError={Boolean(errors?.value)}
|
|
errorText={errors?.value?.message}
|
|
>
|
|
<InfisicalSecretInput
|
|
{...field}
|
|
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
|
/>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
<Controller
|
|
control={control}
|
|
name="tags"
|
|
render={({ field }) => (
|
|
<FormControl
|
|
label="Tags"
|
|
isError={Boolean(errors?.value)}
|
|
errorText={errors?.value?.message}
|
|
helperText={
|
|
!canReadTags ? (
|
|
<div className="flex items-center space-x-2">
|
|
<FontAwesomeIcon icon={faTriangleExclamation} className="text-yellow-400" />
|
|
<span>You do not have permission to read tags.</span>
|
|
</div>
|
|
) : (
|
|
""
|
|
)
|
|
}
|
|
>
|
|
<FilterableSelect
|
|
className="w-full"
|
|
placeholder="Select tags to assign to secrets..."
|
|
isMulti
|
|
name="tagIds"
|
|
isDisabled={!canReadTags}
|
|
isLoading={isTagsLoading && canReadTags}
|
|
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
|
|
value={field.value}
|
|
onChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
<FormLabel label="Environments" className="mb-2" />
|
|
<div className="thin-scrollbar grid max-h-64 grid-cols-3 gap-4 overflow-auto py-2">
|
|
{environments
|
|
.filter((environmentSlug) =>
|
|
permission.can(
|
|
ProjectPermissionActions.Create,
|
|
subject(ProjectPermissionSub.Secrets, {
|
|
environment: environmentSlug.slug,
|
|
secretPath,
|
|
secretName: "*",
|
|
secretTags: ["*"]
|
|
})
|
|
)
|
|
)
|
|
.map((env) => {
|
|
return (
|
|
<Controller
|
|
name={`environments.${env.slug}`}
|
|
key={`secret-input-${env.slug}`}
|
|
control={control}
|
|
render={({ field }) => (
|
|
<Checkbox
|
|
isChecked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
id={`secret-input-${env.slug}`}
|
|
className="!justify-start"
|
|
>
|
|
<span className="flex w-full flex-row items-center justify-start whitespace-pre-wrap">
|
|
<span title={env.name} className="truncate">
|
|
{env.name}
|
|
</span>
|
|
<span>
|
|
{getSecretByKey(env.slug, newSecretKey) && (
|
|
<Tooltip
|
|
className="max-w-[150px]"
|
|
content="Secret already exists, and it will be overwritten"
|
|
>
|
|
<FontAwesomeIcon icon={faWarning} className="ml-1 text-yellow-400" />
|
|
</Tooltip>
|
|
)}
|
|
</span>
|
|
</span>
|
|
</Checkbox>
|
|
)}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="mt-7 flex items-center">
|
|
<Button
|
|
isDisabled={isSubmitting}
|
|
isLoading={isSubmitting}
|
|
key="layout-create-project-submit"
|
|
className="mr-4"
|
|
type="submit"
|
|
>
|
|
Create Secret
|
|
</Button>
|
|
<Button
|
|
key="layout-cancel-create-project"
|
|
onClick={onClose}
|
|
variant="plain"
|
|
colorSchema="secondary"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
);
|
|
};
|