mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 07:26:45 +00:00
304 lines
9.5 KiB
TypeScript
304 lines
9.5 KiB
TypeScript
import { useState } from "react";
|
|
import { Controller, useForm } from "react-hook-form";
|
|
import { faCheck, faCopy, faRedo } 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, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
|
|
import { useTimedReset } from "@app/hooks";
|
|
import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api";
|
|
import { SecretSharingAccessType } from "@app/hooks/api/secretSharing";
|
|
|
|
// values in ms
|
|
const expiresInOptions = [
|
|
{ label: "5 min", value: 5 * 60 * 1000 },
|
|
{ label: "30 min", value: 30 * 60 * 1000 },
|
|
{ label: "1 hour", value: 60 * 60 * 1000 },
|
|
{ label: "1 day", value: 24 * 60 * 60 * 1000 },
|
|
{ label: "7 days", value: 7 * 24 * 60 * 60 * 1000 },
|
|
{ label: "14 days", value: 14 * 24 * 60 * 60 * 1000 },
|
|
{ label: "30 days", value: 30 * 24 * 60 * 60 * 1000 }
|
|
];
|
|
|
|
const viewLimitOptions = [
|
|
{ label: "1", value: 1 },
|
|
{ label: "Unlimited", value: -1 }
|
|
];
|
|
|
|
const schema = z.object({
|
|
name: z.string().optional(),
|
|
password: z.string().optional(),
|
|
secret: z.string().min(1),
|
|
expiresIn: z.string(),
|
|
viewLimit: z.string(),
|
|
accessType: z.nativeEnum(SecretSharingAccessType).optional()
|
|
});
|
|
|
|
export type FormData = z.infer<typeof schema>;
|
|
|
|
type Props = {
|
|
isPublic: boolean; // whether or not this is a public (non-authenticated) secret sharing form
|
|
value?: string;
|
|
allowSecretSharingOutsideOrganization?: boolean;
|
|
maxSharedSecretLifetime?: number;
|
|
maxSharedSecretViewLimit?: number | null;
|
|
};
|
|
|
|
export const ShareSecretForm = ({
|
|
isPublic,
|
|
value,
|
|
allowSecretSharingOutsideOrganization = true,
|
|
maxSharedSecretLifetime,
|
|
maxSharedSecretViewLimit
|
|
}: Props) => {
|
|
const [secretLink, setSecretLink] = useState("");
|
|
const [, isCopyingSecret, setCopyTextSecret] = useTimedReset<string>({
|
|
initialState: "Copy to clipboard"
|
|
});
|
|
|
|
const publicSharedSecretCreator = useCreatePublicSharedSecret();
|
|
const privateSharedSecretCreator = useCreateSharedSecret();
|
|
const createSharedSecret = isPublic ? publicSharedSecretCreator : privateSharedSecretCreator;
|
|
|
|
const filteredExpiresInOptions = maxSharedSecretLifetime
|
|
? expiresInOptions.filter((v) => v.value / 1000 <= maxSharedSecretLifetime)
|
|
: expiresInOptions;
|
|
|
|
const filteredViewLimitOptions = maxSharedSecretViewLimit
|
|
? viewLimitOptions.filter((v) => v.value > 0 && v.value <= maxSharedSecretViewLimit)
|
|
: viewLimitOptions;
|
|
|
|
const {
|
|
control,
|
|
reset,
|
|
handleSubmit,
|
|
formState: { isSubmitting }
|
|
} = useForm<FormData>({
|
|
resolver: zodResolver(schema),
|
|
defaultValues: {
|
|
secret: value || ""
|
|
}
|
|
});
|
|
|
|
const onFormSubmit = async ({
|
|
name,
|
|
password,
|
|
secret,
|
|
expiresIn,
|
|
viewLimit,
|
|
accessType
|
|
}: FormData) => {
|
|
try {
|
|
const expiresAt = new Date(new Date().getTime() + Number(expiresIn));
|
|
|
|
const { id } = await createSharedSecret.mutateAsync({
|
|
name,
|
|
password,
|
|
secretValue: secret,
|
|
expiresAt,
|
|
expiresAfterViews: viewLimit === "-1" ? undefined : Number(viewLimit),
|
|
accessType
|
|
});
|
|
|
|
const link = `${window.location.origin}/shared/secret/${id}`;
|
|
|
|
setSecretLink(link);
|
|
reset();
|
|
|
|
navigator.clipboard.writeText(link);
|
|
setCopyTextSecret("secret");
|
|
|
|
createNotification({
|
|
text: "Shared secret link copied to clipboard.",
|
|
type: "success"
|
|
});
|
|
} catch (error) {
|
|
console.error(error);
|
|
createNotification({
|
|
text: "Failed to create a shared secret.",
|
|
type: "error"
|
|
});
|
|
}
|
|
};
|
|
|
|
const hasSecretLink = Boolean(secretLink);
|
|
|
|
return !hasSecretLink ? (
|
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
|
{!isPublic && (
|
|
<Controller
|
|
control={control}
|
|
name="name"
|
|
render={({ field, fieldState: { error } }) => (
|
|
<FormControl
|
|
label="Name (Optional)"
|
|
isError={Boolean(error)}
|
|
errorText={error?.message}
|
|
>
|
|
<Input
|
|
{...field}
|
|
placeholder="API Key"
|
|
type="text"
|
|
autoComplete="off"
|
|
autoCorrect="off"
|
|
spellCheck="false"
|
|
/>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
)}
|
|
<Controller
|
|
control={control}
|
|
name="secret"
|
|
render={({ field, fieldState: { error } }) => (
|
|
<FormControl
|
|
label="Your Secret"
|
|
isError={Boolean(error)}
|
|
errorText={error?.message}
|
|
className="mb-2"
|
|
isRequired
|
|
>
|
|
<textarea
|
|
placeholder="Enter sensitive data to share via an encrypted link..."
|
|
{...field}
|
|
className="h-40 min-h-[70px] w-full rounded-md border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5 text-bunker-300 outline-none transition-all placeholder:text-mineshaft-400 hover:border-primary-400/30 focus:border-primary-400/50 group-hover:mr-2"
|
|
disabled={value !== undefined}
|
|
/>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
<Controller
|
|
control={control}
|
|
name="password"
|
|
render={({ field, fieldState: { error } }) => (
|
|
<FormControl
|
|
label="Password"
|
|
isError={Boolean(error)}
|
|
errorText={error?.message}
|
|
isOptional
|
|
>
|
|
<Input
|
|
{...field}
|
|
placeholder="Password"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
autoCorrect="off"
|
|
spellCheck="false"
|
|
aria-autocomplete="none"
|
|
data-form-type="other"
|
|
/>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
<Controller
|
|
control={control}
|
|
name="expiresIn"
|
|
defaultValue={filteredExpiresInOptions[
|
|
Math.min(filteredExpiresInOptions.length - 1, 2)
|
|
].value.toString()}
|
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
<FormControl label="Expires In" errorText={error?.message} isError={Boolean(error)}>
|
|
<Select
|
|
defaultValue={field.value}
|
|
{...field}
|
|
onValueChange={(e) => onChange(e)}
|
|
className="w-full"
|
|
>
|
|
{filteredExpiresInOptions.map(({ label, value: expiresInValue }) => (
|
|
<SelectItem value={String(expiresInValue || "")} key={label}>
|
|
{label}
|
|
</SelectItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
<Controller
|
|
control={control}
|
|
name="viewLimit"
|
|
defaultValue={filteredViewLimitOptions[
|
|
filteredViewLimitOptions.length - 1
|
|
].value.toString()}
|
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
<FormControl label="Max Views" errorText={error?.message} isError={Boolean(error)}>
|
|
<Select
|
|
defaultValue={field.value}
|
|
{...field}
|
|
onValueChange={(e) => onChange(e)}
|
|
className="w-full"
|
|
>
|
|
{filteredViewLimitOptions.map(({ label, value: viewLimitValue }) => (
|
|
<SelectItem value={String(viewLimitValue || "")} key={label}>
|
|
{label}
|
|
</SelectItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
{!isPublic && (
|
|
<Controller
|
|
control={control}
|
|
name="accessType"
|
|
defaultValue={SecretSharingAccessType.Organization}
|
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
<FormControl label="General Access" errorText={error?.message} isError={Boolean(error)}>
|
|
<Select
|
|
defaultValue={field.value}
|
|
{...field}
|
|
onValueChange={(e) => onChange(e)}
|
|
className="w-full"
|
|
>
|
|
{allowSecretSharingOutsideOrganization && (
|
|
<SelectItem value={SecretSharingAccessType.Anyone}>Anyone</SelectItem>
|
|
)}
|
|
<SelectItem value={SecretSharingAccessType.Organization}>
|
|
People within your organization
|
|
</SelectItem>
|
|
</Select>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
)}
|
|
<Button
|
|
className="mt-4"
|
|
size="sm"
|
|
type="submit"
|
|
isLoading={isSubmitting}
|
|
isDisabled={isSubmitting}
|
|
>
|
|
Create Secret Link
|
|
</Button>
|
|
</form>
|
|
) : (
|
|
<>
|
|
<div className="mr-2 flex items-center justify-end rounded-md bg-white/[0.05] p-2 text-base text-gray-400">
|
|
<p className="mr-4 break-all">{secretLink}</p>
|
|
<IconButton
|
|
ariaLabel="copy icon"
|
|
colorSchema="secondary"
|
|
className="group relative ml-2"
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(secretLink);
|
|
setCopyTextSecret("Copied");
|
|
}}
|
|
>
|
|
<FontAwesomeIcon icon={isCopyingSecret ? faCheck : faCopy} />
|
|
</IconButton>
|
|
</div>
|
|
<Button
|
|
className="mt-4 w-full bg-mineshaft-700 py-3 text-bunker-200"
|
|
colorSchema="primary"
|
|
variant="outline_bg"
|
|
size="sm"
|
|
onClick={() => setSecretLink("")}
|
|
rightIcon={<FontAwesomeIcon icon={faRedo} className="pl-2" />}
|
|
>
|
|
Share Another Secret
|
|
</Button>
|
|
</>
|
|
);
|
|
};
|