From 3ddb4cd27a8c0470d9f0e715b8daffd7f5d25291 Mon Sep 17 00:00:00 2001 From: = Date: Sat, 10 Aug 2024 22:21:17 +0530 Subject: [PATCH] feat: simplified ui for password based secret sharing --- .../src/hooks/api/secretSharing/queries.ts | 49 +++++--------- frontend/src/hooks/api/secretSharing/types.ts | 16 +++-- .../components/ShareSecretForm.tsx | 13 +++- .../ViewSecretPublicPage.tsx | 51 ++++++++------ .../components/PasswordContainer.tsx | 67 ++++++------------- .../components/SecretContainer.tsx | 2 +- .../ViewSecretPublicPage/components/index.tsx | 2 +- 7 files changed, 89 insertions(+), 111 deletions(-) diff --git a/frontend/src/hooks/api/secretSharing/queries.ts b/frontend/src/hooks/api/secretSharing/queries.ts index d94ca243d..c349d39f5 100644 --- a/frontend/src/hooks/api/secretSharing/queries.ts +++ b/frontend/src/hooks/api/secretSharing/queries.ts @@ -7,7 +7,11 @@ import { TSharedSecret, TViewSharedSecretResponse } from "./types"; export const secretSharingKeys = { allSharedSecrets: () => ["sharedSecrets"] as const, specificSharedSecrets: ({ offset, limit }: { offset: number; limit: number }) => - [...secretSharingKeys.allSharedSecrets(), { offset, limit }] as const + [...secretSharingKeys.allSharedSecrets(), { offset, limit }] as const, + getSecretById: (arg: { id: string; hashedHex: string; password?: string }) => [ + "shared-secret", + arg + ] }; export const useGetSharedSecrets = ({ @@ -38,51 +42,28 @@ export const useGetSharedSecrets = ({ export const useGetActiveSharedSecretById = ({ sharedSecretId, - hashedHex + hashedHex, + password }: { sharedSecretId: string; hashedHex: string; + password?: string; }) => { - return useQuery( - [`sharedSecret-${sharedSecretId}`], + return useQuery( + secretSharingKeys.getSecretById({ id: sharedSecretId, hashedHex, password }), async () => { - const params = new URLSearchParams({ hashedHex }); - const { data } = await apiRequest.get( + const { data } = await apiRequest.post( `/api/v1/secret-sharing/public/${sharedSecretId}`, { - params + hashedHex, + password } ); - - if (!data) return null - - return { - encryptedValue: data.encryptedValue, - iv: data.iv, - tag: data.tag, - accessType: data.accessType, - orgName: data.orgName - }; + + return data; }, { enabled: Boolean(sharedSecretId) && Boolean(hashedHex) } ); }; - -// returns a secret (secret or undefined if password doesn't match) -export const fetchSecretIfPasswordIsValid = async ( - sharedSecretId: string, - hashedHex: string, - password: string, -) => { - const { data } = await apiRequest.post( - `/api/v1/secret-sharing/public/${sharedSecretId}/validate`, - { - hashedHex, - password - } - ); - - return data; -}; diff --git a/frontend/src/hooks/api/secretSharing/types.ts b/frontend/src/hooks/api/secretSharing/types.ts index 708780fe4..0dd4a9555 100644 --- a/frontend/src/hooks/api/secretSharing/types.ts +++ b/frontend/src/hooks/api/secretSharing/types.ts @@ -26,11 +26,14 @@ export type TCreateSharedSecretRequest = { }; export type TViewSharedSecretResponse = { - encryptedValue: string; - iv: string; - tag: string; - accessType: SecretSharingAccessType; - orgName?: string; + isPasswordProtected: boolean; + secret: { + encryptedValue: string; + iv: string; + tag: string; + accessType: SecretSharingAccessType; + orgName?: string; + }; }; export type TDeleteSharedSecretRequest = { @@ -40,4 +43,5 @@ export type TDeleteSharedSecretRequest = { export enum SecretSharingAccessType { Anyone = "anyone", Organization = "organization" -} \ No newline at end of file +} + diff --git a/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx b/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx index 1a850c07e..ea39e1265 100644 --- a/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx +++ b/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx @@ -33,7 +33,7 @@ const viewLimitOptions = [ const schema = z.object({ name: z.string().optional(), password: z.string().optional(), - secret: z.string(), + secret: z.string().min(1), expiresIn: z.string(), viewLimit: z.string(), accessType: z.nativeEnum(SecretSharingAccessType).optional() @@ -68,7 +68,14 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => { } }); - const onFormSubmit = async ({ name, password, secret, expiresIn, viewLimit, accessType }: FormData) => { + const onFormSubmit = async ({ + name, + password, + secret, + expiresIn, + viewLimit, + accessType + }: FormData) => { try { const expiresAt = new Date(new Date().getTime() + Number(expiresIn)); @@ -159,7 +166,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => { label="Password" isError={Boolean(error)} errorText={error?.message} - isOptional={true} + isOptional > diff --git a/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx b/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx index 78ce15d14..999f2d342 100644 --- a/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx +++ b/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx @@ -1,35 +1,42 @@ -import { useState, useCallback, useEffect } from 'react' +import { useState } from "react"; 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 { AxiosError } from "axios"; -import { TViewSharedSecretResponse, useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing"; +import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing"; -import { SecretContainer, SecretErrorContainer, PasswordContainer } from "./components"; +import { PasswordContainer,SecretContainer, SecretErrorContainer } from "./components"; export const ViewSecretPublicPage = () => { - const [secret, setSecret] = useState(null); const router = useRouter(); + const [password, setPassword] = useState(); const { id, key: urlEncodedPublicKey } = router.query; const [hashedHex, key] = urlEncodedPublicKey ? urlEncodedPublicKey.toString().split("-") : ["", ""]; - const { data: fetchSecret, error, isLoading } = useGetActiveSharedSecretById({ + const { + data: fetchSecret, + error, + isLoading, + isFetching + } = useGetActiveSharedSecretById({ sharedSecretId: id as string, - hashedHex + hashedHex, + password }); - useEffect(() => { - if (fetchSecret) setSecret(fetchSecret) - }, [fetchSecret, error]) + const isInvalidCredential = + ((error as AxiosError)?.response?.data as { message: string })?.message === + "Invalid credentials"; - const handleSecret = useCallback((value: TViewSharedSecretResponse) => { - setSecret(value) - }, [setSecret]) + const shouldShowPasswordPrompt = + isInvalidCredential || (fetchSecret?.isPasswordProtected && !fetchSecret.secret); + const isValidatingPassword = Boolean(password) && isFetching; return (
@@ -62,17 +69,21 @@ export const ViewSecretPublicPage = () => {

+ {(shouldShowPasswordPrompt || isValidatingPassword) && ( + { + setPassword(el); + }} + isInvalidCredential={!isFetching && isInvalidCredential} + /> + )} {!isLoading && ( <> - {!error && !secret && ( - + {!error && fetchSecret?.secret && key && ( + )} - {!error && secret && key && } - {error && } + {error && !isInvalidCredential && } )}
diff --git a/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx b/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx index 00876225d..edf60447b 100644 --- a/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx +++ b/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx @@ -1,60 +1,34 @@ -import { z } from "zod"; import { Controller, useForm } from "react-hook-form"; - import { faArrowRight, faSpinner } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; import { Button, FormControl, IconButton, Input } from "@app/components/v2"; -import { fetchSecretIfPasswordIsValid, TViewSharedSecretResponse } from "@app/hooks/api/secretSharing"; -import { createNotification } from "@app/components/notifications"; type Props = { - secretId: string; - hashedHex: string; - handleSecret: (val: any) => void; + onPasswordSubmit: (val: any) => void; + isSubmitting?: boolean; + isInvalidCredential?: boolean; }; const formSchema = z.object({ password: z.string() -}) +}); export type FormData = z.infer; -export const PasswordContainer = ({ secretId, hashedHex, handleSecret }: Props) => { - const { - control, - reset, - handleSubmit, - formState: { isSubmitting } - } = useForm({ - resolver: zodResolver(formSchema), +export const PasswordContainer = ({ + onPasswordSubmit, + isSubmitting, + isInvalidCredential +}: Props) => { + const { control, handleSubmit } = useForm({ + resolver: zodResolver(formSchema) }); const onFormSubmit = async ({ password }: FormData) => { - try { - const secret: TViewSharedSecretResponse = await fetchSecretIfPasswordIsValid( - secretId, - hashedHex, - password, - ) - - if (secret) { - handleSecret(secret); - } else { - reset({ password: "" }); - createNotification({ - text: "Password is Invalid. Try again", - type: "error" - }) - } - } catch (error) { - console.error("Failed to validate password:", error); - createNotification({ - text: "Failed to validate password", - type: "error" - }) - } + onPasswordSubmit(password); }; return ( @@ -63,15 +37,16 @@ export const PasswordContainer = ({ secretId, hashedHex, handleSecret }: Props) ( -
- +
+
-
diff --git a/frontend/src/views/ViewSecretPublicPage/components/SecretContainer.tsx b/frontend/src/views/ViewSecretPublicPage/components/SecretContainer.tsx index 58c6c57c2..a920c9d93 100644 --- a/frontend/src/views/ViewSecretPublicPage/components/SecretContainer.tsx +++ b/frontend/src/views/ViewSecretPublicPage/components/SecretContainer.tsx @@ -14,7 +14,7 @@ import { useTimedReset, useToggle } from "@app/hooks"; import { TViewSharedSecretResponse } from "@app/hooks/api/secretSharing"; type Props = { - secret: TViewSharedSecretResponse; + secret: TViewSharedSecretResponse["secret"]; secretKey: string; }; diff --git a/frontend/src/views/ViewSecretPublicPage/components/index.tsx b/frontend/src/views/ViewSecretPublicPage/components/index.tsx index fd66da812..8ba8b27c0 100644 --- a/frontend/src/views/ViewSecretPublicPage/components/index.tsx +++ b/frontend/src/views/ViewSecretPublicPage/components/index.tsx @@ -1,3 +1,3 @@ +export { PasswordContainer } from "./PasswordContainer"; export { SecretContainer } from "./SecretContainer"; export { SecretErrorContainer } from "./SecretErrorContainer"; -export { PasswordContainer } from "./PasswordContainer";