From bca14dd5c4b0cba6fa09c1096b3a0cee4c438c69 Mon Sep 17 00:00:00 2001 From: akhilmhdh Date: Thu, 27 Jul 2023 16:28:12 +0530 Subject: [PATCH] feat: added new secret input component and updated toolbar key special prop to innerKey --- .../src/components/navigation/NavHeader.tsx | 20 +- .../components/v2/SecretInput/SecretInput.tsx | 92 +++ .../src/components/v2/SecretInput/index.tsx | 1 + .../src/components/v2/Table/Table.stories.tsx | 2 +- frontend/src/components/v2/Table/Table.tsx | 39 +- frontend/src/components/v2/index.tsx | 3 +- .../src/hooks/api/secretFolders/index.tsx | 1 + .../src/hooks/api/secretFolders/queries.tsx | 45 +- frontend/src/hooks/api/secretFolders/types.ts | 7 + frontend/src/hooks/api/secrets/index.ts | 3 +- frontend/src/hooks/api/secrets/mutations.tsx | 172 ++++ frontend/src/hooks/api/secrets/queries.tsx | 141 ++-- frontend/src/hooks/api/secrets/types.ts | 29 + frontend/src/layouts/AppLayout/AppLayout.tsx | 599 ++++++++------ .../src/pages/org/[id]/overview/index.tsx | 352 +++++--- .../[id]/secrets/{index.tsx => [env].tsx} | 8 +- .../OrgIncidentContactsTable.tsx | 6 +- .../OrgMembersTable/OrgMembersTable.tsx | 205 +++-- .../OrgServiceAccountsTable.tsx | 626 +++++++------- .../components/SecretScanningLogsTable.tsx | 160 ++-- .../BillingCloudTab/CurrentPlanSection.tsx | 125 ++- .../BillingCloudTab/ManagePlansTable.tsx | 271 +++---- .../BillingDetailsTab/PmtMethodsTable.tsx | 132 ++- .../BillingDetailsTab/TaxIDTable.tsx | 225 +++-- .../BillingReceiptsTab/InvoicesTable.tsx | 131 ++- .../SAProjectLevelPermissionsTable.tsx | 767 +++++++++--------- .../OrgIncidentContactsTable.tsx | 126 ++- .../OrgServiceAccountsTable.tsx | 645 ++++++++------- .../APIKeySection/APIKeyTable.tsx | 178 ++-- .../SessionsSection/SessionsTable.tsx | 107 ++- .../EnvironmentSection/EnvironmentTable.tsx | 138 ++-- .../SecretTagsSection/SecretTagsTable.tsx | 114 +-- .../ServiceTokenSection/ServiceTokenTable.tsx | 2 +- .../components/WebhooksTab/WebhooksTab.tsx | 2 +- 34 files changed, 2969 insertions(+), 2505 deletions(-) create mode 100644 frontend/src/components/v2/SecretInput/SecretInput.tsx create mode 100644 frontend/src/components/v2/SecretInput/index.tsx create mode 100644 frontend/src/hooks/api/secrets/mutations.tsx rename frontend/src/pages/project/[id]/secrets/{index.tsx => [env].tsx} (68%) diff --git a/frontend/src/components/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx index fac8f4749..d4a5c4352 100644 --- a/frontend/src/components/navigation/NavHeader.tsx +++ b/frontend/src/components/navigation/NavHeader.tsx @@ -58,15 +58,13 @@ export default function NavHeader({ return (
-
+
{currentOrg?.name?.charAt(0)}
- - {currentOrg?.name} + + + {currentOrg?.name} + {isProjectRelated && ( <> @@ -85,7 +83,7 @@ export default function NavHeader({ {pageName} @@ -126,7 +124,11 @@ export default function NavHeader({ {index + 1 === folders?.length ? ( {name} ) : ( - + {name === "root" ? selectedEnv?.name : name} diff --git a/frontend/src/components/v2/SecretInput/SecretInput.tsx b/frontend/src/components/v2/SecretInput/SecretInput.tsx new file mode 100644 index 000000000..73d40026a --- /dev/null +++ b/frontend/src/components/v2/SecretInput/SecretInput.tsx @@ -0,0 +1,92 @@ +/* eslint-disable react/no-danger */ +import { HTMLAttributes } from "react"; +import ContentEditable from "react-contenteditable"; +import sanitizeHtml from "sanitize-html"; + +import { useToggle } from "@app/hooks"; + +const REGEX = /\${([^}]+)}/g; +const stripSpanTags = (str: string) => str.replace(/<\/?span[^>]*>/g, ""); +const replaceContentWithDot = (str: string) => { + let finalStr = ""; + let isHtml = false; + for (let i = 0; i < str.length; i += 1) { + const char = str.at(i); + + if (char === "<" || char === ">") { + isHtml = char === "<"; + finalStr += char; + } else if (!isHtml && char !== "\n") { + finalStr += "•"; + } else { + finalStr += char; + } + } + return finalStr; +}; + +const syntaxHighlight = (orgContent?: string | null, isVisible?: boolean) => { + if (!orgContent) return ""; + if (!isVisible) return replaceContentWithDot(orgContent); + const content = stripSpanTags(orgContent); + const newContent = content.replace( + REGEX, + (_a, b) => + `${${b}}` + ); + + return newContent; +}; + +const sanitizeConf = { + allowedTags: ["div", "span", "br", "p"] +}; + +type Props = Omit, "onChange" | "onBlur"> & { + value?: string | null; + isVisible?: boolean; + isDisabled?: boolean; + onChange?: (val: string, html: string) => void; + onBlur?: (sanitizedHtml: string) => void; +}; + +export const SecretInput = ({ + value, + isVisible, + onChange, + onBlur, + isDisabled, + ...props +}: Props) => { + const [isSecretFocused, setIsSecretFocused] = useToggle(); + + return ( +
+
+ { + if (onChange) onChange(evt.currentTarget.innerText.trim(), evt.currentTarget.innerHTML); + }} + onFocus={() => setIsSecretFocused.on()} + disabled={isDisabled} + spellCheck={false} + onBlur={(evt) => { + if (onBlur) onBlur(sanitizeHtml(evt.currentTarget.innerHTML || "", sanitizeConf)); + setIsSecretFocused.off(); + }} + html={isVisible || isSecretFocused ? value || "" : syntaxHighlight(value, false)} + {...props} + /> +
+ ); +}; diff --git a/frontend/src/components/v2/SecretInput/index.tsx b/frontend/src/components/v2/SecretInput/index.tsx new file mode 100644 index 000000000..1c322fac4 --- /dev/null +++ b/frontend/src/components/v2/SecretInput/index.tsx @@ -0,0 +1 @@ +export { SecretInput } from "./SecretInput"; diff --git a/frontend/src/components/v2/Table/Table.stories.tsx b/frontend/src/components/v2/Table/Table.stories.tsx index a7b47e315..95be1bb05 100644 --- a/frontend/src/components/v2/Table/Table.stories.tsx +++ b/frontend/src/components/v2/Table/Table.stories.tsx @@ -52,7 +52,7 @@ export const Loading: Story = { - + diff --git a/frontend/src/components/v2/Table/Table.tsx b/frontend/src/components/v2/Table/Table.tsx index d6cd74e45..7c698e2bb 100644 --- a/frontend/src/components/v2/Table/Table.tsx +++ b/frontend/src/components/v2/Table/Table.tsx @@ -33,10 +33,7 @@ export type TableProps = { export const Table = ({ children, className }: TableProps): JSX.Element => ( {children}
@@ -58,11 +55,24 @@ export const THead = ({ children, className }: THeadProps): JSX.Element => ( export type TrProps = { children: ReactNode; className?: string; + isHoverable?: boolean; + isSelectable?: boolean; } & HTMLAttributes; -export const Tr = ({ children, className, ...props }: TrProps): JSX.Element => ( +export const Tr = ({ + children, + className, + isHoverable, + isSelectable, + ...props +}: TrProps): JSX.Element => ( {children} @@ -76,7 +86,14 @@ export type ThProps = { }; export const Th = ({ children, className }: ThProps): JSX.Element => ( - {children} + + {children} + ); // table body @@ -106,15 +123,15 @@ export type TBodyLoader = { columns: number; className?: string; // unique key for mapping - key: string; + innerKey: string; }; -export const TableSkeleton = ({ rows = 3, columns, key, className }: TBodyLoader): JSX.Element => ( +export const TableSkeleton = ({ rows = 3, columns, innerKey, className }: TBodyLoader): JSX.Element => ( <> {Array.apply(0, Array(rows)).map((_x, i) => ( - + {Array.apply(0, Array(columns)).map((_y, j) => ( - + ))} diff --git a/frontend/src/components/v2/index.tsx b/frontend/src/components/v2/index.tsx index 3ae3db006..b4412b47b 100644 --- a/frontend/src/components/v2/index.tsx +++ b/frontend/src/components/v2/index.tsx @@ -4,7 +4,7 @@ export * from "./Checkbox"; export * from "./DeleteActionModal"; export * from "./Drawer"; export * from "./Dropdown"; -export * from "./EmailServiceSetupModal" +export * from "./EmailServiceSetupModal"; export * from "./EmptyState"; export * from "./FormControl"; export * from "./HoverCardv2"; @@ -13,6 +13,7 @@ export * from "./Input"; export * from "./Menu"; export * from "./Modal"; export * from "./Popoverv2"; +export * from "./SecretInput"; export * from "./Select"; export * from "./Skeleton"; export * from "./Spinner"; diff --git a/frontend/src/hooks/api/secretFolders/index.tsx b/frontend/src/hooks/api/secretFolders/index.tsx index 4c25ec3a2..3c0692eb6 100644 --- a/frontend/src/hooks/api/secretFolders/index.tsx +++ b/frontend/src/hooks/api/secretFolders/index.tsx @@ -1,6 +1,7 @@ export { useCreateFolder, useDeleteFolder, + useGetFoldersByEnv, useGetProjectFolders, useGetProjectFoldersBatch, useUpdateFolder diff --git a/frontend/src/hooks/api/secretFolders/queries.tsx b/frontend/src/hooks/api/secretFolders/queries.tsx index 013a28793..36d013b72 100644 --- a/frontend/src/hooks/api/secretFolders/queries.tsx +++ b/frontend/src/hooks/api/secretFolders/queries.tsx @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useCallback, useMemo } from "react"; import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; @@ -9,6 +9,7 @@ import { DeleteFolderDTO, GetProjectFoldersBatchDTO, GetProjectFoldersDTO, + TGetFoldersByEnvDTO, TSecretFolder, UpdateFolderDTO } from "./types"; @@ -62,6 +63,48 @@ export const useGetProjectFolders = ({ ) }); +export const useGetFoldersByEnv = ({ + parentFolderPath, + workspaceId, + environments, + parentFolderId +}: TGetFoldersByEnvDTO) => { + const folders = useQueries({ + queries: environments.map((env) => ({ + queryKey: queryKeys.getSecretFolders(workspaceId, env, parentFolderPath || parentFolderId), + queryFn: async () => fetchProjectFolders(workspaceId, env, parentFolderId, parentFolderPath), + enabled: Boolean(workspaceId) && Boolean(env) + })) + }); + + const folderNames = useMemo(() => { + const names = new Set(); + folders?.forEach(({ data }) => { + data?.folders.forEach(({ name }) => { + names.add(name); + }); + }); + return [...names]; + }, [(folders || []).map((folder) => folder.data)]); + + const isFolderPresentInEnv = useCallback( + (name: string, env: string) => { + const selectedEnvIndex = environments.indexOf(env); + if (selectedEnvIndex !== -1) { + return Boolean( + folders?.[selectedEnvIndex]?.data?.folders?.find( + ({ name: folderName }) => folderName === name + ) + ); + } + return false; + }, + [(folders || []).map((folder) => folder.data)] + ); + + return { folders, folderNames, isFolderPresentInEnv }; +}; + export const useGetProjectFoldersBatch = ({ folders = [], isPaused, diff --git a/frontend/src/hooks/api/secretFolders/types.ts b/frontend/src/hooks/api/secretFolders/types.ts index d6240666a..c9d707620 100644 --- a/frontend/src/hooks/api/secretFolders/types.ts +++ b/frontend/src/hooks/api/secretFolders/types.ts @@ -17,6 +17,13 @@ export type GetProjectFoldersBatchDTO = { parentFolderPath?: string; }; +export type TGetFoldersByEnvDTO = { + environments: string[]; + workspaceId: string; + parentFolderPath?: string; + parentFolderId?: string; +}; + export type CreateFolderDTO = { workspaceId: string; environment: string; diff --git a/frontend/src/hooks/api/secrets/index.ts b/frontend/src/hooks/api/secrets/index.ts index cad1e65d6..2e31e4f0b 100644 --- a/frontend/src/hooks/api/secrets/index.ts +++ b/frontend/src/hooks/api/secrets/index.ts @@ -1,6 +1,7 @@ +export { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "./mutations"; export { useBatchSecretsOp, useGetProjectSecrets, - useGetProjectSecretsByKey, + useGetProjectSecretsAllEnv, useGetSecretVersion } from "./queries"; diff --git a/frontend/src/hooks/api/secrets/mutations.tsx b/frontend/src/hooks/api/secrets/mutations.tsx new file mode 100644 index 000000000..6df00174c --- /dev/null +++ b/frontend/src/hooks/api/secrets/mutations.tsx @@ -0,0 +1,172 @@ +import crypto from "crypto"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { + decryptAssymmetric, + encryptSymmetric +} from "@app/components/utilities/cryptography/crypto"; +import { apiRequest } from "@app/config/request"; + +import { secretKeys } from "./queries"; +import { TCreateSecretsV3DTO, TDeleteSecretsV3DTO, TUpdateSecretsV3DTO } from "./types"; + +const encryptSecret = (randomBytes: string, key: string, value?: string, comment?: string) => { + // encrypt key + const { + ciphertext: secretKeyCiphertext, + iv: secretKeyIV, + tag: secretKeyTag + } = encryptSymmetric({ + plaintext: key, + key: randomBytes + }); + + // encrypt value + const { + ciphertext: secretValueCiphertext, + iv: secretValueIV, + tag: secretValueTag + } = encryptSymmetric({ + plaintext: value ?? "", + key: randomBytes + }); + + // encrypt comment + const { + ciphertext: secretCommentCiphertext, + iv: secretCommentIV, + tag: secretCommentTag + } = encryptSymmetric({ + plaintext: comment ?? "", + key: randomBytes + }); + + return { + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag + }; +}; + +export const useCreateSecretV3 = () => { + const queryClient = useQueryClient(); + return useMutation<{}, {}, TCreateSecretsV3DTO>({ + mutationFn: async ({ + secretPath = "/", + type, + environment, + workspaceId, + secretName, + secretValue, + latestFileKey, + secretComment + }) => { + const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; + + const randomBytes = latestFileKey + ? decryptAssymmetric({ + ciphertext: latestFileKey.encryptedKey, + nonce: latestFileKey.nonce, + publicKey: latestFileKey.sender.publicKey, + privateKey: PRIVATE_KEY + }) + : crypto.randomBytes(16).toString("hex"); + + const reqBody = { + workspaceId, + environment, + type, + secretPath, + ...encryptSecret(randomBytes, secretName, secretValue, secretComment) + }; + const { data } = await apiRequest.post(`/api/v3/secrets/${secretName}`, reqBody); + return data; + }, + onSuccess: (_, { workspaceId, environment, secretPath }) => { + queryClient.invalidateQueries( + secretKeys.getProjectSecret(workspaceId, environment, secretPath) + ); + } + }); +}; + +export const useUpdateSecretV3 = () => { + const queryClient = useQueryClient(); + return useMutation<{}, {}, TUpdateSecretsV3DTO>({ + mutationFn: async ({ + secretPath = "/", + type, + environment, + workspaceId, + secretName, + secretValue, + latestFileKey + }) => { + const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; + + const randomBytes = latestFileKey + ? decryptAssymmetric({ + ciphertext: latestFileKey.encryptedKey, + nonce: latestFileKey.nonce, + publicKey: latestFileKey.sender.publicKey, + privateKey: PRIVATE_KEY + }) + : crypto.randomBytes(16).toString("hex"); + const { secretValueIV, secretValueTag, secretValueCiphertext } = encryptSecret( + randomBytes, + secretName, + secretValue, + "" + ); + + const reqBody = { + workspaceId, + environment, + type, + secretPath, + secretValueIV, + secretValueTag, + secretValueCiphertext + }; + const { data } = await apiRequest.patch(`/api/v3/secrets/${secretName}`, reqBody); + return data; + }, + onSuccess: (_, { workspaceId, environment, secretPath }) => { + queryClient.invalidateQueries( + secretKeys.getProjectSecret(workspaceId, environment, secretPath) + ); + } + }); +}; + +export const useDeleteSecretV3 = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TDeleteSecretsV3DTO>({ + mutationFn: async ({ secretPath = "/", type, environment, workspaceId, secretName }) => { + const reqBody = { + workspaceId, + environment, + type, + secretPath + }; + + const { data } = await apiRequest.delete(`/api/v3/secrets/${secretName}`, { + data: reqBody + }); + return data; + }, + onSuccess: (_, { workspaceId, environment, secretPath }) => { + queryClient.invalidateQueries( + secretKeys.getProjectSecret(workspaceId, environment, secretPath) + ); + } + }); +}; diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index 3d0c81643..34ecd7ec4 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -1,5 +1,5 @@ /* eslint-disable no-param-reassign */ -import { useCallback } from "react"; +import { useCallback, useMemo } from "react"; import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; import { @@ -38,40 +38,15 @@ const fetchProjectEncryptedSecrets = async ( folderId?: string, secretPath?: string ) => { - if (typeof env === "string") { - const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", { - params: { - environment: env, - workspaceId, - folderId: folderId || undefined, - secretPath - } - }); - return data.secrets; - } - - if (typeof env === "object") { - let allEnvData: any = []; - - // eslint-disable-next-line no-restricted-syntax - for (const envPoint of env) { - // eslint-disable-next-line no-await-in-loop - const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", { - params: { - environment: envPoint, - workspaceId, - folderId, - secretPath - } - }); - allEnvData = allEnvData.concat(data.secrets); + const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", { + params: { + environment: env, + workspaceId, + folderId: folderId || undefined, + secretPath } - - return allEnvData; - // eslint-disable-next-line no-else-return - } else { - return null; - } + }); + return data.secrets; }; export const useGetProjectSecrets = ({ @@ -167,31 +142,13 @@ export const useGetProjectSecretsAllEnv = ({ decryptFileKey, folderId, secretPath -}: TGetProjectSecretsAllEnvDTO) => - useQueries({ +}: TGetProjectSecretsAllEnvDTO) => { + const secrets = useQueries({ queries: envs.map((env) => ({ queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath || folderId), enabled: Boolean(decryptFileKey && workspaceId && env), - queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath) - })) - }); - -export const useGetProjectSecretsByKey = ({ - workspaceId, - env, - decryptFileKey, - isPaused, - folderId, - secretPath -}: GetProjectSecretsDTO) => - useQuery({ - // wait for all values to be available - enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused, - // right now secretpath is passed as folderid as only this is used in overview - queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath), - queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath), - select: useCallback( - (data: EncryptedSecret[]) => { + queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath), + select: (data: EncryptedSecret[]) => { const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; const latestKey = decryptFileKey; const key = decryptAssymmetric({ @@ -201,12 +158,11 @@ export const useGetProjectSecretsByKey = ({ privateKey: PRIVATE_KEY }); - const sharedSecrets: Record = {}; + const sharedSecrets: Record = {}; const personalSecrets: Record = {}; // this used for add-only mode in dashboard // type won't be there thus only one key is shown const duplicateSecretKey: Record = {}; - const uniqSecKeys: Record = {}; data.forEach((encSecret: EncryptedSecret) => { const secretKey = decryptSymmetric({ ciphertext: encSecret.secretKeyCiphertext, @@ -214,7 +170,6 @@ export const useGetProjectSecretsByKey = ({ tag: encSecret.secretKeyTag, key }); - if (!uniqSecKeys?.[secretKey]) uniqSecKeys[secretKey] = true; const secretValue = decryptSymmetric({ ciphertext: encSecret.secretValueCiphertext, @@ -242,35 +197,65 @@ export const useGetProjectSecretsByKey = ({ }; if (encSecret.type === "personal") { - personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = { + personalSecrets[decryptedSecret.key] = { id: encSecret._id, value: secretValue }; } else { - if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) { - if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = []; - sharedSecrets[secretKey].push(decryptedSecret); + if (!duplicateSecretKey?.[decryptedSecret.key]) { + sharedSecrets[decryptedSecret.key] = decryptedSecret; } - duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true; + duplicateSecretKey[decryptedSecret.key] = true; } }); - Object.keys(sharedSecrets).forEach((secName) => { - sharedSecrets[secName].forEach((val) => { - const dupKey = `${val.key}-${val.env}`; - if (personalSecrets?.[dupKey]) { - val.idOverride = personalSecrets[dupKey].id; - val.valueOverride = personalSecrets[dupKey].value; - val.overrideAction = "modified"; - } - }); - }); - return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length }; - }, - [decryptFileKey] - ) + Object.keys(sharedSecrets).forEach((val) => { + if (personalSecrets?.[val]) { + sharedSecrets[val].idOverride = personalSecrets[val].id; + sharedSecrets[val].valueOverride = personalSecrets[val].value; + sharedSecrets[val].overrideAction = "modified"; + } + }); + return sharedSecrets; + } + })) }); + const secKeys = useMemo(() => { + const keys = new Set(); + secrets?.forEach(({ data }) => { + // TODO(akhilmhdh): find out why this is unknown + Object.keys(data || {}).forEach((key) => keys.add(key)); + }); + return [...keys]; + }, [(secrets || []).map((sec) => sec.data)]); + + const getEnvSecretKeyCount = useCallback( + (env: string) => { + const selectedEnvIndex = envs.indexOf(env); + if (selectedEnvIndex !== -1) { + return Object.keys(secrets[selectedEnvIndex]?.data || {}).length; + } + return 0; + }, + [(secrets || []).map((sec) => sec.data)] + ); + + const getSecretByKey = useCallback( + (env: string, key: string) => { + const selectedEnvIndex = envs.indexOf(env); + if (selectedEnvIndex !== -1) { + const sec = secrets[selectedEnvIndex]?.data?.[key]; + return sec; + } + return undefined; + }, + [(secrets || []).map((sec) => sec.data)] + ); + + return { data: secrets, secKeys, getSecretByKey, getEnvSecretKeyCount }; +}; + const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => { const { data } = await apiRequest.get<{ secretVersions: EncryptedSecretVersion[] }>( `/api/v1/secret/${secretId}/secret-versions`, diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index fb995ce80..9e3092257 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -118,3 +118,32 @@ export type GetSecretVersionsDTO = { offset: number; decryptFileKey: UserWsKeyPair; }; + +export type TCreateSecretsV3DTO = { + latestFileKey: UserWsKeyPair; + secretName: string; + secretValue: string; + secretComment: string; + secretPath: string; + workspaceId: string; + environment: string; + type: string; +}; + +export type TUpdateSecretsV3DTO = { + latestFileKey: UserWsKeyPair; + workspaceId: string; + environment: string; + type: string; + secretPath: string; + secretName: string; + secretValue: string; +}; + +export type TDeleteSecretsV3DTO = { + workspaceId: string; + environment: string; + type: string; + secretPath: string; + secretName: string; +}; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 877db215d..fea50dd8a 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -13,7 +13,18 @@ import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/router"; import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; -import { faAngleDown, faArrowLeft, faArrowUpRightFromSquare, faBook, faCheck, faEnvelope, faInfinity, faMobile, faPlus, faQuestion } from "@fortawesome/free-solid-svg-icons"; +import { + faAngleDown, + faArrowLeft, + faArrowUpRightFromSquare, + faBook, + faCheck, + faEnvelope, + faInfinity, + faMobile, + faPlus, + faQuestion +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { yupResolver } from "@hookform/resolvers/yup"; import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; @@ -41,7 +52,14 @@ import { } from "@app/components/v2"; import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context"; import { usePopUp } from "@app/hooks"; -import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useGetOrgTrialUrl, useLogoutUser, useUploadWsKey } from "@app/hooks/api"; +import { + fetchOrgUsers, + useAddUserToWs, + useCreateWorkspace, + useGetOrgTrialUrl, + useLogoutUser, + useUploadWsKey +} from "@app/hooks/api"; interface LayoutProps { children: React.ReactNode; @@ -89,7 +107,9 @@ export const AppLayout = ({ children }: LayoutProps) => { const { subscription } = useSubscription(); // const [ isLearningNoteOpen, setIsLearningNoteOpen ] = useState(true); - const isAddingProjectsAllowed = subscription?.workspaceLimit ? (subscription.workspacesUsed < subscription.workspaceLimit) : true; + const isAddingProjectsAllowed = subscription?.workspaceLimit + ? subscription.workspacesUsed < subscription.workspaceLimit + : true; const createWs = useCreateWorkspace(); const uploadWsKey = useUploadWsKey(); @@ -110,22 +130,22 @@ export const AppLayout = ({ children }: LayoutProps) => { const { t } = useTranslation(); - useEffect(() => { - const handleRouteChange = () => { - (window).Intercom("update"); - }; - - router.events.on("routeChangeComplete", handleRouteChange); - - return () => { - router.events.off("routeChangeComplete", handleRouteChange); - }; - }, []); + useEffect(() => { + const handleRouteChange = () => { + window.Intercom("update"); + }; + + router.events.on("routeChangeComplete", handleRouteChange); + + return () => { + router.events.off("routeChangeComplete", handleRouteChange); + }; + }, []); const logout = useLogoutUser(); const logOutUser = async () => { try { - console.log("Logging out...") + console.log("Logging out..."); await logout.mutateAsync(); localStorage.removeItem("protectedKey"); localStorage.removeItem("protectedKeyIV"); @@ -145,27 +165,30 @@ export const AppLayout = ({ children }: LayoutProps) => { const changeOrg = async (orgId) => { localStorage.setItem("orgData.id", orgId); - router.push(`/org/${orgId}/overview`) - } + router.push(`/org/${orgId}/overview`); + }; // TODO(akhilmhdh): This entire logic will be rechecked and will try to avoid // Placing the localstorage as much as possible // Wait till tony integrates the azure and its launched useEffect(() => { - // Put a user in an org if they're not in one yet const putUserInOrg = async () => { if (tempLocalStorage("orgData.id") === "") { localStorage.setItem("orgData.id", orgs[0]?._id); } - if (currentOrg && ( - (workspaces?.length === 0 && router.asPath.includes("project")) - || router.asPath.includes("/project/undefined") - || (!orgs?.map(org => org._id)?.includes(router.query.id) && !router.asPath.includes("project") && !router.asPath.includes("personal") && !router.asPath.includes("integration")) - )) { + if ( + currentOrg && + ((workspaces?.length === 0 && router.asPath.includes("project")) || + router.asPath.includes("/project/undefined") || + (!orgs?.map((org) => org._id)?.includes(router.query.id) && + !router.asPath.includes("project") && + !router.asPath.includes("personal") && + !router.asPath.includes("integration"))) + ) { router.push(`/org/${currentOrg?._id}/overview`); - } + } // else if (!router.asPath.includes("org") && !router.asPath.includes("project") && !router.asPath.includes("integrations") && !router.asPath.includes("personal-settings")) { // const pathSegments = router.asPath.split("/").filter((segment) => segment.length > 0); @@ -233,7 +256,7 @@ export const AppLayout = ({ children }: LayoutProps) => { } createNotification({ text: "Workspace created", type: "success" }); handlePopUpClose("addNewWs"); - router.push(`/project/${newWorkspaceId}/secrets`); + router.push(`/project/${newWorkspaceId}/secrets/overview`); } catch (err) { console.error(err); createNotification({ text: "Failed to create workspace", type: "error" }); @@ -244,190 +267,235 @@ export const AppLayout = ({ children }: LayoutProps) => { <>
-