From 0321ec32fb34bdd09a291b28cb49da183357dff9 Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Tue, 6 Aug 2024 12:26:23 +0300 Subject: [PATCH 01/25] feat: add password input --- .../components/ShareSecretForm.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx b/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx index da9aa3c98..87dc34ad6 100644 --- a/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx +++ b/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx @@ -32,6 +32,7 @@ const viewLimitOptions = [ const schema = z.object({ name: z.string().optional(), + password: z.string().optional(), secret: z.string(), expiresIn: z.string(), viewLimit: z.string(), @@ -67,7 +68,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => { } }); - const onFormSubmit = async ({ name, 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)); @@ -80,6 +81,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => { const { id } = await createSharedSecret.mutateAsync({ name, + password, encryptedValue: ciphertext, hashedHex, iv, @@ -149,6 +151,19 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => { )} /> + ( + + + + )} + /> Date: Tue, 6 Aug 2024 12:27:16 +0300 Subject: [PATCH 02/25] feat: update type resolvers to include password --- backend/src/db/schemas/secret-sharing.ts | 1 + backend/src/server/routes/v1/secret-sharing-router.ts | 2 ++ backend/src/services/secret-sharing/secret-sharing-types.ts | 2 ++ 3 files changed, 5 insertions(+) diff --git a/backend/src/db/schemas/secret-sharing.ts b/backend/src/db/schemas/secret-sharing.ts index de75fbafc..823624c74 100644 --- a/backend/src/db/schemas/secret-sharing.ts +++ b/backend/src/db/schemas/secret-sharing.ts @@ -21,6 +21,7 @@ export const SecretSharingSchema = z.object({ expiresAfterViews: z.number().nullable().optional(), accessType: z.string().default("anyone"), name: z.string().nullable().optional(), + password: z.string().optional(), lastViewedAt: z.date().nullable().optional() }); diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index a23c1f596..d9837b4c5 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -101,6 +101,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => schema: { body: z.object({ encryptedValue: z.string(), + password: z.string().optional(), hashedHex: z.string(), iv: z.string(), tag: z.string(), @@ -131,6 +132,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => schema: { body: z.object({ name: z.string().max(50).optional(), + password: z.string().optional(), encryptedValue: z.string(), hashedHex: z.string(), iv: z.string(), diff --git a/backend/src/services/secret-sharing/secret-sharing-types.ts b/backend/src/services/secret-sharing/secret-sharing-types.ts index 76d723c62..16a89077a 100644 --- a/backend/src/services/secret-sharing/secret-sharing-types.ts +++ b/backend/src/services/secret-sharing/secret-sharing-types.ts @@ -15,6 +15,7 @@ export type TSharedSecretPermission = { orgId: string; accessType?: SecretSharingAccessType; name?: string; + password?: string; }; export type TCreatePublicSharedSecretDTO = { @@ -24,6 +25,7 @@ export type TCreatePublicSharedSecretDTO = { tag: string; expiresAt: string; expiresAfterViews?: number; + password?: string; accessType: SecretSharingAccessType; }; From f606e31b981f7e02dae1f8294d84ff3688481a29 Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Tue, 6 Aug 2024 12:28:03 +0300 Subject: [PATCH 03/25] feat: apply table migrations (add password field) --- .../20240806083221_secret-sharing-password.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 backend/src/db/migrations/20240806083221_secret-sharing-password.ts diff --git a/backend/src/db/migrations/20240806083221_secret-sharing-password.ts b/backend/src/db/migrations/20240806083221_secret-sharing-password.ts new file mode 100644 index 000000000..7e0f5f30f --- /dev/null +++ b/backend/src/db/migrations/20240806083221_secret-sharing-password.ts @@ -0,0 +1,25 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.SecretSharing)) { + const doesPasswordExist = await knex.schema.hasColumn(TableName.SecretSharing, "password"); + if (!doesPasswordExist) { + await knex.schema.alterTable(TableName.SecretSharing, (t) => { + t.string("password").nullable(); + }); + } + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.SecretSharing)) { + const doesPasswordExist = await knex.schema.hasColumn(TableName.SecretSharing, "password"); + if (doesPasswordExist) { + await knex.schema.alterTable(TableName.SecretSharing, (t) => { + t.dropColumn("password"); + }); + } + } +} From ce4ba24ef2bac21c655c67692e03fc00b4c95511 Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Tue, 6 Aug 2024 12:58:27 +0300 Subject: [PATCH 04/25] feat: create secret with password --- backend/src/db/schemas/secret-sharing.ts | 2 +- .../src/services/secret-sharing/secret-sharing-service.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/src/db/schemas/secret-sharing.ts b/backend/src/db/schemas/secret-sharing.ts index 823624c74..be5643e2b 100644 --- a/backend/src/db/schemas/secret-sharing.ts +++ b/backend/src/db/schemas/secret-sharing.ts @@ -21,7 +21,7 @@ export const SecretSharingSchema = z.object({ expiresAfterViews: z.number().nullable().optional(), accessType: z.string().default("anyone"), name: z.string().nullable().optional(), - password: z.string().optional(), + password: z.string().nullable().optional(), lastViewedAt: z.date().nullable().optional() }); diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 1f38bf1f1..71ce43de4 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -36,6 +36,7 @@ export const secretSharingServiceFactory = ({ iv, tag, name, + password, accessType, expiresAt, expiresAfterViews @@ -62,6 +63,7 @@ export const secretSharingServiceFactory = ({ const newSharedSecret = await secretSharingDAL.create({ name, + password, encryptedValue, hashedHex, iv, @@ -77,6 +79,7 @@ export const secretSharingServiceFactory = ({ }; const createPublicSharedSecret = async ({ + password, encryptedValue, hashedHex, iv, @@ -103,6 +106,7 @@ export const secretSharingServiceFactory = ({ } const newSharedSecret = await secretSharingDAL.create({ + password, encryptedValue, hashedHex, iv, @@ -111,6 +115,7 @@ export const secretSharingServiceFactory = ({ expiresAfterViews, accessType }); + return { id: newSharedSecret.id }; }; From 63333159cabcd092b29351ca3f0f54e3d41a3e20 Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Tue, 6 Aug 2024 12:58:53 +0300 Subject: [PATCH 05/25] feat: fetch password when fetching secrets --- backend/src/server/routes/v1/secret-sharing-router.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index d9837b4c5..95fe4e54b 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -63,6 +63,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => response: { 200: SecretSharingSchema.pick({ encryptedValue: true, + password: true, iv: true, tag: true, expiresAt: true, @@ -84,6 +85,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => encryptedValue: sharedSecret.encryptedValue, iv: sharedSecret.iv, tag: sharedSecret.tag, + password: sharedSecret.password, expiresAt: sharedSecret.expiresAt, expiresAfterViews: sharedSecret.expiresAfterViews, accessType: sharedSecret.accessType, From 6a402950c348e80836e1ab7e8dc918efe1c2ef0e Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Tue, 6 Aug 2024 12:59:46 +0300 Subject: [PATCH 06/25] chore: add check migration status cmd scripts --- backend/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/package.json b/backend/package.json index 1328db4dd..0a76b3701 100644 --- a/backend/package.json +++ b/backend/package.json @@ -50,6 +50,7 @@ "migration:down": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down", "migration:list": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list", "migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest", + "migration:status": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:status", "migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback", "seed:new": "tsx ./scripts/create-seed-file.ts", "seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run", From 4b07234997ab74a465b4e0881a13728b98209e7c Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Tue, 6 Aug 2024 14:08:40 +0300 Subject: [PATCH 07/25] feat: update frontend queries to retrieve password --- frontend/src/hooks/api/secretSharing/queries.ts | 1 + frontend/src/hooks/api/secretSharing/types.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/frontend/src/hooks/api/secretSharing/queries.ts b/frontend/src/hooks/api/secretSharing/queries.ts index 89a804f6e..403521759 100644 --- a/frontend/src/hooks/api/secretSharing/queries.ts +++ b/frontend/src/hooks/api/secretSharing/queries.ts @@ -58,6 +58,7 @@ export const useGetActiveSharedSecretById = ({ ); return { encryptedValue: data.encryptedValue, + password: data.password, iv: data.iv, tag: data.tag, accessType: data.accessType, diff --git a/frontend/src/hooks/api/secretSharing/types.ts b/frontend/src/hooks/api/secretSharing/types.ts index d35fea8d6..c0dd2f8d6 100644 --- a/frontend/src/hooks/api/secretSharing/types.ts +++ b/frontend/src/hooks/api/secretSharing/types.ts @@ -30,6 +30,7 @@ export type TViewSharedSecretResponse = { tag: string; accessType: SecretSharingAccessType; orgName?: string; + password?: string; }; export type TDeleteSharedSecretRequest = { From 1917e0fdb76c42b1c296ae3e8b1ad236c76da604 Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Tue, 6 Aug 2024 14:13:03 +0300 Subject: [PATCH 08/25] feat: validate via password before showing secret --- .../ViewSecretPublicPage.tsx | 16 ++++- .../components/PasswordContainer.tsx | 60 +++++++++++++++++++ .../ViewSecretPublicPage/components/index.tsx | 1 + 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx diff --git a/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx b/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx index 99664b780..ee5c4a65c 100644 --- a/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx +++ b/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx @@ -1,3 +1,4 @@ +import { useState, useCallback } from 'react' import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -6,9 +7,10 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing"; -import { SecretContainer, SecretErrorContainer } from "./components"; +import { SecretContainer, SecretErrorContainer, PasswordContainer } from "./components"; export const ViewSecretPublicPage = () => { + const [passMatches, setPassMatch] = useState(false) const router = useRouter(); const { id, key: urlEncodedPublicKey } = router.query; @@ -21,6 +23,10 @@ export const ViewSecretPublicPage = () => { hashedHex }); + const handlePassMatch = useCallback((val: boolean) => { + setPassMatch(val) + }, [setPassMatch]) + return (
@@ -52,7 +58,13 @@ export const ViewSecretPublicPage = () => {

- {secret && key && } + {secret && ( + !passMatches ? ( + + ) : ( + key && + ) + )} {error && }
diff --git a/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx b/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx new file mode 100644 index 000000000..b0a47a0b2 --- /dev/null +++ b/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx @@ -0,0 +1,60 @@ +import { useState, ChangeEvent } from "react"; +import { + faArrowRight, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { Button, IconButton, Input } from "@app/components/v2"; +import { TViewSharedSecretResponse } from "@app/hooks/api/secretSharing"; + +type Props = { + secret: TViewSharedSecretResponse; + handlePassMatch: (val: boolean) => void; +}; + +export const PasswordContainer = ({ secret, handlePassMatch }: Props) => { + const [password, setPassword] = useState('') + + const handleChange = (e: ChangeEvent) => { + setPassword(e.target.value) + } + + const validatePassword = () => { + if (secret.password === password) { + handlePassMatch(true) + console.log('match') + } else { + console.log({secret}) + } + } + + return ( +
+
+ +
+ { + validatePassword() + }} + > + + +
+
+ +
+ ); +}; diff --git a/frontend/src/views/ViewSecretPublicPage/components/index.tsx b/frontend/src/views/ViewSecretPublicPage/components/index.tsx index b3424d790..fd66da812 100644 --- a/frontend/src/views/ViewSecretPublicPage/components/index.tsx +++ b/frontend/src/views/ViewSecretPublicPage/components/index.tsx @@ -1,2 +1,3 @@ export { SecretContainer } from "./SecretContainer"; export { SecretErrorContainer } from "./SecretErrorContainer"; +export { PasswordContainer } from "./PasswordContainer"; From 4de1713a18c88bd5dda0ea7e3629f726eb24adfb Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Tue, 6 Aug 2024 14:28:02 +0300 Subject: [PATCH 09/25] fix: remove error logs --- .../ViewSecretPublicPage/components/PasswordContainer.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx b/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx index b0a47a0b2..20334837e 100644 --- a/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx +++ b/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx @@ -22,9 +22,6 @@ export const PasswordContainer = ({ secret, handlePassMatch }: Props) => { const validatePassword = () => { if (secret.password === password) { handlePassMatch(true) - console.log('match') - } else { - console.log({secret}) } } From ad89ffe94d5c5513d069aea331c0e85e6d29ff58 Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Tue, 6 Aug 2024 14:42:01 +0300 Subject: [PATCH 10/25] feat: show secret if no password was set --- .../src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx b/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx index ee5c4a65c..337c581fb 100644 --- a/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx +++ b/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx @@ -59,7 +59,7 @@ export const ViewSecretPublicPage = () => {

{secret && ( - !passMatches ? ( + !passMatches && secret.password ? ( ) : ( key && From 15cc157c5f38e3f2a65ea72bc53de3cf8a456a9c Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Tue, 6 Aug 2024 15:32:48 +0300 Subject: [PATCH 11/25] fix(lint): make password optional --- frontend/src/hooks/api/secretSharing/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/hooks/api/secretSharing/types.ts b/frontend/src/hooks/api/secretSharing/types.ts index c0dd2f8d6..47263d977 100644 --- a/frontend/src/hooks/api/secretSharing/types.ts +++ b/frontend/src/hooks/api/secretSharing/types.ts @@ -15,6 +15,7 @@ export type TSharedSecret = { export type TCreateSharedSecretRequest = { name?: string; + password?: string; encryptedValue: string; hashedHex: string; iv: string; From e420973dd274b5c8f5dfa9776ea4e42b393abbf8 Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Tue, 6 Aug 2024 17:01:13 +0300 Subject: [PATCH 12/25] feat: hashpassword and add validation endpoint --- .../server/routes/v1/secret-sharing-router.ts | 44 +++++++++++++++++++ .../secret-sharing/secret-sharing-service.ts | 5 ++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index 95fe4e54b..7a5361049 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import bcrypt from "bcrypt" import { SecretSharingSchema } from "@app/db/schemas"; import { SecretSharingAccessType } from "@app/lib/types"; @@ -94,6 +95,49 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => } }); + server.route({ + method: "POST", + url: "/public/:id/validate", + config: { + rateLimit: publicEndpointLimit + }, + schema: { + params: z.object({ + id: z.string().uuid() + }), + body: z.object({ + password: z.string().min(1), + hashedHex: z.string() + }), + response: { + 200: z.object({ + isValid: z.boolean() + }) + } + }, + handler: async (req) => { + const { id } = req.params; + const { password, hashedHex } = req.body; + + const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretById({ + sharedSecretId: id, + hashedHex, + orgId: req.permission?.orgId + }); + + if (!sharedSecret) { + return { isValid: false }; + } + + if (sharedSecret.password) { + const isMatch = await bcrypt.compare(password, sharedSecret.password); + return { isValid: isMatch }; + } + + return { isValid: false }; + } + }); + server.route({ method: "POST", url: "/public", diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 71ce43de4..73a1bb6e8 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -1,3 +1,5 @@ +import bcrypt from "bcrypt"; + import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { SecretSharingAccessType } from "@app/lib/types"; @@ -61,9 +63,10 @@ export const secretSharingServiceFactory = ({ throw new BadRequestError({ message: "Shared secret value too long" }); } + const hashedPassword = password ? await bcrypt.hash(password, 10) : null; const newSharedSecret = await secretSharingDAL.create({ name, - password, + password: hashedPassword, encryptedValue, hashedHex, iv, From 3c643595977549552da193dfc7f3ac12709e6c32 Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Tue, 6 Aug 2024 18:29:43 +0300 Subject: [PATCH 13/25] feat: handle error logs and validate password --- .../src/hooks/api/secretSharing/queries.ts | 28 ++++++++++- frontend/src/hooks/api/secretSharing/types.ts | 4 ++ .../ViewSecretPublicPage.tsx | 2 +- .../components/PasswordContainer.tsx | 46 ++++++++++++++----- 4 files changed, 67 insertions(+), 13 deletions(-) diff --git a/frontend/src/hooks/api/secretSharing/queries.ts b/frontend/src/hooks/api/secretSharing/queries.ts index 403521759..d8aee233c 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 } from "./types"; +import { TSharedSecret, TViewSharedSecretResponse, ValidateSecretPassword } from "./types"; export const secretSharingKeys = { allSharedSecrets: () => ["sharedSecrets"] as const, @@ -67,3 +67,29 @@ export const useGetActiveSharedSecretById = ({ } }); }; + +export const useValidateSecretPassword = ({ + sharedSecretId, + hashedHex, + userPassword, +}: { + sharedSecretId: string; + hashedHex: string; + userPassword: string; +}) => { + return useQuery({ + enabled: Boolean(sharedSecretId) && Boolean(userPassword), + queryKey: `validateSecretPass-${sharedSecretId}`, + queryFn: async () => { + const { data, isLoading, refetch } = await apiRequest.post( + `/api/v1/secret-sharing/public/${sharedSecretId}/validate`, + { + hashedHex, + password: userPassword + } + ); + + return { data, isLoading, refetch } + } + }) +} diff --git a/frontend/src/hooks/api/secretSharing/types.ts b/frontend/src/hooks/api/secretSharing/types.ts index 47263d977..253321cbf 100644 --- a/frontend/src/hooks/api/secretSharing/types.ts +++ b/frontend/src/hooks/api/secretSharing/types.ts @@ -42,3 +42,7 @@ export enum SecretSharingAccessType { Anyone = "anyone", Organization = "organization" } + +export type ValidateSecretPassword = { + isValid: boolean +} diff --git a/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx b/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx index 337c581fb..7352235da 100644 --- a/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx +++ b/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx @@ -60,7 +60,7 @@ export const ViewSecretPublicPage = () => {
{secret && ( !passMatches && secret.password ? ( - + ) : ( key && ) diff --git a/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx b/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx index 20334837e..6a0ea649b 100644 --- a/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx +++ b/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx @@ -1,27 +1,51 @@ import { useState, ChangeEvent } from "react"; -import { - faArrowRight, -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { faArrowRight } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon, faSpinner } from "@fortawesome/react-fontawesome"; import { Button, IconButton, Input } from "@app/components/v2"; -import { TViewSharedSecretResponse } from "@app/hooks/api/secretSharing"; +import { useValidateSecretPassword } from "@app/hooks/api/secretSharing"; +import { createNotification } from "@app/components/notifications"; type Props = { - secret: TViewSharedSecretResponse; + secretId: string; + hashedHex: string; handlePassMatch: (val: boolean) => void; }; -export const PasswordContainer = ({ secret, handlePassMatch }: Props) => { +export const PasswordContainer = ({ secretId, hashedHex, handlePassMatch }: Props) => { const [password, setPassword] = useState('') + const [isLoading, setLoading] = useState(false) + const { refetch } = useValidateSecretPassword({ + sharedSecretId: secretId, + hashedHex, + userPassword: password, + }) const handleChange = (e: ChangeEvent) => { setPassword(e.target.value) } - const validatePassword = () => { - if (secret.password === password) { - handlePassMatch(true) + const validatePassword = async () => { + try { + const { data: freshData } = await refetch(); + + if (freshData.data.isValid === true) { + handlePassMatch(true); + } else { + 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" + }) + } finally { + setLoading(false); } } @@ -38,7 +62,7 @@ export const PasswordContainer = ({ secret, handlePassMatch }: Props) => { validatePassword() }} > - +
From fb719a9383ece30fa8a655fa18625672f303e702 Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Tue, 6 Aug 2024 18:52:03 +0300 Subject: [PATCH 14/25] fix(lint): fix some lint issues --- .../server/routes/v1/secret-sharing-router.ts | 6 +++--- frontend/src/hooks/api/secretSharing/queries.ts | 17 +++++++++-------- .../components/ShareSecretForm.tsx | 2 +- .../ViewSecretPublicPage.tsx | 2 +- .../components/PasswordContainer.tsx | 13 +++++++++---- 5 files changed, 23 insertions(+), 17 deletions(-) diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index 7a5361049..22279d8ea 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -1,5 +1,5 @@ +import bcrypt from "bcrypt"; import { z } from "zod"; -import bcrypt from "bcrypt" import { SecretSharingSchema } from "@app/db/schemas"; import { SecretSharingAccessType } from "@app/lib/types"; @@ -124,11 +124,11 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => hashedHex, orgId: req.permission?.orgId }); - + if (!sharedSecret) { return { isValid: false }; } - + if (sharedSecret.password) { const isMatch = await bcrypt.compare(password, sharedSecret.password); return { isValid: isMatch }; diff --git a/frontend/src/hooks/api/secretSharing/queries.ts b/frontend/src/hooks/api/secretSharing/queries.ts index d8aee233c..02aa20f4a 100644 --- a/frontend/src/hooks/api/secretSharing/queries.ts +++ b/frontend/src/hooks/api/secretSharing/queries.ts @@ -77,19 +77,20 @@ export const useValidateSecretPassword = ({ hashedHex: string; userPassword: string; }) => { - return useQuery({ - enabled: Boolean(sharedSecretId) && Boolean(userPassword), - queryKey: `validateSecretPass-${sharedSecretId}`, - queryFn: async () => { - const { data, isLoading, refetch } = await apiRequest.post( + return useQuery( + [`validateSecretPass-${sharedSecretId}`], + async () => { + const { data } = await apiRequest.post( `/api/v1/secret-sharing/public/${sharedSecretId}/validate`, { hashedHex, password: userPassword } ); - - return { data, isLoading, refetch } + return data; + }, + { + enabled: Boolean(sharedSecretId) && Boolean(userPassword), } - }) + ) } diff --git a/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx b/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx index 87dc34ad6..f4a647fb2 100644 --- a/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx +++ b/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx @@ -160,7 +160,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => { isError={Boolean(error)} errorText={error?.message} > - + )} /> diff --git a/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx b/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx index 7352235da..620195910 100644 --- a/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx +++ b/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx @@ -60,7 +60,7 @@ export const ViewSecretPublicPage = () => { {secret && ( !passMatches && secret.password ? ( - + ) : ( key && ) diff --git a/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx b/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx index 6a0ea649b..c433bf1fe 100644 --- a/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx +++ b/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx @@ -1,7 +1,7 @@ import { useState, ChangeEvent } from "react"; -import { faArrowRight } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon, faSpinner } from "@fortawesome/react-fontawesome"; +import { faArrowRight, faSpinner } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Button, IconButton, Input } from "@app/components/v2"; import { useValidateSecretPassword } from "@app/hooks/api/secretSharing"; @@ -27,10 +27,12 @@ export const PasswordContainer = ({ secretId, hashedHex, handlePassMatch }: Prop } const validatePassword = async () => { + setLoading(true); + try { const { data: freshData } = await refetch(); - if (freshData.data.isValid === true) { + if (freshData?.isValid === true) { handlePassMatch(true); } else { createNotification({ @@ -62,7 +64,10 @@ export const PasswordContainer = ({ secretId, hashedHex, handlePassMatch }: Prop validatePassword() }} > - + From 406da1b5f09facf7bbfadbc0a0e37bc1fceb03c0 Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Wed, 7 Aug 2024 08:27:17 +0300 Subject: [PATCH 15/25] refactor: convert usequery hook to normal fetch fn (no need for caching) --- .../src/hooks/api/secretSharing/queries.ts | 37 +++++++------------ .../components/PasswordContainer.tsx | 15 ++++---- 2 files changed, 20 insertions(+), 32 deletions(-) diff --git a/frontend/src/hooks/api/secretSharing/queries.ts b/frontend/src/hooks/api/secretSharing/queries.ts index 02aa20f4a..de9556a09 100644 --- a/frontend/src/hooks/api/secretSharing/queries.ts +++ b/frontend/src/hooks/api/secretSharing/queries.ts @@ -68,29 +68,18 @@ export const useGetActiveSharedSecretById = ({ }); }; -export const useValidateSecretPassword = ({ - sharedSecretId, - hashedHex, - userPassword, -}: { - sharedSecretId: string; - hashedHex: string; - userPassword: string; -}) => { - return useQuery( - [`validateSecretPass-${sharedSecretId}`], - async () => { - const { data } = await apiRequest.post( - `/api/v1/secret-sharing/public/${sharedSecretId}/validate`, - { - hashedHex, - password: userPassword - } - ); - return data; - }, +export const fetchIsSecretPasswordValid = async ( + sharedSecretId: string, + hashedHex: string, + password: string, +) => { + const { data } = await apiRequest.post( + `/api/v1/secret-sharing/public/${sharedSecretId}/validate`, { - enabled: Boolean(sharedSecretId) && Boolean(userPassword), + hashedHex, + password } - ) -} + ); + + return data; +}; diff --git a/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx b/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx index c433bf1fe..d3316416e 100644 --- a/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx +++ b/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx @@ -4,7 +4,7 @@ import { faArrowRight, faSpinner } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Button, IconButton, Input } from "@app/components/v2"; -import { useValidateSecretPassword } from "@app/hooks/api/secretSharing"; +import { fetchIsSecretPasswordValid } from "@app/hooks/api/secretSharing"; import { createNotification } from "@app/components/notifications"; type Props = { @@ -16,11 +16,6 @@ type Props = { export const PasswordContainer = ({ secretId, hashedHex, handlePassMatch }: Props) => { const [password, setPassword] = useState('') const [isLoading, setLoading] = useState(false) - const { refetch } = useValidateSecretPassword({ - sharedSecretId: secretId, - hashedHex, - userPassword: password, - }) const handleChange = (e: ChangeEvent) => { setPassword(e.target.value) @@ -30,9 +25,13 @@ export const PasswordContainer = ({ secretId, hashedHex, handlePassMatch }: Prop setLoading(true); try { - const { data: freshData } = await refetch(); + const data = await fetchIsSecretPasswordValid( + secretId, + hashedHex, + password, + ) - if (freshData?.isValid === true) { + if (data?.isValid === true) { handlePassMatch(true); } else { createNotification({ From 56f2a3afa4c878cf77aee4dd3f771aeb7f0a403f Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Wed, 7 Aug 2024 16:06:37 +0300 Subject: [PATCH 16/25] feat: only fetch secret if password wasn't set on initial load --- backend/src/db/schemas/secret-sharing.ts | 1 - .../server/routes/v1/secret-sharing-router.ts | 43 +++++++++++-------- .../secret-sharing/secret-sharing-service.ts | 25 +++++++++-- .../secret-sharing/secret-sharing-types.ts | 4 ++ 4 files changed, 52 insertions(+), 21 deletions(-) diff --git a/backend/src/db/schemas/secret-sharing.ts b/backend/src/db/schemas/secret-sharing.ts index be5643e2b..de75fbafc 100644 --- a/backend/src/db/schemas/secret-sharing.ts +++ b/backend/src/db/schemas/secret-sharing.ts @@ -21,7 +21,6 @@ export const SecretSharingSchema = z.object({ expiresAfterViews: z.number().nullable().optional(), accessType: z.string().default("anyone"), name: z.string().nullable().optional(), - password: z.string().nullable().optional(), lastViewedAt: z.date().nullable().optional() }); diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index 22279d8ea..5fedfd5a0 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -1,4 +1,3 @@ -import bcrypt from "bcrypt"; import { z } from "zod"; import { SecretSharingSchema } from "@app/db/schemas"; @@ -64,7 +63,6 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => response: { 200: SecretSharingSchema.pick({ encryptedValue: true, - password: true, iv: true, tag: true, expiresAt: true, @@ -81,12 +79,14 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => hashedHex: req.query.hashedHex, orgId: req.permission?.orgId }); - if (!sharedSecret) return undefined; + + // only return secret if it exists and has no password set + if (!sharedSecret || sharedSecret.password) return undefined; + return { encryptedValue: sharedSecret.encryptedValue, iv: sharedSecret.iv, tag: sharedSecret.tag, - password: sharedSecret.password, expiresAt: sharedSecret.expiresAt, expiresAfterViews: sharedSecret.expiresAfterViews, accessType: sharedSecret.accessType, @@ -110,8 +110,15 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => hashedHex: z.string() }), response: { - 200: z.object({ - isValid: z.boolean() + 200: SecretSharingSchema.pick({ + encryptedValue: true, + iv: true, + tag: true, + expiresAt: true, + expiresAfterViews: true, + accessType: true, + }).extend({ + orgName: z.string().optional() }) } }, @@ -119,22 +126,24 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => const { id } = req.params; const { password, hashedHex } = req.body; - const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretById({ + const sharedSecret = await req.server.services.secretSharing.validateSecretPassword({ sharedSecretId: id, hashedHex, - orgId: req.permission?.orgId + orgId: req.permission?.orgId, + password }); - if (!sharedSecret) { - return { isValid: false }; - } + if (!sharedSecret) return undefined; - if (sharedSecret.password) { - const isMatch = await bcrypt.compare(password, sharedSecret.password); - return { isValid: isMatch }; - } - - return { isValid: false }; + return { + encryptedValue: sharedSecret.encryptedValue, + iv: sharedSecret.iv, + tag: sharedSecret.tag, + expiresAt: sharedSecret.expiresAt, + expiresAfterViews: sharedSecret.expiresAfterViews, + accessType: sharedSecret.accessType, + orgName: sharedSecret.orgName + }; } }); diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 73a1bb6e8..2f01c6748 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -11,7 +11,8 @@ import { TCreateSharedSecretDTO, TDeleteSharedSecretDTO, TGetActiveSharedSecretByIdDTO, - TGetSharedSecretsDTO + TGetSharedSecretsDTO, + TValidateActiveSharedSecretDTO } from "./secret-sharing-types"; type TSecretSharingServiceFactoryDep = { @@ -108,8 +109,9 @@ export const secretSharingServiceFactory = ({ throw new BadRequestError({ message: "Shared secret value too long" }); } + const hashedPassword = password ? await bcrypt.hash(password, 10) : null; const newSharedSecret = await secretSharingDAL.create({ - password, + password: hashedPassword, encryptedValue, hashedHex, iv, @@ -211,6 +213,22 @@ export const secretSharingServiceFactory = ({ }; }; + const validateSecretPassword = async ({ + sharedSecretId, + hashedHex, + orgId, + password + }: TValidateActiveSharedSecretDTO) => { + const sharedSecret = await getActiveSharedSecretById({ sharedSecretId, hashedHex, orgId }); + + if (!sharedSecret || !sharedSecret.password) return undefined; + + const isMatch = await bcrypt.compare(password, sharedSecret.password); + + if (!isMatch) return undefined; + return sharedSecret + }; + const deleteSharedSecretById = async (deleteSharedSecretInput: TDeleteSharedSecretDTO) => { const { actor, actorId, orgId, actorAuthMethod, actorOrgId, sharedSecretId } = deleteSharedSecretInput; const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); @@ -224,6 +242,7 @@ export const secretSharingServiceFactory = ({ createPublicSharedSecret, getSharedSecrets, deleteSharedSecretById, - getActiveSharedSecretById + getActiveSharedSecretById, + validateSecretPassword }; }; diff --git a/backend/src/services/secret-sharing/secret-sharing-types.ts b/backend/src/services/secret-sharing/secret-sharing-types.ts index 16a89077a..e96a68e64 100644 --- a/backend/src/services/secret-sharing/secret-sharing-types.ts +++ b/backend/src/services/secret-sharing/secret-sharing-types.ts @@ -35,6 +35,10 @@ export type TGetActiveSharedSecretByIdDTO = { orgId?: string; }; +export type TValidateActiveSharedSecretDTO = TGetActiveSharedSecretByIdDTO & { + password: string; +}; + export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO; export type TDeleteSharedSecretDTO = { From f12d4d80c6c1386b6dfde9b07cc8be581d45f5e6 Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Wed, 7 Aug 2024 16:07:08 +0300 Subject: [PATCH 17/25] feat: address changes on the client --- .../src/hooks/api/secretSharing/queries.ts | 6 +- .../components/ShareSecretForm.tsx | 3 +- .../ViewSecretPublicPage.tsx | 23 +++-- .../components/PasswordContainer.tsx | 93 ++++++++++++------- 4 files changed, 77 insertions(+), 48 deletions(-) diff --git a/frontend/src/hooks/api/secretSharing/queries.ts b/frontend/src/hooks/api/secretSharing/queries.ts index de9556a09..b697b92b4 100644 --- a/frontend/src/hooks/api/secretSharing/queries.ts +++ b/frontend/src/hooks/api/secretSharing/queries.ts @@ -56,6 +56,9 @@ export const useGetActiveSharedSecretById = ({ params } ); + + if (!data) return null; + return { encryptedValue: data.encryptedValue, password: data.password, @@ -68,7 +71,8 @@ export const useGetActiveSharedSecretById = ({ }); }; -export const fetchIsSecretPasswordValid = async ( +// returns a secret (secret or undefined if password doesn't match) +export const fetchSecretIfPasswordIsValid = async ( sharedSecretId: string, hashedHex: string, password: string, diff --git a/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx b/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx index f4a647fb2..1a850c07e 100644 --- a/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx +++ b/frontend/src/views/ShareSecretPublicPage/components/ShareSecretForm.tsx @@ -156,9 +156,10 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => { name="password" render={({ field, fieldState: { error } }) => ( diff --git a/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx b/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx index 620195910..fecf72659 100644 --- a/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx +++ b/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback } from 'react' +import { useState, useCallback, useEffect } from 'react' import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -10,7 +10,7 @@ import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing"; import { SecretContainer, SecretErrorContainer, PasswordContainer } from "./components"; export const ViewSecretPublicPage = () => { - const [passMatches, setPassMatch] = useState(false) + const [secret, setSecret] = useState(null) const router = useRouter(); const { id, key: urlEncodedPublicKey } = router.query; @@ -18,14 +18,18 @@ export const ViewSecretPublicPage = () => { ? urlEncodedPublicKey.toString().split("-") : ["", ""]; - const { data: secret, error } = useGetActiveSharedSecretById({ + const { data, error } = useGetActiveSharedSecretById({ sharedSecretId: id as string, hashedHex }); - const handlePassMatch = useCallback((val: boolean) => { - setPassMatch(val) - }, [setPassMatch]) + useEffect(() => { + if (data) setSecret(data) + }, [data]) + + const handleSecret = useCallback((val: any) => { + setSecret(val) + }, [setSecret]) return (
@@ -58,13 +62,12 @@ export const ViewSecretPublicPage = () => {

- {secret && ( - !passMatches && secret.password ? ( - + {!secret ? ( + ) : ( key && ) - )} + } {error && }
diff --git a/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx b/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx index d3316416e..3ff918f24 100644 --- a/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx +++ b/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx @@ -1,43 +1,52 @@ -import { useState, ChangeEvent } from "react"; +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 { Button, IconButton, Input } from "@app/components/v2"; -import { fetchIsSecretPasswordValid } from "@app/hooks/api/secretSharing"; +import { Button, FormControl, IconButton, Input } from "@app/components/v2"; +import { fetchSecretIfPasswordIsValid } from "@app/hooks/api/secretSharing"; import { createNotification } from "@app/components/notifications"; type Props = { secretId: string; hashedHex: string; - handlePassMatch: (val: boolean) => void; + handleSecret: (val: any) => void; }; -export const PasswordContainer = ({ secretId, hashedHex, handlePassMatch }: Props) => { - const [password, setPassword] = useState('') - const [isLoading, setLoading] = useState(false) +const formSchema = z.object({ + password: z.string() +}) - const handleChange = (e: ChangeEvent) => { - setPassword(e.target.value) - } +export type FormData = z.infer; - const validatePassword = async () => { - setLoading(true); +export const PasswordContainer = ({ secretId, hashedHex, handleSecret }: Props) => { + const { + control, + reset, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema), + }); + const onFormSubmit = async ({ password }: FormData) => { try { - const data = await fetchIsSecretPasswordValid( + const secret = await fetchSecretIfPasswordIsValid( secretId, hashedHex, password, ) - if (data?.isValid === true) { - handlePassMatch(true); + if (secret) { + handleSecret(secret); } else { createNotification({ text: "Password is Invalid. Try again", type: "error" }) + reset(); } } catch (error) { console.error("Failed to validate password:", error); @@ -45,31 +54,43 @@ export const PasswordContainer = ({ secretId, hashedHex, handlePassMatch }: Prop text: "Failed to validate password", type: "error" }) - } finally { - setLoading(false); } - } + }; return (
-
- -
- { - validatePassword() - }} - > - - -
-
+
+ ( + +
+ +
+ + + +
+
+
+ )} + /> + +
- {!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" >
- +
Date: Wed, 7 Aug 2024 22:59:50 +0300 Subject: [PATCH 20/25] fix(lint): fix type errors --- backend/src/db/schemas/secret-sharing.ts | 1 + .../server/routes/v1/secret-sharing-router.ts | 1 - .../secret-sharing/secret-sharing-service.ts | 9 ++++++-- .../src/hooks/api/secretSharing/queries.ts | 23 +++++++++---------- .../ViewSecretPublicPage.tsx | 8 +++---- 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/backend/src/db/schemas/secret-sharing.ts b/backend/src/db/schemas/secret-sharing.ts index de75fbafc..be5643e2b 100644 --- a/backend/src/db/schemas/secret-sharing.ts +++ b/backend/src/db/schemas/secret-sharing.ts @@ -21,6 +21,7 @@ export const SecretSharingSchema = z.object({ expiresAfterViews: z.number().nullable().optional(), accessType: z.string().default("anyone"), name: z.string().nullable().optional(), + password: z.string().nullable().optional(), lastViewedAt: z.date().nullable().optional() }); diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index de1bfe968..e13107131 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -80,7 +80,6 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => orgId: req.permission?.orgId }); - // 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 b37f8a771..80d661b40 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -1,7 +1,7 @@ import bcrypt from "bcrypt"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, InternalServerError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { SecretSharingAccessType } from "@app/lib/types"; import { TOrgDALFactory } from "../org/org-dal"; @@ -217,7 +217,12 @@ export const secretSharingServiceFactory = ({ if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) throw new UnauthorizedError(); - const isMatch = await bcrypt.compare(password, sharedSecret.password); + if (!sharedSecret.password) + throw new InternalServerError({ + message: "Something went wrong" + }); + + const isMatch = await bcrypt.compare(password, sharedSecret.password as string); if (!isMatch) return undefined // we reduce the view count when we are sure the password matches. diff --git a/frontend/src/hooks/api/secretSharing/queries.ts b/frontend/src/hooks/api/secretSharing/queries.ts index acf091e5b..d94ca243d 100644 --- a/frontend/src/hooks/api/secretSharing/queries.ts +++ b/frontend/src/hooks/api/secretSharing/queries.ts @@ -43,32 +43,31 @@ export const useGetActiveSharedSecretById = ({ sharedSecretId: string; hashedHex: string; }) => { - return useQuery({ - enabled: Boolean(sharedSecretId) && Boolean(hashedHex), - queryFn: async () => { - const params = new URLSearchParams({ - hashedHex - }); - + return useQuery( + [`sharedSecret-${sharedSecretId}`], + async () => { + const params = new URLSearchParams({ hashedHex }); const { data } = await apiRequest.get( `/api/v1/secret-sharing/public/${sharedSecretId}`, { params } ); - - if (!data) return null; - + + if (!data) return null + return { encryptedValue: data.encryptedValue, - password: data.password, iv: data.iv, tag: data.tag, accessType: data.accessType, orgName: data.orgName }; + }, + { + enabled: Boolean(sharedSecretId) && Boolean(hashedHex) } - }); + ); }; // returns a secret (secret or undefined if password doesn't match) diff --git a/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx b/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx index 27f1d37bd..78ce15d14 100644 --- a/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx +++ b/frontend/src/views/ViewSecretPublicPage/ViewSecretPublicPage.tsx @@ -10,8 +10,7 @@ import { TViewSharedSecretResponse, useGetActiveSharedSecretById } from "@app/ho import { SecretContainer, SecretErrorContainer, PasswordContainer } from "./components"; export const ViewSecretPublicPage = () => { - const [secret, setSecret] = useState(null) - const [error, setError] = useState(null) + const [secret, setSecret] = useState(null); const router = useRouter(); const { id, key: urlEncodedPublicKey } = router.query; @@ -19,15 +18,14 @@ export const ViewSecretPublicPage = () => { ? urlEncodedPublicKey.toString().split("-") : ["", ""]; - const { data: fetchSecret, error: fetchError, isLoading } = useGetActiveSharedSecretById({ + const { data: fetchSecret, error, isLoading } = useGetActiveSharedSecretById({ sharedSecretId: id as string, hashedHex }); useEffect(() => { if (fetchSecret) setSecret(fetchSecret) - if (fetchError) setError(fetchError) - }, [fetchSecret, fetchError]) + }, [fetchSecret, error]) const handleSecret = useCallback((value: TViewSharedSecretResponse) => { setSecret(value) From 069651bdb438d2b88ccae16338e513fd7e89768f Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Wed, 7 Aug 2024 23:26:24 +0300 Subject: [PATCH 21/25] fix: fix lint errors --- .../server/routes/v1/secret-sharing-router.ts | 2 +- .../secret-sharing/secret-sharing-service.ts | 81 ++++++++++--------- .../secret-sharing/secret-sharing-types.ts | 2 + .../components/PasswordContainer.tsx | 2 +- 4 files changed, 48 insertions(+), 39 deletions(-) diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index e13107131..21bcf013c 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -115,7 +115,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => tag: true, expiresAt: true, expiresAfterViews: true, - accessType: true, + accessType: true }).extend({ orgName: z.string().optional() }) diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 80d661b40..08dbdc91f 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -1,12 +1,19 @@ import bcrypt from "bcrypt"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { BadRequestError, ForbiddenRequestError, InternalServerError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + InternalServerError, + NotFoundError, + UnauthorizedError +} from "@app/lib/errors"; import { SecretSharingAccessType } from "@app/lib/types"; import { TOrgDALFactory } from "../org/org-dal"; import { TSecretSharingDALFactory } from "./secret-sharing-dal"; import { + SharedSecretWithDate, TCreatePublicSharedSecretDTO, TCreateSharedSecretDTO, TDeleteSharedSecretDTO, @@ -162,6 +169,40 @@ export const secretSharingServiceFactory = ({ }; }; + /** Checks if secret is expired and throws error if true */ + const checkIfExpired = async (sharedSecret: SharedSecretWithDate, sharedSecretId: string) => { + const { expiresAt, expiresAfterViews } = sharedSecret; + + if (expiresAt !== null && expiresAt < new Date()) { + // check lifetime expiry + await secretSharingDAL.softDeleteById(sharedSecretId); + throw new ForbiddenRequestError({ + message: "Access denied: Secret has expired by lifetime" + }); + } + + if (expiresAfterViews !== null && expiresAfterViews === 0) { + // check view count expiry + await secretSharingDAL.softDeleteById(sharedSecretId); + throw new ForbiddenRequestError({ + message: "Access denied: Secret has expired by view count" + }); + } + }; + + const decrementSecretViewCount = async (sharedSecret: SharedSecretWithDate, sharedSecretId: string) => { + const { expiresAfterViews } = sharedSecret; + + if (expiresAfterViews) { + // decrement view count if view count expiry set + await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } }); + } + + await secretSharingDAL.updateById(sharedSecretId, { + lastViewedAt: new Date() + }); + }; + const getActiveSharedSecretById = async ({ sharedSecretId, hashedHex, orgId }: TGetActiveSharedSecretByIdDTO) => { const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, @@ -180,7 +221,7 @@ export const secretSharingServiceFactory = ({ throw new UnauthorizedError(); await checkIfExpired(sharedSecret, sharedSecretId); - + if (sharedSecret.password !== null) return undefined; // decrement when we are sure the user will view secret. @@ -223,7 +264,7 @@ export const secretSharingServiceFactory = ({ }); const isMatch = await bcrypt.compare(password, sharedSecret.password as string); - if (!isMatch) return undefined + if (!isMatch) return undefined; // we reduce the view count when we are sure the password matches. await decrementSecretViewCount(sharedSecret, sharedSecretId); @@ -245,40 +286,6 @@ export const secretSharingServiceFactory = ({ return deletedSharedSecret; }; - /** 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()) { - // check lifetime expiry - await secretSharingDAL.softDeleteById(sharedSecretId); - throw new ForbiddenRequestError({ - message: "Access denied: Secret has expired by lifetime" - }); - } - - if (expiresAfterViews !== null && expiresAfterViews === 0) { - // check view count expiry - await secretSharingDAL.softDeleteById(sharedSecretId); - throw new ForbiddenRequestError({ - 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 - await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } }); - } - - await secretSharingDAL.updateById(sharedSecretId, { - lastViewedAt: new Date() - }); - } - return { createSharedSecret, createPublicSharedSecret, diff --git a/backend/src/services/secret-sharing/secret-sharing-types.ts b/backend/src/services/secret-sharing/secret-sharing-types.ts index e96a68e64..6720a7bee 100644 --- a/backend/src/services/secret-sharing/secret-sharing-types.ts +++ b/backend/src/services/secret-sharing/secret-sharing-types.ts @@ -41,6 +41,8 @@ export type TValidateActiveSharedSecretDTO = TGetActiveSharedSecretByIdDTO & { export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO; +export type SharedSecretWithDate = Omit & { expiresAt: Date }; + export type TDeleteSharedSecretDTO = { sharedSecretId: string; } & TSharedSecretPermission; diff --git a/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx b/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx index e79a3b5b1..00876225d 100644 --- a/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx +++ b/frontend/src/views/ViewSecretPublicPage/components/PasswordContainer.tsx @@ -42,11 +42,11 @@ export const PasswordContainer = ({ secretId, hashedHex, handleSecret }: Props) if (secret) { handleSecret(secret); } else { + reset({ password: "" }); createNotification({ text: "Password is Invalid. Try again", type: "error" }) - reset(); } } catch (error) { console.error("Failed to validate password:", error); From 8e0b4254b14cd8b27715fd6ddae37461129dd9ec Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Thu, 8 Aug 2024 09:56:18 +0300 Subject: [PATCH 22/25] refactor: fix lint issues and refactor code --- .../server/routes/v1/secret-sharing-router.ts | 6 ++--- .../secret-sharing/secret-sharing-service.ts | 22 +++++++++++-------- .../secret-sharing/secret-sharing-types.ts | 2 -- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index 21bcf013c..00b0fb9b3 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -74,13 +74,13 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretById({ + const sharedSecret = await req.server.services.secretSharing.getPasswordlessSecretByID({ sharedSecretId: req.params.id, hashedHex: req.query.hashedHex, orgId: req.permission?.orgId }); - if (!sharedSecret || sharedSecret.password) return undefined; + if (!sharedSecret) return undefined; return { encryptedValue: sharedSecret.encryptedValue, @@ -125,7 +125,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => const { id } = req.params; const { password, hashedHex } = req.body; - const sharedSecret = await req.server.services.secretSharing.validateSecretPassword({ + const sharedSecret = await req.server.services.secretSharing.getValidatedSecretByID({ sharedSecretId: id, hashedHex, orgId: req.permission?.orgId, diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 08dbdc91f..e7e6d4c16 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -9,11 +9,11 @@ import { UnauthorizedError } from "@app/lib/errors"; import { SecretSharingAccessType } from "@app/lib/types"; +import { TSecretSharing } from "@app/db/schemas"; import { TOrgDALFactory } from "../org/org-dal"; import { TSecretSharingDALFactory } from "./secret-sharing-dal"; import { - SharedSecretWithDate, TCreatePublicSharedSecretDTO, TCreateSharedSecretDTO, TDeleteSharedSecretDTO, @@ -170,7 +170,7 @@ export const secretSharingServiceFactory = ({ }; /** Checks if secret is expired and throws error if true */ - const checkIfExpired = async (sharedSecret: SharedSecretWithDate, sharedSecretId: string) => { + const checkIfSecretIsExpired = async (sharedSecret: TSecretSharing, sharedSecretId: string) => { const { expiresAt, expiresAfterViews } = sharedSecret; if (expiresAt !== null && expiresAt < new Date()) { @@ -190,7 +190,7 @@ export const secretSharingServiceFactory = ({ } }; - const decrementSecretViewCount = async (sharedSecret: SharedSecretWithDate, sharedSecretId: string) => { + const decrementSecretViewCount = async (sharedSecret: TSecretSharing, sharedSecretId: string) => { const { expiresAfterViews } = sharedSecret; if (expiresAfterViews) { @@ -203,7 +203,8 @@ export const secretSharingServiceFactory = ({ }); }; - const getActiveSharedSecretById = async ({ sharedSecretId, hashedHex, orgId }: TGetActiveSharedSecretByIdDTO) => { + /** Get's passwordless secret. validates all secret's requested (must be fresh). */ + const getPasswordlessSecretByID = async ({ sharedSecretId, hashedHex, orgId }: TGetActiveSharedSecretByIdDTO) => { const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex @@ -220,7 +221,9 @@ export const secretSharingServiceFactory = ({ if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) throw new UnauthorizedError(); - await checkIfExpired(sharedSecret, sharedSecretId); + // all secrets pass through here, meaning we check if its expired first and then check if it needs verification + // or can be safely sent to the client. + await checkIfSecretIsExpired(sharedSecret, sharedSecretId); if (sharedSecret.password !== null) return undefined; @@ -236,7 +239,8 @@ export const secretSharingServiceFactory = ({ }; }; - const validateSecretPassword = async ({ + /** Get's the requested secret if password passed is valid */ + const getValidatedSecretByID = async ({ sharedSecretId, hashedHex, orgId, @@ -266,7 +270,7 @@ export const secretSharingServiceFactory = ({ const isMatch = await bcrypt.compare(password, sharedSecret.password as string); if (!isMatch) return undefined; - // we reduce the view count when we are sure the password matches. + // reduce the view count when the password matches (will be returned to the client). await decrementSecretViewCount(sharedSecret, sharedSecretId); return { @@ -291,7 +295,7 @@ export const secretSharingServiceFactory = ({ createPublicSharedSecret, getSharedSecrets, deleteSharedSecretById, - getActiveSharedSecretById, - validateSecretPassword + getPasswordlessSecretByID, + getValidatedSecretByID }; }; diff --git a/backend/src/services/secret-sharing/secret-sharing-types.ts b/backend/src/services/secret-sharing/secret-sharing-types.ts index 6720a7bee..e96a68e64 100644 --- a/backend/src/services/secret-sharing/secret-sharing-types.ts +++ b/backend/src/services/secret-sharing/secret-sharing-types.ts @@ -41,8 +41,6 @@ export type TValidateActiveSharedSecretDTO = TGetActiveSharedSecretByIdDTO & { export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO; -export type SharedSecretWithDate = Omit & { expiresAt: Date }; - export type TDeleteSharedSecretDTO = { sharedSecretId: string; } & TSharedSecretPermission; From 8479c406a516d2a8aeb6271785457c43ed815b4b Mon Sep 17 00:00:00 2001 From: lemmyMwaura Date: Thu, 8 Aug 2024 10:06:55 +0300 Subject: [PATCH 23/25] fix: fix type assersion error --- backend/src/services/secret-sharing/secret-sharing-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index e7e6d4c16..6961a9e6d 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -221,7 +221,7 @@ export const secretSharingServiceFactory = ({ if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) throw new UnauthorizedError(); - // all secrets pass through here, meaning we check if its expired first and then check if it needs verification + // all secrets pass through here, meaning we check if its expired first and then check if it needs verification // or can be safely sent to the client. await checkIfSecretIsExpired(sharedSecret, sharedSecretId); @@ -267,7 +267,7 @@ export const secretSharingServiceFactory = ({ message: "Something went wrong" }); - const isMatch = await bcrypt.compare(password, sharedSecret.password as string); + const isMatch = await bcrypt.compare(password, sharedSecret.password); if (!isMatch) return undefined; // reduce the view count when the password matches (will be returned to the client). From a5555c381637116d18d8bbebe7d73b3c8fe03b24 Mon Sep 17 00:00:00 2001 From: = Date: Sat, 10 Aug 2024 22:19:42 +0530 Subject: [PATCH 24/25] feat: simplified endpoints to support password based secret sharing --- .../server/routes/v1/secret-sharing-router.ts | 99 +++-------- .../secret-sharing/secret-sharing-service.ts | 156 ++++++------------ .../secret-sharing/secret-sharing-types.ts | 1 + 3 files changed, 78 insertions(+), 178 deletions(-) diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index 00b0fb9b3..7a909cae4 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -48,7 +48,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => }); server.route({ - method: "GET", + method: "POST", url: "/public/:id", config: { rateLimit: publicEndpointLimit @@ -57,92 +57,37 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => params: z.object({ id: z.string().uuid() }), - querystring: z.object({ - hashedHex: z.string().min(1) + body: z.object({ + hashedHex: z.string().min(1), + password: z.string().optional() }), response: { - 200: SecretSharingSchema.pick({ - encryptedValue: true, - iv: true, - tag: true, - expiresAt: true, - expiresAfterViews: true, - accessType: true - }).extend({ - orgName: z.string().optional() + 200: z.object({ + isPasswordProtected: z.boolean(), + secret: SecretSharingSchema.pick({ + encryptedValue: true, + iv: true, + tag: true, + expiresAt: true, + expiresAfterViews: true, + accessType: true + }) + .extend({ + orgName: z.string().optional() + }) + .optional() }) } }, handler: async (req) => { - const sharedSecret = await req.server.services.secretSharing.getPasswordlessSecretByID({ + const sharedSecret = await req.server.services.secretSharing.getSharedSecretById({ sharedSecretId: req.params.id, - hashedHex: req.query.hashedHex, + hashedHex: req.body.hashedHex, + password: req.body.password, orgId: req.permission?.orgId }); - if (!sharedSecret) return undefined; - - return { - encryptedValue: sharedSecret.encryptedValue, - iv: sharedSecret.iv, - tag: sharedSecret.tag, - expiresAt: sharedSecret.expiresAt, - expiresAfterViews: sharedSecret.expiresAfterViews, - accessType: sharedSecret.accessType, - orgName: sharedSecret.orgName - }; - } - }); - - server.route({ - method: "POST", - url: "/public/:id/validate", - config: { - rateLimit: publicEndpointLimit - }, - schema: { - params: z.object({ - id: z.string().uuid() - }), - body: z.object({ - password: z.string().min(1), - hashedHex: z.string() - }), - response: { - 200: SecretSharingSchema.pick({ - encryptedValue: true, - iv: true, - tag: true, - expiresAt: true, - expiresAfterViews: true, - accessType: true - }).extend({ - orgName: z.string().optional() - }) - } - }, - handler: async (req) => { - const { id } = req.params; - const { password, hashedHex } = req.body; - - const sharedSecret = await req.server.services.secretSharing.getValidatedSecretByID({ - sharedSecretId: id, - hashedHex, - orgId: req.permission?.orgId, - password - }); - - if (!sharedSecret) return undefined; - - return { - encryptedValue: sharedSecret.encryptedValue, - iv: sharedSecret.iv, - tag: sharedSecret.tag, - expiresAt: sharedSecret.expiresAt, - expiresAfterViews: sharedSecret.expiresAfterViews, - accessType: sharedSecret.accessType, - orgName: sharedSecret.orgName - }; + return sharedSecret; } }); diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 6961a9e6d..6133db559 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -1,15 +1,9 @@ import bcrypt from "bcrypt"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { - BadRequestError, - ForbiddenRequestError, - InternalServerError, - NotFoundError, - UnauthorizedError -} from "@app/lib/errors"; -import { SecretSharingAccessType } from "@app/lib/types"; import { TSecretSharing } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { SecretSharingAccessType } from "@app/lib/types"; import { TOrgDALFactory } from "../org/org-dal"; import { TSecretSharingDALFactory } from "./secret-sharing-dal"; @@ -18,8 +12,7 @@ import { TCreateSharedSecretDTO, TDeleteSharedSecretDTO, TGetActiveSharedSecretByIdDTO, - TGetSharedSecretsDTO, - TValidateActiveSharedSecretDTO + TGetSharedSecretsDTO } from "./secret-sharing-types"; type TSecretSharingServiceFactoryDep = { @@ -169,10 +162,39 @@ export const secretSharingServiceFactory = ({ }; }; - /** Checks if secret is expired and throws error if true */ - const checkIfSecretIsExpired = async (sharedSecret: TSecretSharing, sharedSecretId: string) => { - const { expiresAt, expiresAfterViews } = sharedSecret; + const $decrementSecretViewCount = async (sharedSecret: TSecretSharing, sharedSecretId: string) => { + const { expiresAfterViews } = sharedSecret; + if (expiresAfterViews) { + // decrement view count if view count expiry set + await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } }); + } + + await secretSharingDAL.updateById(sharedSecretId, { + lastViewedAt: new Date() + }); + }; + + /** Get's passwordless secret. validates all secret's requested (must be fresh). */ + const getSharedSecretById = async ({ sharedSecretId, hashedHex, orgId, password }: TGetActiveSharedSecretByIdDTO) => { + const sharedSecret = await secretSharingDAL.findOne({ + id: sharedSecretId, + hashedHex + }); + if (!sharedSecret) + throw new NotFoundError({ + message: "Shared secret not found" + }); + + const { accessType, expiresAt, expiresAfterViews } = sharedSecret; + + const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : ""; + + if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) + throw new UnauthorizedError(); + + // all secrets pass through here, meaning we check if its expired first and then check if it needs verification + // or can be safely sent to the client. if (expiresAt !== null && expiresAt < new Date()) { // check lifetime expiry await secretSharingDAL.softDeleteById(sharedSecretId); @@ -188,97 +210,30 @@ export const secretSharingServiceFactory = ({ message: "Access denied: Secret has expired by view count" }); } - }; - const decrementSecretViewCount = async (sharedSecret: TSecretSharing, sharedSecretId: string) => { - const { expiresAfterViews } = sharedSecret; - - if (expiresAfterViews) { - // decrement view count if view count expiry set - await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } }); + const isPasswordProtected = Boolean(sharedSecret.password); + const hasProvidedPassword = Boolean(password); + if (isPasswordProtected) { + if (hasProvidedPassword) { + const isMatch = await bcrypt.compare(password as string, sharedSecret.password as string); + if (!isMatch) throw new UnauthorizedError({ message: "Invalid credentials" }); + } else { + return { isPasswordProtected }; + } } - await secretSharingDAL.updateById(sharedSecretId, { - lastViewedAt: new Date() - }); - }; - - /** Get's passwordless secret. validates all secret's requested (must be fresh). */ - const getPasswordlessSecretByID = async ({ sharedSecretId, hashedHex, orgId }: TGetActiveSharedSecretByIdDTO) => { - const sharedSecret = await secretSharingDAL.findOne({ - id: sharedSecretId, - hashedHex - }); - if (!sharedSecret) - throw new NotFoundError({ - message: "Shared secret not found" - }); - - const { accessType } = sharedSecret; - - const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : ""; - - if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) - throw new UnauthorizedError(); - - // all secrets pass through here, meaning we check if its expired first and then check if it needs verification - // or can be safely sent to the client. - await checkIfSecretIsExpired(sharedSecret, sharedSecretId); - - if (sharedSecret.password !== null) return undefined; - // decrement when we are sure the user will view secret. - await decrementSecretViewCount(sharedSecret, sharedSecretId); + await $decrementSecretViewCount(sharedSecret, sharedSecretId); return { - ...sharedSecret, - orgName: - sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId - ? orgName - : undefined - }; - }; - - /** Get's the requested secret if password passed is valid */ - const getValidatedSecretByID = async ({ - sharedSecretId, - hashedHex, - orgId, - password - }: TValidateActiveSharedSecretDTO) => { - const sharedSecret = await secretSharingDAL.findOne({ - id: sharedSecretId, - hashedHex - }); - if (!sharedSecret) - throw new NotFoundError({ - message: "Shared secret not found" - }); - - const { accessType } = sharedSecret; - - const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : ""; - - if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) - throw new UnauthorizedError(); - - if (!sharedSecret.password) - throw new InternalServerError({ - message: "Something went wrong" - }); - - const isMatch = await bcrypt.compare(password, sharedSecret.password); - if (!isMatch) return undefined; - - // reduce the view count when the password matches (will be returned to the client). - await decrementSecretViewCount(sharedSecret, sharedSecretId); - - return { - ...sharedSecret, - orgName: - sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId - ? orgName - : undefined + isPasswordProtected, + secret: { + ...sharedSecret, + orgName: + sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId + ? orgName + : undefined + } }; }; @@ -295,7 +250,6 @@ export const secretSharingServiceFactory = ({ createPublicSharedSecret, getSharedSecrets, deleteSharedSecretById, - getPasswordlessSecretByID, - getValidatedSecretByID + getSharedSecretById }; }; diff --git a/backend/src/services/secret-sharing/secret-sharing-types.ts b/backend/src/services/secret-sharing/secret-sharing-types.ts index e96a68e64..794d99a33 100644 --- a/backend/src/services/secret-sharing/secret-sharing-types.ts +++ b/backend/src/services/secret-sharing/secret-sharing-types.ts @@ -33,6 +33,7 @@ export type TGetActiveSharedSecretByIdDTO = { sharedSecretId: string; hashedHex: string; orgId?: string; + password?: string; }; export type TValidateActiveSharedSecretDTO = TGetActiveSharedSecretByIdDTO & { From 3ddb4cd27a8c0470d9f0e715b8daffd7f5d25291 Mon Sep 17 00:00:00 2001 From: = Date: Sat, 10 Aug 2024 22:21:17 +0530 Subject: [PATCH 25/25] 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";