diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index c466be087..135effc4c 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -904,7 +904,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { projectId, path: secretPath, search, - tagSlugs: tags + tagSlugs: tags, + includeTagsInSearch: true, + includeMetadataInSearch: true }); if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { @@ -924,7 +926,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { search, limit: remainingLimit, offset: adjustedOffset, - tagSlugs: tags + tagSlugs: tags, + includeTagsInSearch: true, + includeMetadataInSearch: true }) ).secrets; } @@ -1097,7 +1101,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { filters: { ...sharedFilters, tagSlugs: tags, - includeTagsInSearch: true + includeTagsInSearch: true, + includeMetadataInSearch: true } }, req.permission diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index cd2773172..1047a09cb 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -415,6 +415,8 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { filters?: { search?: string; tagSlugs?: string[]; + includeTagsInSearch?: boolean; + includeMetadataInSearch?: boolean; } ) => { try { @@ -433,17 +435,27 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { .whereIn("folderId", folderIds) .where((bd) => { if (filters?.search) { - void bd.whereILike("key", `%${filters?.search}%`); + void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`); + if (filters?.includeTagsInSearch) { + void bd.orWhereILike(`${TableName.SecretTag}.slug`, `%${filters?.search}%`); + } + if (filters?.includeMetadataInSearch) { + void bd + .orWhereILike(`${TableName.ResourceMetadata}.key`, `%${filters?.search}%`) + .orWhereILike(`${TableName.ResourceMetadata}.value`, `%${filters?.search}%`); + } } }) .where((bd) => { - void bd.whereNull("userId").orWhere({ userId: userId || null }); + void bd + .whereNull(`${TableName.SecretV2}.userId`) + .orWhere({ [`${TableName.SecretV2}.userId` as "userId"]: userId || null }); }) - .countDistinct("key"); + .countDistinct(`${TableName.SecretV2}.key`); // only need to join tags if filtering by tag slugs const slugs = filters?.tagSlugs?.filter(Boolean); - if (slugs && slugs.length > 0) { + if ((slugs && slugs.length > 0) || filters?.includeTagsInSearch) { void query .leftJoin( TableName.SecretV2JnTag, @@ -454,12 +466,24 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { TableName.SecretTag, `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id` - ) - .whereIn("slug", slugs); + ); + + if (slugs?.length) { + void query.whereIn("slug", slugs); + } + } + + if (filters?.includeMetadataInSearch) { + void query.leftJoin( + TableName.ResourceMetadata, + `${TableName.SecretV2}.id`, + `${TableName.ResourceMetadata}.secretId` + ); } const secrets = await query; + // @ts-expect-error not inferred by knex return Number(secrets[0]?.count ?? 0); } catch (error) { throw new DatabaseError({ error, name: "get folder secret count" }); @@ -485,12 +509,14 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { .whereIn(`${TableName.SecretV2}.folderId`, folderIds) .where((bd) => { if (filters?.search) { + void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`); if (filters?.includeTagsInSearch) { + void bd.orWhereILike(`${TableName.SecretTag}.slug`, `%${filters?.search}%`); + } + if (filters?.includeMetadataInSearch) { void bd - .whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`) - .orWhereILike(`${TableName.SecretTag}.slug`, `%${filters?.search}%`); - } else { - void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`); + .orWhereILike(`${TableName.ResourceMetadata}.key`, `%${filters?.search}%`) + .orWhereILike(`${TableName.ResourceMetadata}.value`, `%${filters?.search}%`); } } diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index 825e7c805..48b2e0ef6 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -358,6 +358,7 @@ export type TFindSecretsByFolderIdsFilter = { tagSlugs?: string[]; metadataFilter?: { key?: string; value?: string }[]; includeTagsInSearch?: boolean; + includeMetadataInSearch?: boolean; keys?: string[]; }; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 02b52473f..10527938f 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -1137,6 +1137,8 @@ export const secretServiceFactory = ({ | "environment" | "tagSlugs" | "search" + | "includeTagsInSearch" + | "includeMetadataInSearch" >) => { const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 2d52344e5..6a2a2b7e3 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -212,6 +212,8 @@ export type TGetSecretsRawDTO = { limit?: number; search?: string; keys?: string[]; + includeTagsInSearch?: boolean; + includeMetadataInSearch?: boolean; } & TProjectPermission; export type TGetSecretAccessListDTO = { diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/SecretSearchInput.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/SecretSearchInput.tsx index 9c8562e06..9a69bfb9c 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/SecretSearchInput.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/SecretSearchInput.tsx @@ -53,7 +53,7 @@ export const SecretSearchInput = ({ }} autoComplete="off" className="input text-md h-[2.3rem] w-full rounded-md rounded-l-none bg-mineshaft-800 py-[0.375rem] pl-2.5 pr-8 text-gray-400 placeholder-mineshaft-50 placeholder-opacity-50 outline-none duration-200 placeholder:text-sm hover:ring-bunker-400/60 focus:bg-mineshaft-700/80 focus:ring-1 focus:ring-primary-400/50" - placeholder="Search by secret/folder name..." + placeholder="Search by secret, folder, tag or metadata..." value={value} onChange={(e) => onChange(e.target.value)} /> diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx index 80b53d426..a9d592b1c 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx @@ -1,6 +1,7 @@ import { faCheck, faChevronRight, + faCode, faCopy, faEye, faFolder, @@ -82,6 +83,17 @@ export const QuickSearchSecretItem = ({ search.trim() && secretGroupTags?.find((tag) => tag && tag.slug.toLowerCase().includes(search.toLowerCase())); + const secretGroupMetadata = secretGroup.flatMap((secret) => secret.secretMetadata); + + const metadataMatch = + search.trim() && + secretGroupMetadata?.find( + (metadata) => + metadata && + (metadata.key.toLowerCase().includes(search.toLowerCase()) || + metadata.value.toLowerCase().includes(search.toLowerCase())) + ); + return ( )} + {metadataMatch && !tagMatch && ( + + +

Metadata Match

+
+ )} {isSingleEnv ? (