From 27f32aa2bcab235ec10b275195f750ba25e5b052 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Thu, 16 Oct 2025 23:59:49 +0530 Subject: [PATCH] fix: adds forceDelete flag to folder deletion V2 endpoint --- backend/src/lib/api-docs/constants.ts | 3 +- backend/src/server/routes/index.ts | 3 +- .../server/routes/v2/secret-folder-router.ts | 6 ++-- .../secret-folder/secret-folder-service.ts | 29 +++++++++++++++++-- .../secret-folder/secret-folder-types.ts | 1 + .../src/hooks/api/secretFolders/queries.tsx | 5 ++-- frontend/src/hooks/api/secretFolders/types.ts | 1 + 7 files changed, 39 insertions(+), 9 deletions(-) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 1799555fb..a2294a940 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -949,7 +949,8 @@ export const FOLDERS = { projectId: "The ID of the project to delete the folder from.", environment: "The slug of the environment where the folder is located.", directory: "The directory of the folder to delete. (Deprecated in favor of path)", - path: "The path of the folder to delete." + path: "The path of the folder to delete.", + forceDelete: "Whether to force delete the folder even if it contains resources." } } as const; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index e95dd501c..2a3a4cf8d 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1344,7 +1344,8 @@ export const registerRoutes = async ( projectDAL, folderCommitService, secretApprovalPolicyService, - secretV2BridgeDAL + secretV2BridgeDAL, + dynamicSecretDAL }); const secretImportService = secretImportServiceFactory({ diff --git a/backend/src/server/routes/v2/secret-folder-router.ts b/backend/src/server/routes/v2/secret-folder-router.ts index 0bf062452..251ad8e85 100644 --- a/backend/src/server/routes/v2/secret-folder-router.ts +++ b/backend/src/server/routes/v2/secret-folder-router.ts @@ -263,7 +263,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .default("/") .transform(prefixWithSlash) // Transformations get skipped if path is undefined .transform(removeTrailingSlash) - .describe(FOLDERS.DELETE.path) + .describe(FOLDERS.DELETE.path), + forceDelete: z.boolean().optional().default(false).describe(FOLDERS.DELETE.forceDelete) }), response: { 200: z.object({ @@ -279,7 +280,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, - idOrName: req.params.folderIdOrName + idOrName: req.params.folderIdOrName, + forceDelete: req.body.forceDelete }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index 8e0892bd1..ba60b6d0d 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -5,6 +5,7 @@ import path from "path"; import { v4 as uuidv4, validate as uuidValidate } from "uuid"; import { ActionProjectType, TProjectEnvironments, TSecretFolders, TSecretFoldersInsert } from "@app/db/schemas"; +import { TDynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; @@ -47,7 +48,11 @@ type TSecretFolderServiceFactoryDep = { folderCommitService: Pick; projectDAL: Pick; secretApprovalPolicyService: Pick; - secretV2BridgeDAL: Pick; + secretV2BridgeDAL: Pick< + TSecretV2BridgeDALFactory, + "findByFolderIds" | "invalidateSecretCacheByProjectId" | "findOne" + >; + dynamicSecretDAL: Pick; }; export type TSecretFolderServiceFactory = ReturnType; @@ -61,7 +66,8 @@ export const secretFolderServiceFactory = ({ folderCommitService, projectDAL, secretApprovalPolicyService, - secretV2BridgeDAL + secretV2BridgeDAL, + dynamicSecretDAL }: TSecretFolderServiceFactoryDep) => { const createFolder = async ({ projectId, @@ -638,7 +644,8 @@ export const secretFolderServiceFactory = ({ actorAuthMethod, environment, path: secretPath, - idOrName + idOrName, + forceDelete = false }: TDeleteFolderDTO) => { const { permission } = await permissionService.getProjectPermission({ actor, @@ -690,6 +697,22 @@ export const secretFolderServiceFactory = ({ throw new NotFoundError({ message: `Folder with ID '${idOrName}' not found` }); } + // Check if folder contains resources (secrets, dynamic secrets, subfolders) + if (!forceDelete) { + const error = new BadRequestError({ + message: `Cannot delete folder "${folderToDelete.name}" because it contains resources. Use forceDelete=true to delete it forcefully.`, + name: "deleteFolder" + }); + const secretV2 = await secretV2BridgeDAL.findOne({ folderId: folderToDelete.id }); + if (secretV2) throw error; + + const dynamicSecret = await dynamicSecretDAL.findOne({ folderId: folderToDelete.id }); + if (dynamicSecret) throw error; + + const subfolder = await folderDAL.findByParentId(folderToDelete.id); + if (subfolder) throw error; + } + const [doc] = await folderDAL.delete( { envId: env.id, diff --git a/backend/src/services/secret-folder/secret-folder-types.ts b/backend/src/services/secret-folder/secret-folder-types.ts index eed815da5..da8be52a0 100644 --- a/backend/src/services/secret-folder/secret-folder-types.ts +++ b/backend/src/services/secret-folder/secret-folder-types.ts @@ -37,6 +37,7 @@ export type TDeleteFolderDTO = { environment: string; path: string; idOrName: string; + forceDelete?: boolean; } & TProjectPermission; export type TGetFolderDTO = { diff --git a/frontend/src/hooks/api/secretFolders/queries.tsx b/frontend/src/hooks/api/secretFolders/queries.tsx index 3e752eed3..694a33d13 100644 --- a/frontend/src/hooks/api/secretFolders/queries.tsx +++ b/frontend/src/hooks/api/secretFolders/queries.tsx @@ -218,12 +218,13 @@ export const useDeleteFolder = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ path = "/", folderId, environment, projectId }) => { + mutationFn: async ({ path = "/", folderId, environment, projectId, forceDelete = true }) => { const { data } = await apiRequest.delete(`/api/v2/folders/${folderId}`, { data: { environment, projectId, - path + path, + forceDelete } }); return data; diff --git a/frontend/src/hooks/api/secretFolders/types.ts b/frontend/src/hooks/api/secretFolders/types.ts index e9ee2ae5d..f3e3cecaf 100644 --- a/frontend/src/hooks/api/secretFolders/types.ts +++ b/frontend/src/hooks/api/secretFolders/types.ts @@ -59,6 +59,7 @@ export type TDeleteFolderDTO = { environment: string; folderId: string; path?: string; + forceDelete?: boolean; }; export type TUpdateFolderBatchDTO = {