diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index 5fedfd5a0..de1bfe968 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -80,7 +80,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => orgId: req.permission?.orgId }); - // only return secret if it exists and has no password set + // return undefined if it does not exist, has password set or has no more views allowed. if (!sharedSecret || sharedSecret.password) return undefined; return { diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 14a11a65e..b37f8a771 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -179,10 +179,12 @@ export const secretSharingServiceFactory = ({ if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) throw new UnauthorizedError(); - if (sharedSecret.password) return undefined; + await checkIfExpired(sharedSecret, sharedSecretId); + + if (sharedSecret.password !== null) return undefined; - // we only update the view count when secret has no password, when password is set view count is updated when we validate the password. - manageSecretViewCount(sharedSecret, sharedSecretId); + // decrement when we are sure the user will view secret. + await decrementSecretViewCount(sharedSecret, sharedSecretId); return { ...sharedSecret, @@ -215,13 +217,11 @@ export const secretSharingServiceFactory = ({ if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) throw new UnauthorizedError(); - if (!sharedSecret.password) return undefined; - const isMatch = await bcrypt.compare(password, sharedSecret.password); - if (!isMatch) return undefined; + if (!isMatch) return undefined - // reduce view count when we are sure the password matches. - manageSecretViewCount(sharedSecret, sharedSecretId) + // we reduce the view count when we are sure the password matches. + await decrementSecretViewCount(sharedSecret, sharedSecretId); return { ...sharedSecret, @@ -240,7 +240,8 @@ export const secretSharingServiceFactory = ({ return deletedSharedSecret; }; - const manageSecretViewCount = async (sharedSecret: any, sharedSecretId: string) => { + /** Checks if secret is expired and throws error if true */ + const checkIfExpired = async (sharedSecret: any, sharedSecretId: string) => { const { expiresAt, expiresAfterViews } = sharedSecret; if (expiresAt !== null && expiresAt < new Date()) { @@ -258,6 +259,10 @@ export const secretSharingServiceFactory = ({ message: "Access denied: Secret has expired by view count" }); } + } + + const decrementSecretViewCount = async (sharedSecret: any, sharedSecretId: string) => { + const { expiresAfterViews } = sharedSecret; if (expiresAfterViews) { // decrement view count if view count expiry set diff --git a/frontend/src/hooks/api/secretSharing/queries.ts b/frontend/src/hooks/api/secretSharing/queries.ts index b697b92b4..acf091e5b 100644 --- a/frontend/src/hooks/api/secretSharing/queries.ts +++ b/frontend/src/hooks/api/secretSharing/queries.ts @@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { TSharedSecret, TViewSharedSecretResponse, ValidateSecretPassword } from "./types"; +import { TSharedSecret, TViewSharedSecretResponse } from "./types"; export const secretSharingKeys = { allSharedSecrets: () => ["sharedSecrets"] as const, @@ -77,7 +77,7 @@ export const fetchSecretIfPasswordIsValid = async ( hashedHex: string, password: string, ) => { - const { data } = await apiRequest.post( + const { data } = await apiRequest.post( `/api/v1/secret-sharing/public/${sharedSecretId}/validate`, { hashedHex, diff --git a/frontend/src/hooks/api/secretSharing/types.ts b/frontend/src/hooks/api/secretSharing/types.ts index 253321cbf..708780fe4 100644 --- a/frontend/src/hooks/api/secretSharing/types.ts +++ b/frontend/src/hooks/api/secretSharing/types.ts @@ -31,7 +31,6 @@ export type TViewSharedSecretResponse = { tag: string; accessType: SecretSharingAccessType; orgName?: string; - password?: string; }; export type TDeleteSharedSecretRequest = { @@ -41,8 +40,4 @@ export type TDeleteSharedSecretRequest = { export enum SecretSharingAccessType { Anyone = "anyone", Organization = "organization" -} - -export type ValidateSecretPassword = { - isValid: boolean -} +} \ No newline at end of file diff --git a/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx b/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx index fecf72659..27f1d37bd 100644 --- a/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx +++ b/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx @@ -5,12 +5,13 @@ import { useRouter } from "next/router"; import { faArrowRight } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing"; +import { TViewSharedSecretResponse, useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing"; import { SecretContainer, SecretErrorContainer, PasswordContainer } from "./components"; export const ViewSecretPublicPage = () => { const [secret, setSecret] = useState(null) + const [error, setError] = useState(null) const router = useRouter(); const { id, key: urlEncodedPublicKey } = router.query; @@ -18,17 +19,18 @@ export const ViewSecretPublicPage = () => { ? urlEncodedPublicKey.toString().split("-") : ["", ""]; - const { data, error } = useGetActiveSharedSecretById({ + const { data: fetchSecret, error: fetchError, isLoading } = useGetActiveSharedSecretById({ sharedSecretId: id as string, hashedHex }); useEffect(() => { - if (data) setSecret(data) - }, [data]) + if (fetchSecret) setSecret(fetchSecret) + if (fetchError) setError(fetchError) + }, [fetchSecret, fetchError]) - const handleSecret = useCallback((val: any) => { - setSecret(val) + const handleSecret = useCallback((value: TViewSharedSecretResponse) => { + setSecret(value) }, [setSecret]) return ( @@ -62,13 +64,19 @@ export const ViewSecretPublicPage = () => {

- {!secret ? ( - - ) : ( - key && - ) - } - {error && } + {!isLoading && ( + <> + {!error && !secret && ( + + )} + {!error && secret && key && } + {error && } + + )}
diff --git a/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx b/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx index ceef5b291..e79a3b5b1 100644 --- a/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx +++ b/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx @@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { Button, FormControl, IconButton, Input } from "@app/components/v2"; -import { fetchSecretIfPasswordIsValid } from "@app/hooks/api/secretSharing"; +import { fetchSecretIfPasswordIsValid, TViewSharedSecretResponse } from "@app/hooks/api/secretSharing"; import { createNotification } from "@app/components/notifications"; type Props = { @@ -33,7 +33,7 @@ export const PasswordContainer = ({ secretId, hashedHex, handleSecret }: Props) const onFormSubmit = async ({ password }: FormData) => { try { - const secret = await fetchSecretIfPasswordIsValid( + const secret: TViewSharedSecretResponse = await fetchSecretIfPasswordIsValid( secretId, hashedHex, password, @@ -71,7 +71,7 @@ export const PasswordContainer = ({ secretId, hashedHex, handleSecret }: Props) label="Password" >
- +