diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index baa1484b1..c925d2587 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -232,6 +232,7 @@ export const secretFolderServiceFactory = ({ if (!parentFolder) return []; const folders = await folderDAL.find({ envId: env.id, parentId: parentFolder.id }); + return folders; }; diff --git a/frontend/src/components/v2/SecretInput/SecretInput.tsx b/frontend/src/components/v2/SecretInput/SecretInput.tsx index 2dfafca74..7fe1c4829 100644 --- a/frontend/src/components/v2/SecretInput/SecretInput.tsx +++ b/frontend/src/components/v2/SecretInput/SecretInput.tsx @@ -14,7 +14,8 @@ const replaceContentWithDot = (str: string) => { return finalStr; }; -const syntaxHighlight = (content?: string | null, isVisible?: boolean) => { +const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?: boolean) => { + if (isImport) return "IMPORTED"; if (content === "") return "EMPTY"; if (!content) return "EMPTY"; if (!isVisible) return replaceContentWithDot(content); @@ -46,6 +47,7 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean) => { type Props = TextareaHTMLAttributes & { value?: string | null; isVisible?: boolean; + isImport?: boolean; isReadOnly?: boolean; isDisabled?: boolean; containerClassName?: string; @@ -55,7 +57,17 @@ const commonClassName = "font-mono text-sm caret-white border-none outline-none export const SecretInput = forwardRef( ( - { value, isVisible, containerClassName, onBlur, isDisabled, isReadOnly, onFocus, ...props }, + { + value, + isVisible, + isImport, + containerClassName, + onBlur, + isDisabled, + isReadOnly, + onFocus, + ...props + }, ref ) => { const [isSecretFocused, setIsSecretFocused] = useToggle(); @@ -69,7 +81,7 @@ export const SecretInput = forwardRef(
             
               
-                {syntaxHighlight(value, isVisible || isSecretFocused)}
+                {syntaxHighlight(value, isVisible || isSecretFocused, isImport)}
               
             
           
diff --git a/frontend/src/components/v2/Tooltip/Tooltip.tsx b/frontend/src/components/v2/Tooltip/Tooltip.tsx index 9d1285aa0..93df61e61 100644 --- a/frontend/src/components/v2/Tooltip/Tooltip.tsx +++ b/frontend/src/components/v2/Tooltip/Tooltip.tsx @@ -11,6 +11,8 @@ export type TooltipProps = Omit void; defaultOpen?: boolean; position?: "top" | "bottom" | "left" | "right"; + isDisabled?: boolean; + center?: boolean; }; export const Tooltip = ({ @@ -20,7 +22,9 @@ export const Tooltip = ({ onOpenChange, defaultOpen, className, + center, asChild = true, + isDisabled, position = "top", ...props }: TooltipProps) => ( @@ -38,11 +42,13 @@ export const Tooltip = ({ {...props} className={twMerge( `z-50 max-w-[15rem] select-none rounded-md border border-mineshaft-600 bg-mineshaft-800 py-2 px-4 text-sm font-light text-bunker-200 shadow-md -data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade -data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade -data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade -data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade -`, + data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade + data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade + data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade + data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade + `, + isDisabled && "!hidden", + center && "text-center", className )} > diff --git a/frontend/src/hooks/api/secretImports/index.ts b/frontend/src/hooks/api/secretImports/index.ts index 2e1150cb8..f30506b6b 100644 --- a/frontend/src/hooks/api/secretImports/index.ts +++ b/frontend/src/hooks/api/secretImports/index.ts @@ -1,2 +1,7 @@ export { useCreateSecretImport, useDeleteSecretImport, useUpdateSecretImport } from "./mutation"; -export { useGetImportedSecrets, useGetSecretImports } from "./queries"; +export { + useGetImportedFoldersByEnv, + useGetImportedSecretsAllEnvs, + useGetImportedSecretsSingleEnv, + useGetSecretImports +} from "./queries"; diff --git a/frontend/src/hooks/api/secretImports/queries.tsx b/frontend/src/hooks/api/secretImports/queries.tsx index 1af6ca878..2879c859b 100644 --- a/frontend/src/hooks/api/secretImports/queries.tsx +++ b/frontend/src/hooks/api/secretImports/queries.tsx @@ -1,5 +1,5 @@ import { useCallback } from "react"; -import { useQuery, UseQueryOptions } from "@tanstack/react-query"; +import { useQueries, useQuery, UseQueryOptions } from "@tanstack/react-query"; import { decryptAssymmetric, @@ -7,7 +7,15 @@ import { } from "@app/components/utilities/cryptography/crypto"; import { apiRequest } from "@app/config/request"; -import { TGetImportedSecrets, TGetSecretImports, TImportedSecrets, TSecretImport } from "./types"; +import { + TGetImportedFoldersByEnvDTO, + TGetImportedSecrets, + TGetSecretImports, + TGetSecretImportsAllEnvs, + TImportedSecrets, + TSecretImport, + TuseGetImportedFoldersByEnv +} from "./types"; export const secretImportKeys = { getProjectSecretImports: ({ environment, projectId, path }: TGetSecretImports) => @@ -17,7 +25,11 @@ export const secretImportKeys = { projectId, path }: Omit) => - [{ environment, path, projectId }, "secrets-import-sec"] as const + [{ environment, path, projectId }, "secrets-import-sec"] as const, + getImportedFoldersByEnv: ({ environment, projectId, path }: TGetImportedFoldersByEnvDTO) => + [{ environment, projectId, path }, "imported-folders"] as const, + getImportedFoldersAllEnvs: ({ projectId, path, environment }: TGetImportedFoldersByEnvDTO) => + [{ projectId, path, environment }, "imported-folders-all-envs"] as const }; const fetchSecretImport = async ({ projectId, environment, path = "/" }: TGetSecretImports) => { @@ -75,7 +87,25 @@ const fetchImportedSecrets = async ( return data.secrets; }; -export const useGetImportedSecrets = ({ +const fetchImportedFolders = async ({ + projectId, + environment, + path +}: TGetImportedFoldersByEnvDTO) => { + const { data } = await apiRequest.get<{ secretImports: TSecretImport[] }>( + "/api/v1/secret-imports", + { + params: { + workspaceId: projectId, + environment, + path + } + } + ); + return data.secretImports; +}; + +export const useGetImportedSecretsSingleEnv = ({ environment, decryptFileKey, path, @@ -159,3 +189,138 @@ export const useGetImportedSecrets = ({ [decryptFileKey] ) }); + +export const useGetImportedSecretsAllEnvs = ({ + projectId, + environments, + path = "/", + decryptFileKey +}: TGetSecretImportsAllEnvs) => { + const secretImports = useQueries({ + queries: environments.map((env) => ({ + queryKey: secretImportKeys.getImportedFoldersAllEnvs({ + environment: env, + projectId, + path + }), + queryFn: () => fetchImportedSecrets(projectId, env, path).catch(() => []), + enabled: Boolean(projectId) && Boolean(env), + // eslint-disable-next-line react-hooks/rules-of-hooks + select: useCallback( + (data: TImportedSecrets[]) => { + const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; + const latestKey = decryptFileKey; + const key = decryptAssymmetric({ + ciphertext: latestKey.encryptedKey, + nonce: latestKey.nonce, + publicKey: latestKey.sender.publicKey, + privateKey: PRIVATE_KEY + }); + + return data.map((el) => ({ + environment: el.environment, + secretPath: el.secretPath, + environmentInfo: el.environmentInfo, + folderId: el.folderId, + secrets: el.secrets.map((encSecret) => { + const secretKey = decryptSymmetric({ + ciphertext: encSecret.secretKeyCiphertext, + iv: encSecret.secretKeyIV, + tag: encSecret.secretKeyTag, + key + }); + + const secretValue = decryptSymmetric({ + ciphertext: encSecret.secretValueCiphertext, + iv: encSecret.secretValueIV, + tag: encSecret.secretValueTag, + key + }); + + const secretComment = decryptSymmetric({ + ciphertext: encSecret.secretCommentCiphertext, + iv: encSecret.secretCommentIV, + tag: encSecret.secretCommentTag, + key + }); + + return { + id: encSecret.id, + env: encSecret.environment, + key: secretKey, + value: secretValue, + tags: encSecret.tags, + comment: secretComment, + createdAt: encSecret.createdAt, + updatedAt: encSecret.updatedAt, + version: encSecret.version + }; + }) + })); + }, + [decryptFileKey] + ) + })) + }); + + const isImportedSecretPresentInEnv = useCallback( + (secPath: string, envSlug: string, secretName: string) => { + const selectedEnvIndex = environments.indexOf(envSlug); + + if (selectedEnvIndex !== -1) { + const isPresent = secretImports?.[selectedEnvIndex]?.data?.find( + ({ secretPath, secrets }) => + secretPath === secPath && secrets.some((s) => s.key === secretName) + ); + + return Boolean(isPresent); + } + return false; + }, + [(secretImports || []).map((response) => response.data)] + ); + + return { secretImports, isImportedSecretPresentInEnv }; +}; + +export const useGetImportedFoldersByEnv = ({ + projectId, + environments, + path = "/" +}: TuseGetImportedFoldersByEnv) => { + const queryParams = new URLSearchParams(window.location.search); + + const currentPath = path; + + const importedFolders = useQueries({ + queries: environments.map((env) => ({ + queryKey: secretImportKeys.getImportedFoldersByEnv({ + projectId, + environment: env, + path: currentPath + }), + queryFn: async () => fetchImportedFolders({ projectId, environment: env, path: currentPath }), + enabled: Boolean(projectId) && Boolean(env) + })) + }); + + const isImportedFolderPresentInEnv = useCallback( + (name: string, env: string) => { + const selectedEnvIndex = environments.indexOf(env); + + if (selectedEnvIndex !== -1) { + const currentlyBrowsingPath = queryParams.get("secretPath") || ""; + + const isPresent = importedFolders?.[selectedEnvIndex]?.data?.find( + ({ importPath }) => importPath === `${currentlyBrowsingPath}/${name}` + ); + + return Boolean(isPresent); + } + return false; + }, + [(importedFolders || []).map((response) => response.data)] + ); + + return { importedFolders, isImportedFolderPresentInEnv }; +}; diff --git a/frontend/src/hooks/api/secretImports/types.ts b/frontend/src/hooks/api/secretImports/types.ts index eb63e8a96..950fc20c4 100644 --- a/frontend/src/hooks/api/secretImports/types.ts +++ b/frontend/src/hooks/api/secretImports/types.ts @@ -12,6 +12,12 @@ export type TSecretImport = { updatedAt: string; }; +export type TGetImportedFoldersByEnvDTO = { + projectId: string; + environment: string; + path?: string; +}; + export type TImportedSecrets = { environment: string; environmentInfo: WorkspaceEnv; @@ -26,6 +32,13 @@ export type TGetSecretImports = { path?: string; }; +export type TGetSecretImportsAllEnvs = { + projectId: string; + decryptFileKey: UserWsKeyPair; + path?: string; + environments: string[]; +}; + export type TGetImportedSecrets = { projectId: string; environment: string; @@ -33,6 +46,12 @@ export type TGetImportedSecrets = { decryptFileKey: UserWsKeyPair; }; +export type TuseGetImportedFoldersByEnv = { + environments: string[]; + projectId: string; + path?: string; +}; + export type TCreateSecretImportDTO = { projectId: string; environment: string; diff --git a/frontend/src/views/SecretMainPage/SecretMainPage.tsx b/frontend/src/views/SecretMainPage/SecretMainPage.tsx index 312b676b7..a06ceda9d 100644 --- a/frontend/src/views/SecretMainPage/SecretMainPage.tsx +++ b/frontend/src/views/SecretMainPage/SecretMainPage.tsx @@ -17,7 +17,7 @@ import { } from "@app/context"; import { usePopUp } from "@app/hooks"; import { - useGetImportedSecrets, + useGetImportedSecretsSingleEnv, useGetProjectFolders, useGetProjectSecrets, useGetSecretApprovalPolicyOfABoard, @@ -124,7 +124,7 @@ export const SecretMainPage = () => { }); // fetch imported secrets to show user the overriden ones - const { data: importedSecrets } = useGetImportedSecrets({ + const { data: importedSecrets } = useGetImportedSecretsSingleEnv({ projectId: workspaceId, environment, decryptFileKey: decryptFileKey!, diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx index ec8b5c2f9..4b78411a9 100644 --- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx +++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx @@ -55,6 +55,7 @@ import { useCreateSecretV3, useDeleteSecretV3, useGetFoldersByEnv, + useGetImportedSecretsAllEnvs, useGetProjectSecretsAllEnv, useGetUserWsKey, useUpdateSecretV3 @@ -131,6 +132,12 @@ export const SecretOverviewPage = () => { environments: userAvailableEnvs.map(({ slug }) => slug) }); + const { isImportedSecretPresentInEnv } = useGetImportedSecretsAllEnvs({ + projectId: workspaceId, + decryptFileKey: latestFileKey!, + environments: userAvailableEnvs.map(({ slug }) => slug) + }); + const { mutateAsync: createSecretV3 } = useCreateSecretV3(); const { mutateAsync: updateSecretV3 } = useUpdateSecretV3(); const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3(); @@ -649,6 +656,7 @@ export const SecretOverviewPage = () => { filteredSecretNames.map((key, index) => ( { return ( @@ -29,6 +30,7 @@ export const SecretOverviewFolderRow = ({ {environments.map(({ slug }, i) => { const isPresent = isFolderPresentInEnv(folderName, slug); + return (
- +
); diff --git a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx index 9653a9bd0..628e3d008 100644 --- a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx @@ -2,6 +2,7 @@ import { Controller, useForm } from "react-hook-form"; import { subject } from "@casl/ability"; import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { ProjectPermissionCan } from "@app/components/permissions"; @@ -15,6 +16,7 @@ type Props = { secretId?: string; isCreatable?: boolean; isVisible?: boolean; + isImportedSecret: boolean; environment: string; secretPath: string; onSecretCreate: (env: string, key: string, value: string) => Promise; @@ -25,6 +27,7 @@ type Props = { export const SecretEditRow = ({ defaultValue, isCreatable, + isImportedSecret, onSecretUpdate, secretName, onSecretCreate, @@ -90,14 +93,25 @@ export const SecretEditRow = ({
( - + )} />
-
+
{isDirty ? ( <> Promise; onSecretUpdate: (env: string, key: string, value: string, secretId?: string) => Promise; onSecretDelete: (env: string, key: string, secretId?: string) => Promise; + isImportedSecretPresentInEnv: (name: string, env: string, secretName: string) => boolean; }; export const SecretOverviewTableRow = ({ @@ -36,6 +38,7 @@ export const SecretOverviewTableRow = ({ onSecretUpdate, onSecretCreate, onSecretDelete, + isImportedSecretPresentInEnv, expandableColWidth }: Props) => { const [isFormExpanded, setIsFormExpanded] = useToggle(); @@ -61,6 +64,9 @@ export const SecretOverviewTableRow = ({ {environments.map(({ slug }, i) => { const secret = getSecretByKey(slug, secretKey); + + const isSecretImported = isImportedSecretPresentInEnv(secretPath, slug, secretKey); + const isSecretPresent = Boolean(secret); const isSecretEmpty = secret?.value === ""; return ( @@ -69,16 +75,29 @@ export const SecretOverviewTableRow = ({ className={twMerge( "py-0 px-0 group-hover:bg-mineshaft-700", isFormExpanded && "border-t-2 border-mineshaft-500", - isSecretPresent && !isSecretEmpty ? "text-green-600" : "", - isSecretPresent && isSecretEmpty ? "text-yellow" : "", - !isSecretPresent && !isSecretEmpty ? "text-red-600" : "" + (isSecretPresent && !isSecretEmpty) || isSecretImported ? "text-green-600" : "", + isSecretPresent && isSecretEmpty && !isSecretImported ? "text-yellow" : "", + !isSecretPresent && !isSecretEmpty && !isSecretImported ? "text-red-600" : "" )} >
{!isSecretEmpty && ( - - + + )} {isSecretEmpty && ( @@ -143,6 +162,12 @@ export const SecretOverviewTableRow = ({ const secret = getSecretByKey(slug, secretKey); const isCreatable = !secret; + const isImportedSecret = isImportedSecretPresentInEnv( + secretPath, + slug, + secretKey + ); + return (