feat: public page has direct secret creation (no modal))

This commit is contained in:
ShubhamPalriwala
2024-06-10 08:01:27 +05:30
parent 0e86d5573a
commit a6e71c98a6
5 changed files with 327 additions and 241 deletions
@@ -0,0 +1,233 @@
import crypto from "crypto";
import { Controller } from "react-hook-form";
import { AxiosError } from "axios";
import * as yup from "yup";
import { createNotification } from "@app/components/notifications";
import { encryptSymmetric } from "@app/components/utilities/cryptography/crypto";
import {
Button,
FormControl,
Input,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api/secretSharing";
const schema = yup.object({
value: yup.string().max(10000).required().label("Shared Secret Value"),
expiresAfterViews: yup.number().min(1).required().label("Expires After Views"),
expiresInValue: yup.number().min(1).required().label("Expiration Value"),
expiresInUnit: yup.string().required().label("Expiration Unit")
});
export type FormData = yup.InferType<typeof schema>;
export const AddShareSecretForm = ({
isPublic,
inModal,
handleSubmit,
control,
isSubmitting,
setNewSharedSecret
}: {
isPublic: boolean;
inModal: boolean;
handleSubmit: any;
control: any;
isSubmitting: boolean;
setNewSharedSecret: (value: string) => void;
}) => {
const publicSharedSecretCreator = useCreatePublicSharedSecret();
const privateSharedSecretCreator = useCreateSharedSecret();
const createSharedSecret = isPublic ? publicSharedSecretCreator : privateSharedSecretCreator;
const expirationUnitsAndActions = [
{
unit: "Minutes",
action: (expiresAt: Date, expiresInValue: number) =>
expiresAt.setMinutes(expiresAt.getMinutes() + expiresInValue)
},
{
unit: "Hours",
action: (expiresAt: Date, expiresInValue: number) =>
expiresAt.setHours(expiresAt.getHours() + expiresInValue)
},
{
unit: "Days",
action: (expiresAt: Date, expiresInValue: number) =>
expiresAt.setDate(expiresAt.getDate() + expiresInValue)
},
{
unit: "Weeks",
action: (expiresAt: Date, expiresInValue: number) =>
expiresAt.setDate(expiresAt.getDate() + expiresInValue * 7)
}
];
const onFormSubmit = async ({
value,
expiresInValue,
expiresInUnit,
expiresAfterViews
}: FormData) => {
try {
const key = crypto.randomBytes(16).toString("hex");
const hashedHex = crypto.createHash("sha256").update(key).digest("hex");
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext: value,
key
});
const expiresAt = new Date();
const updateExpiresAt = expirationUnitsAndActions.find(
(item) => item.unit === expiresInUnit
)?.action;
if (updateExpiresAt && expiresInValue) {
updateExpiresAt(expiresAt, expiresInValue);
}
const { id } = await createSharedSecret.mutateAsync({
encryptedValue: ciphertext,
iv,
tag,
hashedHex,
expiresAt,
expiresAfterViews
});
setNewSharedSecret(
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
hashedHex
)}-${encodeURIComponent(key)}`
);
createNotification({
text: "Successfully created a shared secret",
type: "success"
});
} catch (err) {
console.error(err);
const axiosError = err as AxiosError;
if (axiosError?.response?.status === 401) {
createNotification({
text: "You do not have access to create shared secrets",
type: "error"
});
} else {
createNotification({
text: "Failed to create a shared secret",
type: "error"
});
}
}
};
return (
<form className="flex w-full flex-col items-center" onSubmit={handleSubmit(onFormSubmit)}>
<div
className={`w-[${inModal ? "33rem" : "36rem"}] ${
!inModal && "border border-mineshaft-600 bg-mineshaft-800 p-4"
}`}
>
<div className="mb-4">
<Controller
control={control}
name="value"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Shared Secret"
isError={Boolean(error)}
errorText={error?.message}
>
<SecretInput
isVisible
{...field}
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2 text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 min-h-[70px]"
/>
</FormControl>
)}
/>
</div>
<div className="flex w-full flex-row justify-center">
<div className="w-2/7 flex">
<Controller
control={control}
name="expiresAfterViews"
defaultValue={6}
render={({ field, fieldState: { error } }) => (
<FormControl
className="mb-4 w-full"
label="Expires After Views"
isError={Boolean(error)}
errorText="Please enter a valid number of views"
>
<Input {...field} type="number" min={1} />
</FormControl>
)}
/>
</div>
<div className="w-1/7 flex items-center justify-center px-2">
<p className="px-4 text-sm text-gray-400">OR</p>
</div>
<div className="w-4/7 flex">
<div className="flex w-full">
<div className="flex w-2/5 w-full justify-center">
<Controller
control={control}
name="expiresInValue"
defaultValue={6}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Expires after Time"
isError={Boolean(error)}
errorText="Please enter a valid time duration"
>
<Input {...field} type="number" min={0} />
</FormControl>
)}
/>
</div>
<div className="flex w-3/5 w-full justify-center">
<Controller
control={control}
name="expiresInUnit"
defaultValue={expirationUnitsAndActions[0].unit}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Unit" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{expirationUnitsAndActions.map(({ unit }) => (
<SelectItem value={unit} key={unit}>
{unit}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
</div>
</div>
</div>
<div className={`flex items-center ${!inModal && "justify-center"}`}>
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
{inModal ? "Create" : "Share your own Secret"}
</Button>
{inModal && (
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
)}
</div>
</div>
</form>
);
};
@@ -1,53 +1,14 @@
import crypto from "crypto";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { AxiosError } from "axios";
import * as yup from "yup"; import * as yup from "yup";
import { createNotification } from "@app/components/notifications"; import { Modal, ModalContent } from "@app/components/v2";
import { encryptSymmetric } from "@app/components/utilities/cryptography/crypto";
import {
Button,
FormControl,
IconButton,
Input,
Modal,
ModalClose,
ModalContent,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { useTimedReset } from "@app/hooks"; import { useTimedReset } from "@app/hooks";
import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api/secretSharing";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const expirationUnitsAndActions = [ import { AddShareSecretForm } from "./AddShareSecretForm";
{ import { ViewAndCopySharedSecret } from "./ViewAndCopySharedSecret";
unit: "Minutes",
action: (expiresAt: Date, expiresInValue: number) =>
expiresAt.setMinutes(expiresAt.getMinutes() + expiresInValue)
},
{
unit: "Hours",
action: (expiresAt: Date, expiresInValue: number) =>
expiresAt.setHours(expiresAt.getHours() + expiresInValue)
},
{
unit: "Days",
action: (expiresAt: Date, expiresInValue: number) =>
expiresAt.setDate(expiresAt.getDate() + expiresInValue)
},
{
unit: "Weeks",
action: (expiresAt: Date, expiresInValue: number) =>
expiresAt.setDate(expiresAt.getDate() + expiresInValue * 7)
}
];
const schema = yup.object({ const schema = yup.object({
value: yup.string().max(10000).required().label("Shared Secret Value"), value: yup.string().max(10000).required().label("Shared Secret Value"),
@@ -65,9 +26,10 @@ type Props = {
state?: boolean state?: boolean
) => void; ) => void;
isPublic: boolean; isPublic: boolean;
inModal: boolean;
}; };
export const AddShareSecretModal = ({ popUp, handlePopUpToggle, isPublic }: Props) => { export const AddShareSecretModal = ({ popUp, handlePopUpToggle, isPublic, inModal }: Props) => {
const { const {
control, control,
reset, reset,
@@ -76,11 +38,8 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle, isPublic }: Prop
} = useForm<FormData>({ } = useForm<FormData>({
resolver: yupResolver(schema) resolver: yupResolver(schema)
}); });
const publicSharedSecretCreator = useCreatePublicSharedSecret();
const privateSharedSecretCreator = useCreateSharedSecret();
const createSharedSecret = isPublic ? publicSharedSecretCreator : privateSharedSecretCreator;
const [newSharedSecret, setnewSharedSecret] = useState(""); const [newSharedSecret, setNewSharedSecret] = useState("");
const hasSharedSecret = Boolean(newSharedSecret); const hasSharedSecret = Boolean(newSharedSecret);
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({ const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
initialState: false initialState: false
@@ -96,70 +55,14 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle, isPublic }: Prop
} }
}, [isUrlCopied]); }, [isUrlCopied]);
const onFormSubmit = async ({ // eslint-disable-next-line no-nested-ternary
value, return inModal ? (
expiresInValue,
expiresInUnit,
expiresAfterViews
}: FormData) => {
try {
const key = crypto.randomBytes(16).toString("hex");
const hashedHex = crypto.createHash("sha256").update(key).digest("hex");
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext: value,
key
});
const expiresAt = new Date();
const updateExpiresAt = expirationUnitsAndActions.find(
(item) => item.unit === expiresInUnit
)?.action;
if (updateExpiresAt && expiresInValue) {
updateExpiresAt(expiresAt, expiresInValue);
}
const { id } = await createSharedSecret.mutateAsync({
encryptedValue: ciphertext,
iv,
tag,
hashedHex,
expiresAt,
expiresAfterViews
});
setnewSharedSecret(
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
hashedHex
)}-${encodeURIComponent(key)}`
);
createNotification({
text: "Successfully created a shared secret",
type: "success"
});
} catch (err) {
console.error(err);
const axiosError = err as AxiosError;
if (axiosError?.response?.status === 401) {
createNotification({
text: "You do not have access to create shared secrets",
type: "error"
});
} else {
createNotification({
text: "Failed to create a shared secret",
type: "error"
});
}
}
};
return (
<Modal <Modal
isOpen={popUp?.createSharedSecret?.isOpen} isOpen={popUp?.createSharedSecret?.isOpen}
onOpenChange={(open) => { onOpenChange={(open) => {
handlePopUpToggle("createSharedSecret", open); handlePopUpToggle("createSharedSecret", open);
reset(); reset();
setnewSharedSecret(""); setNewSharedSecret("");
}} }}
> >
<ModalContent <ModalContent
@@ -167,127 +70,39 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle, isPublic }: Prop
subTitle="This link is only accessible once. Please share this link with intended recipients. " subTitle="This link is only accessible once. Please share this link with intended recipients. "
> >
{!hasSharedSecret ? ( {!hasSharedSecret ? (
<form onSubmit={handleSubmit(onFormSubmit)}> <AddShareSecretForm
<Controller isPublic={isPublic}
inModal={inModal}
control={control} control={control}
name="value" handleSubmit={handleSubmit}
defaultValue="" isSubmitting={isSubmitting}
render={({ field, fieldState: { error } }) => ( setNewSharedSecret={setNewSharedSecret}
<FormControl
label="Shared Secret"
isError={Boolean(error)}
errorText={error?.message}
>
<SecretInput
isVisible={false}
{...field}
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2 text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 min-h-[70px]"
/> />
</FormControl>
)}
/>
<div className="flex w-full flex-row">
<div className="w-2/7 flex">
<Controller
control={control}
name="expiresAfterViews"
defaultValue={6}
render={({ field, fieldState: { error } }) => (
<FormControl
className="mb-4 w-full"
label="Expires After Views"
isError={Boolean(error)}
errorText="Please enter a valid number of views"
>
<Input {...field} type="number" min={1} />
</FormControl>
)}
/>
</div>
<div className="w-1/7 flex items-center justify-center px-2">
<p className="px-4 text-sm text-gray-400">OR</p>
</div>
<div className="w-4/7 flex">
<div className="flex w-full">
<div className="flex w-2/5 w-full justify-center">
<Controller
control={control}
name="expiresInValue"
defaultValue={6}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Expires after Time"
isError={Boolean(error)}
errorText="Please enter a valid time duration"
>
<Input {...field} type="number" min={0} />
</FormControl>
)}
/>
</div>
<div className="flex w-3/5 w-full justify-center">
<Controller
control={control}
name="expiresInUnit"
defaultValue={expirationUnitsAndActions[0].unit}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Unit"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{expirationUnitsAndActions.map(({ unit }) => (
<SelectItem value={unit} key={unit}>
{unit}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
</div>
</div>
</div>
<div className="flex items-center">
<Button
className="mr-4"
type="submit"
isDisabled={isSubmitting}
isLoading={isSubmitting}
>
Create
</Button>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
</div>
</form>
) : ( ) : (
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400"> <ViewAndCopySharedSecret
<p className="mr-4 break-all">{newSharedSecret}</p> inModal={inModal}
<IconButton newSharedSecret={newSharedSecret}
ariaLabel="copy icon" isUrlCopied={isUrlCopied}
colorSchema="secondary" copyUrlToClipboard={copyUrlToClipboard}
className="group relative" />
onClick={copyUrlToClipboard}
>
<FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Click to Copy
</span>
</IconButton>
</div>
)} )}
</ModalContent> </ModalContent>
</Modal> </Modal>
) : !hasSharedSecret ? (
<AddShareSecretForm
isPublic={isPublic}
inModal={inModal}
control={control}
handleSubmit={handleSubmit}
isSubmitting={isSubmitting}
setNewSharedSecret={setNewSharedSecret}
/>
) : (
<ViewAndCopySharedSecret
inModal={inModal}
newSharedSecret={newSharedSecret}
isUrlCopied={isUrlCopied}
copyUrlToClipboard={copyUrlToClipboard}
/>
); );
}; };
@@ -60,7 +60,12 @@ export const ShareSecretSection = () => {
</Button> </Button>
</div> </div>
<ShareSecretsTable handlePopUpOpen={handlePopUpOpen} /> <ShareSecretsTable handlePopUpOpen={handlePopUpOpen} />
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} isPublic={false} /> <AddShareSecretModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
isPublic={false}
inModal
/>
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteSharedSecretConfirmation.isOpen} isOpen={popUp.deleteSharedSecretConfirmation.isOpen}
title={`Delete ${ title={`Delete ${
@@ -0,0 +1,39 @@
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconButton } from "@app/components/v2";
export const ViewAndCopySharedSecret = ({
inModal,
newSharedSecret,
isUrlCopied,
copyUrlToClipboard
}: {
inModal: boolean;
newSharedSecret: string;
isUrlCopied: boolean;
copyUrlToClipboard: () => void;
}) => {
return (
<div
className={`w-[${inModal ? "33rem" : "36rem"}] ${
!inModal && "border border-mineshaft-600 bg-mineshaft-800 p-4"
}`}
>
<div className="mt-2 mb-3 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">{newSharedSecret}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={copyUrlToClipboard}
>
<FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Click to Copy
</span>
</IconButton>
</div>
</div>
);
};
@@ -3,11 +3,8 @@ import Head from "next/head";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { decryptSymmetric } from "@app/components/utilities/cryptography/crypto"; import { decryptSymmetric } from "@app/components/utilities/cryptography/crypto";
import { Button } from "@app/components/v2";
import { usePopUp, useTimedReset } from "@app/hooks"; import { usePopUp, useTimedReset } from "@app/hooks";
import { useGetActiveSharedSecretByIdAndHashedHex } from "@app/hooks/api/secretSharing"; import { useGetActiveSharedSecretByIdAndHashedHex } from "@app/hooks/api/secretSharing";
@@ -54,7 +51,7 @@ export const ShareSecretPublicPage = () => {
navigator.clipboard.writeText(decryptedSecret); navigator.clipboard.writeText(decryptedSecret);
setIsUrlCopied(true); setIsUrlCopied(true);
}; };
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["createSharedSecret"] as const); const { popUp, handlePopUpToggle } = usePopUp(["createSharedSecret"] as const);
return ( return (
<div className="flex h-screen flex-col bg-bunker-800 text-gray-200"> <div className="flex h-screen flex-col bg-bunker-800 text-gray-200">
@@ -119,19 +116,16 @@ export const ShareSecretPublicPage = () => {
</p> </p>
</div> </div>
</div> </div>
<Button </div>
className="mt-4 max-w-[600px] md:mt-0" <div className="pt-20">
colorSchema="primary" <AddShareSecretModal
leftIcon={<FontAwesomeIcon icon={faPlus} />} popUp={popUp}
onClick={() => { handlePopUpToggle={handlePopUpToggle}
handlePopUpOpen("createSharedSecret"); isPublic
}} inModal={false}
> />
Share your own Secret
</Button>
</div> </div>
</div> </div>
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} isPublic />
<div className="flex items-center justify-center p-4"> <div className="flex items-center justify-center p-4">
<p className="text-center text-sm text-gray-200"> <p className="text-center text-sm text-gray-200">
Developed by{" "} Developed by{" "}