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

This commit is contained in:
ShubhamPalriwala
2024-06-06 21:43:44 +05:30
parent 0e86d5573a
commit a6e71c98a6
5 changed files with 327 additions and 241 deletions

View File

@@ -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>
);
};

View File

@@ -1,53 +1,14 @@
import crypto from "crypto";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
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,
IconButton,
Input,
Modal,
ModalClose,
ModalContent,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { Modal, ModalContent } from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api/secretSharing";
import { UsePopUpState } from "@app/hooks/usePopUp";
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)
}
];
import { AddShareSecretForm } from "./AddShareSecretForm";
import { ViewAndCopySharedSecret } from "./ViewAndCopySharedSecret";
const schema = yup.object({
value: yup.string().max(10000).required().label("Shared Secret Value"),
@@ -65,9 +26,10 @@ type Props = {
state?: boolean
) => void;
isPublic: boolean;
inModal: boolean;
};
export const AddShareSecretModal = ({ popUp, handlePopUpToggle, isPublic }: Props) => {
export const AddShareSecretModal = ({ popUp, handlePopUpToggle, isPublic, inModal }: Props) => {
const {
control,
reset,
@@ -76,11 +38,8 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle, isPublic }: Prop
} = useForm<FormData>({
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 [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
initialState: false
@@ -96,70 +55,14 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle, isPublic }: Prop
}
}, [isUrlCopied]);
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 (
// eslint-disable-next-line no-nested-ternary
return inModal ? (
<Modal
isOpen={popUp?.createSharedSecret?.isOpen}
onOpenChange={(open) => {
handlePopUpToggle("createSharedSecret", open);
reset();
setnewSharedSecret("");
setNewSharedSecret("");
}}
>
<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. "
>
{!hasSharedSecret ? (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="value"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<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>
<AddShareSecretForm
isPublic={isPublic}
inModal={inModal}
control={control}
handleSubmit={handleSubmit}
isSubmitting={isSubmitting}
setNewSharedSecret={setNewSharedSecret}
/>
) : (
<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>
<ViewAndCopySharedSecret
inModal={inModal}
newSharedSecret={newSharedSecret}
isUrlCopied={isUrlCopied}
copyUrlToClipboard={copyUrlToClipboard}
/>
)}
</ModalContent>
</Modal>
) : !hasSharedSecret ? (
<AddShareSecretForm
isPublic={isPublic}
inModal={inModal}
control={control}
handleSubmit={handleSubmit}
isSubmitting={isSubmitting}
setNewSharedSecret={setNewSharedSecret}
/>
) : (
<ViewAndCopySharedSecret
inModal={inModal}
newSharedSecret={newSharedSecret}
isUrlCopied={isUrlCopied}
copyUrlToClipboard={copyUrlToClipboard}
/>
);
};

View File

@@ -60,7 +60,12 @@ export const ShareSecretSection = () => {
</Button>
</div>
<ShareSecretsTable handlePopUpOpen={handlePopUpOpen} />
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} isPublic={false} />
<AddShareSecretModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
isPublic={false}
inModal
/>
<DeleteActionModal
isOpen={popUp.deleteSharedSecretConfirmation.isOpen}
title={`Delete ${

View File

@@ -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>
);
};

View File

@@ -3,11 +3,8 @@ import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
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 { Button } from "@app/components/v2";
import { usePopUp, useTimedReset } from "@app/hooks";
import { useGetActiveSharedSecretByIdAndHashedHex } from "@app/hooks/api/secretSharing";
@@ -54,7 +51,7 @@ export const ShareSecretPublicPage = () => {
navigator.clipboard.writeText(decryptedSecret);
setIsUrlCopied(true);
};
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["createSharedSecret"] as const);
const { popUp, handlePopUpToggle } = usePopUp(["createSharedSecret"] as const);
return (
<div className="flex h-screen flex-col bg-bunker-800 text-gray-200">
@@ -119,19 +116,16 @@ export const ShareSecretPublicPage = () => {
</p>
</div>
</div>
<Button
className="mt-4 max-w-[600px] md:mt-0"
colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
handlePopUpOpen("createSharedSecret");
}}
>
Share your own Secret
</Button>
</div>
<div className="pt-20">
<AddShareSecretModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
isPublic
inModal={false}
/>
</div>
</div>
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} isPublic />
<div className="flex items-center justify-center p-4">
<p className="text-center text-sm text-gray-200">
Developed by{" "}