mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-17 02:47:35 +00:00
Continue secret sharing
This commit is contained in:
@@ -1,180 +1,36 @@
|
||||
import { useMemo } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { decryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { usePopUp, useTimedReset } from "@app/hooks";
|
||||
import { useGetActiveSharedSecretByIdAndHashedHex } from "@app/hooks/api/secretSharing";
|
||||
|
||||
import { AddShareSecretModal } from "../ShareSecretPage/components/AddShareSecretModal";
|
||||
import { SecretTable } from "./components";
|
||||
|
||||
// note: isNewSession: controls if the user is sharing a new secret or viewing a shared secret
|
||||
|
||||
export const ShareSecretPublicPage = ({ isNewSession }: { isNewSession: boolean }) => {
|
||||
const router = useRouter();
|
||||
const { id, key: urlEncodedPublicKey } = router.query;
|
||||
const [hashedHex, key] = urlEncodedPublicKey
|
||||
? urlEncodedPublicKey.toString().split("-")
|
||||
: ["", ""];
|
||||
|
||||
const publicKey = decodeURIComponent(urlEncodedPublicKey as string);
|
||||
const { isLoading, data } = useGetActiveSharedSecretByIdAndHashedHex(
|
||||
id as string,
|
||||
hashedHex as string
|
||||
);
|
||||
const accessType = data?.accessType;
|
||||
const orgName = data?.orgName;
|
||||
|
||||
const decryptedSecret = useMemo(() => {
|
||||
if (data && data.encryptedValue && publicKey) {
|
||||
const res = decryptSymmetric({
|
||||
ciphertext: data.encryptedValue,
|
||||
iv: data.iv,
|
||||
tag: data.tag,
|
||||
key
|
||||
});
|
||||
return res;
|
||||
}
|
||||
return "";
|
||||
}, [data, publicKey]);
|
||||
|
||||
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
|
||||
initialState: false
|
||||
});
|
||||
|
||||
const copyUrlToClipboard = () => {
|
||||
navigator.clipboard.writeText(decryptedSecret);
|
||||
setIsUrlCopied(true);
|
||||
};
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["createSharedSecret"] as const);
|
||||
import { ShareSecretForm } from "./components";
|
||||
|
||||
export const ShareSecretPublicPage = () => {
|
||||
return (
|
||||
<div className="flex h-screen flex-col overflow-y-auto bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
|
||||
<Head>
|
||||
<title>Infisical | Secret Sharing</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<div className="flex w-full flex-grow items-center justify-center dark:[color-scheme:dark]">
|
||||
<div className="relative">
|
||||
<div className="mb-4 flex justify-center pt-8">
|
||||
<Link href="https://infisical.com">
|
||||
<Image
|
||||
src="/images/gradientLogo.svg"
|
||||
height={90}
|
||||
width={120}
|
||||
alt="Infisical logo"
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex w-full justify-center">
|
||||
<h1
|
||||
className={`${
|
||||
id ? "mb-4 max-w-sm" : "mt-4 mb-6 max-w-md"
|
||||
} bg-gradient-to-b from-white to-bunker-200 bg-clip-text px-4 text-center text-3xl font-medium text-transparent`}
|
||||
<div className="flex h-screen flex-col justify-between bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
|
||||
<div />
|
||||
<div className="mx-auto w-full max-w-lg">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="bg-gradient-to-b from-white to-bunker-200 bg-clip-text px-4 text-center text-3xl font-medium text-transparent">
|
||||
Share a Secret
|
||||
</h1>
|
||||
<p className="text-md">
|
||||
Powered by{" "}
|
||||
<a
|
||||
href="https://github.com/infisical/infisical"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
|
||||
>
|
||||
{id
|
||||
? "Someone shared a secret via Infisical with you"
|
||||
: "Share a secret via Infisical"}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="m-auto mt-4 flex w-full max-w-2xl justify-center px-6">
|
||||
{id && (
|
||||
<SecretTable
|
||||
isLoading={isLoading}
|
||||
decryptedSecret={decryptedSecret}
|
||||
isUrlCopied={isUrlCopied}
|
||||
copyUrlToClipboard={copyUrlToClipboard}
|
||||
accessType={accessType}
|
||||
orgName={orgName}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isNewSession && (
|
||||
<div className="px-0 sm:px-6">
|
||||
<AddShareSecretModal
|
||||
popUp={popUp}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
inModal={false}
|
||||
isPublic
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!isNewSession && (
|
||||
<div className="flex flex-1 flex-col items-center justify-center px-6 pt-4">
|
||||
<a
|
||||
href="https://share.infisical.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
|
||||
>
|
||||
<Button
|
||||
className="w-full bg-mineshaft-700 py-3 text-bunker-200"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
onClick={() => {}}
|
||||
rightIcon={<FontAwesomeIcon icon={faArrowRight} className="pl-2" />}
|
||||
>
|
||||
Share your own Secret
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<div className="m-auto my-6 flex w-full max-w-xl justify-center px-4 sm:my-8">
|
||||
<div className="w-full border-t border-mineshaft-600" />
|
||||
</div>
|
||||
<div className="m-auto flex max-w-2xl flex-col items-center justify-center px-4 sm:px-6">
|
||||
<div className="m-auto mb-12 flex w-full max-w-2xl flex-col justify-center rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
|
||||
<p className="w-full pb-2 text-lg font-semibold text-mineshaft-100 md:pb-3 md:text-xl">
|
||||
Open source{" "}
|
||||
<span className="bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent">
|
||||
secret management
|
||||
</span>{" "}
|
||||
for developers
|
||||
</p>
|
||||
<div className="flex flex-col gap-x-4 sm:flex-row">
|
||||
<p className="md:text-md text-md">
|
||||
<a
|
||||
href="https://github.com/infisical/infisical"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
|
||||
>
|
||||
Infisical
|
||||
</a>{" "}
|
||||
is the all-in-one secret management platform to securely manage secrets, configs,
|
||||
and certificates across your team and infrastructure.
|
||||
</p>
|
||||
<Link href="https://infisical.com">
|
||||
<span className="mt-4 h-min w-[17.5rem] cursor-pointer rounded-md border border-mineshaft-400/40 bg-mineshaft-600 py-2 px-3 duration-200 hover:border-primary/60 hover:bg-primary/20 hover:text-white">
|
||||
Try Infisical <FontAwesomeIcon icon={faArrowRight} className="pl-1" />
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AddShareSecretModal
|
||||
popUp={popUp}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
isPublic
|
||||
inModal
|
||||
/>
|
||||
Infisical →
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||
<ShareSecretForm isPublic />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-auto flex w-full items-center justify-center bg-mineshaft-600 p-2">
|
||||
<div className="w-full bg-mineshaft-600 p-2">
|
||||
<p className="text-center text-sm text-mineshaft-300">
|
||||
© 2024{" "}
|
||||
Made with ❤️ by{" "}
|
||||
<a className="text-primary" href="https://infisical.com">
|
||||
Infisical
|
||||
</a>
|
||||
. All rights reserved.
|
||||
<br />
|
||||
156 2nd st, 3rd Floor, San Francisco, California, 94105, United States. 🇺🇸
|
||||
</p>
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import { faArrowRight, faCheck, faCopy, faEye, faEyeSlash, faKey } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button, EmptyState, IconButton, Td, Tr } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { SecretSharingAccessType } from "@app/hooks/api/secretSharing/types";
|
||||
|
||||
type Props = {
|
||||
isLoading: boolean;
|
||||
decryptedSecret: string;
|
||||
isUrlCopied: boolean;
|
||||
copyUrlToClipboard: () => void;
|
||||
accessType?: SecretSharingAccessType;
|
||||
orgName?: string;
|
||||
};
|
||||
|
||||
const replaceContentWithDot = (str: string) => {
|
||||
let finalStr = "";
|
||||
for (let i = 0; i < str.length; i += 1) {
|
||||
const char = str.at(i);
|
||||
finalStr += char === "\n" ? "\n" : "*";
|
||||
}
|
||||
return finalStr;
|
||||
};
|
||||
|
||||
export const SecretTable = ({
|
||||
isLoading,
|
||||
decryptedSecret,
|
||||
isUrlCopied,
|
||||
copyUrlToClipboard,
|
||||
accessType,
|
||||
orgName
|
||||
}: Props) => {
|
||||
const [isVisible, setIsVisible] = useToggle(false);
|
||||
const title = orgName
|
||||
? (<p>Someone from <strong>{orgName}</strong> organization has shared a secret with you</p>)
|
||||
: (<p>You need to be logged in to view this secret</p>);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center rounded-md border border-solid border-mineshaft-700 bg-mineshaft-800 p-2">
|
||||
{isLoading && <div className="bg-mineshaft-800 text-center text-bunker-400">Loading...</div>}
|
||||
{!isLoading && !decryptedSecret && accessType !== SecretSharingAccessType.Organization && (
|
||||
<Tr>
|
||||
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
|
||||
<EmptyState title="Secret has either expired or does not exist!" icon={faKey} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
{!isLoading && !decryptedSecret && accessType === SecretSharingAccessType.Organization && (
|
||||
<Tr>
|
||||
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-4000">
|
||||
<EmptyState title={title} icon={faKey}>
|
||||
<div className="flex flex-1 flex-col items-center justify-center pt-6">
|
||||
<a
|
||||
href="/login"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
size="sm"
|
||||
onClick={() => {}}
|
||||
rightIcon={<FontAwesomeIcon icon={faArrowRight} className="ml-2" />}
|
||||
>
|
||||
Login into <strong>{orgName}</strong> to view this secret
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</EmptyState>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
{!isLoading && decryptedSecret && (
|
||||
<div className="dark relative flex h-full w-full items-center overflow-y-auto rounded-md border border-mineshaft-700 bg-mineshaft-900 p-2 pr-2 md:p-3">
|
||||
<div
|
||||
className={`thin-scrollbar flex h-full max-h-44 w-full flex-1 overflow-y-scroll ${
|
||||
isVisible ? "break-words" : "break-all"
|
||||
} pr-4 dark:[color-scheme:dark]`}
|
||||
>
|
||||
<div className="align-center flex w-full min-w-full whitespace-pre-line">
|
||||
{isVisible ? decryptedSecret : replaceContentWithDot(decryptedSecret)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute top-1 right-0 mx-1 flex max-h-8 sm:top-2 sm:right-5">
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
colorSchema="primary"
|
||||
ariaLabel="copy to clipboard"
|
||||
onClick={copyUrlToClipboard}
|
||||
className="mr-1 flex max-h-8 items-center rounded"
|
||||
size="xs"
|
||||
>
|
||||
<FontAwesomeIcon className="pr-2" icon={isUrlCopied ? faCheck : faCopy} /> Copy
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
colorSchema="primary"
|
||||
ariaLabel="toggle visibility"
|
||||
onClick={() => setIsVisible.toggle()}
|
||||
className="flex max-h-8 items-center rounded"
|
||||
size="xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={isVisible ? faEyeSlash : faEye} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,252 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
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 { encryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||
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(),
|
||||
secret: z.string(),
|
||||
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;
|
||||
};
|
||||
|
||||
export const ShareSecretForm = ({ isPublic, value }: 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 {
|
||||
control,
|
||||
reset,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
secret: value || ""
|
||||
}
|
||||
});
|
||||
|
||||
const onFormSubmit = async ({ name, secret, expiresIn, viewLimit, accessType }: FormData) => {
|
||||
try {
|
||||
const expiresAt = new Date(new Date().getTime() + Number(expiresIn));
|
||||
|
||||
const key = crypto.randomBytes(16).toString("hex");
|
||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||
plaintext: secret,
|
||||
key
|
||||
});
|
||||
|
||||
const { id } = await createSharedSecret.mutateAsync({
|
||||
name,
|
||||
encryptedValue: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
expiresAt,
|
||||
expiresAfterViews: viewLimit === "-1" ? undefined : Number(viewLimit),
|
||||
accessType
|
||||
});
|
||||
|
||||
setSecretLink(`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(key)}`);
|
||||
|
||||
reset();
|
||||
|
||||
setCopyTextSecret("secret");
|
||||
createNotification({
|
||||
text: "Successfully created a shared secret",
|
||||
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" />
|
||||
</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 py-1.5 px-2 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="expiresIn"
|
||||
defaultValue="3600000"
|
||||
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"
|
||||
>
|
||||
{expiresInOptions.map(({ label, value: expiresInValue }) => (
|
||||
<SelectItem value={String(expiresInValue || "")} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="viewLimit"
|
||||
defaultValue="-1"
|
||||
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"
|
||||
>
|
||||
{viewLimitOptions.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"
|
||||
>
|
||||
<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="mb-4 flex items-center justify-between">
|
||||
<h2>Secret Link</h2>
|
||||
<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>
|
||||
<div className="mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{secretLink}</p>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1 +1 @@
|
||||
export { SecretTable } from "./SecretTable";
|
||||
export { ShareSecretForm } from "./ShareSecretForm";
|
||||
|
||||
Reference in New Issue
Block a user