feat: secret sharing

This commit is contained in:
ShubhamPalriwala
2024-05-29 14:10:46 +05:30
parent 81b0c8bc12
commit 387981ea87
42 changed files with 1497 additions and 2 deletions
@@ -0,0 +1,113 @@
import { useEffect, useMemo, useState } from "react";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { openSignedAssymmetric } from "@app/components/utilities/cryptography/crypto";
import { useToggle } from "@app/hooks";
import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing";
import { DragonMainImage, SecretTable } from "./components";
export const ShareSecretPublicPage = () => {
const router = useRouter();
const { id, key: urlEncodedPublicKey } = router.query;
const publicKey = decodeURIComponent(urlEncodedPublicKey as string);
useEffect(() => {
if (!id || !publicKey) {
router.push("/404");
}
}, [id, publicKey]);
const { isLoading, data } = useGetActiveSharedSecretById(id as string);
const decryptedSecret = useMemo(() => {
if (data && data.signedValue && publicKey) {
const res = openSignedAssymmetric({
signedMessage: data.signedValue,
publicKey: publicKey as string
});
return res;
}
return "";
}, [data, publicKey]);
const [timeLeft, setTimeLeft] = useState("");
const [isUrlCopied, setIsUrlCopied] = useToggle(false);
useEffect(() => {
const updateTimer = () => {
if (data && data.expiresAt) {
const expiryDate = new Date(data.expiresAt).getTime();
const now = new Date().getTime();
const distance = expiryDate - now;
if (distance < 0) {
setTimeLeft("Expired");
} else {
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
setTimeLeft(`${hours}h ${minutes}m ${seconds}s`);
}
}
};
const timer = setInterval(updateTimer, 1000);
return () => clearInterval(timer);
}, [data?.expiresAt]);
useEffect(() => {
let timer: NodeJS.Timeout;
if (isUrlCopied) {
timer = setTimeout(() => setIsUrlCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isUrlCopied]);
const copyUrlToClipboard = () => {
navigator.clipboard.writeText(decryptedSecret as string);
setIsUrlCopied.on();
};
return (
<div className="flex flex-col justify-between bg-bunker-800 text-gray-200 md:h-screen">
<Head>
<title>Secret Shared | Infisical</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<div className="my-4 flex justify-center md:my-8">
<Image src="/images/biglogo.png" height={180} width={240} alt="Infisical logo" />
</div>
<p className="mb-6 px-8 text-center text-xl md:px-0 md:text-3xl">
You’ve been shared a secret securely with Infisical.
</p>
<div className="flex min-h-screen w-full flex-col md:flex-row">
<DragonMainImage />
<div className="m-4 flex flex-1 flex-col items-center justify-start md:m-0">
<p className="mt-8 mb-2 text-xl font-semibold text-mineshaft-100 md:mt-20">
Secret Details
</p>
<div className="mb-16 rounded-lg border border-mineshaft-600 bg-mineshaft-900 md:p-8">
<SecretTable
isLoading={isLoading}
sharedSecret={data}
decryptedSecret={decryptedSecret}
timeLeft={timeLeft}
isUrlCopied={isUrlCopied}
copyUrlToClipboard={copyUrlToClipboard}
/>
</div>
<Link href="/">
<a className="mt-4 cursor-pointer rounded-md bg-mineshaft-500 py-2 px-4 text-lg font-semibold duration-200 hover:bg-primary hover:text-black">
Check Infisical out now!
</a>
</Link>
</div>
</div>
</div>
);
};
@@ -0,0 +1,14 @@
import Image from "next/image";
export const DragonMainImage = () => {
return (
<div className="hidden flex-1 flex-col items-center justify-center md:block md:items-start md:p-4">
<Image
src="/images/dragon-book.svg"
height={1000}
width={1413}
alt="Infisical Dragon - Came to send you a secret!"
/>
</div>
);
};
@@ -0,0 +1,89 @@
import { faCheck, faCopy, faKey } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
EmptyState,
IconButton,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { TViewSharedSecretResponse } from "@app/hooks/api/secretSharing";
type Props = {
isLoading: boolean;
sharedSecret?: TViewSharedSecretResponse;
decryptedSecret: string;
timeLeft: string;
isUrlCopied: boolean;
copyUrlToClipboard: () => void;
};
export const SecretTable = ({
isLoading,
sharedSecret,
decryptedSecret,
timeLeft,
isUrlCopied,
copyUrlToClipboard
}: Props) => {
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Value</Th>
<Th>Valid Until</Th>
</Tr>
</THead>
<TBody>
{!isLoading && sharedSecret && decryptedSecret && (
<Tr key={sharedSecret.name}>
<Td>{sharedSecret.name}</Td>
<Td>
<div className="flex items-center md:space-x-2">
<div className="max-w-[20rem] flex-1 break-words">{decryptedSecret}</div>
<IconButton
ariaLabel="copy to clipboard"
onClick={copyUrlToClipboard}
className="rounded p-2 hover:bg-gray-700"
size="xs"
>
<FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} />
</IconButton>
</div>
</Td>
<Td>{timeLeft}</Td>
</Tr>
)}
{isLoading && (
<Tr>
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
Loading...
</Td>
</Tr>
)}
{!isLoading && !sharedSecret && (
<Tr>
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
<EmptyState title="No such secret is shared yet!" icon={faKey} />
</Td>
</Tr>
)}
{!isLoading && sharedSecret && !decryptedSecret && (
<Tr>
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
<EmptyState title="Invalid URL to fetch the Secret!" icon={faKey} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
};
@@ -0,0 +1,2 @@
export { DragonMainImage } from "./MainImage";
export { SecretTable } from "./SecretTable";
@@ -0,0 +1 @@
export { ShareSecretPublicPage } from "./ShareSecretPublicPage";