feat: move folders

This commit is contained in:
Daniel Hougaard
2024-11-12 01:41:37 +04:00
parent 0e946f73bd
commit abb56379fc
4 changed files with 196 additions and 1 deletions

View File

@ -582,6 +582,11 @@ export const FOLDERS = {
projectSlug: "The slug of the project where the folder is located.",
workspaceId: "The ID of the project where the folder is located."
},
MOVE: {
projectId: "The ID of the project to move the folder from.",
folderId: "The ID of the folder to move.",
newPath: "The new path of the folder."
},
DELETE: {
folderIdOrName: "The ID or name of the folder to delete.",
workspaceId: "The ID of the project to delete the folder from.",

View File

@ -5,7 +5,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { FOLDERS } from "@app/lib/api-docs";
import { prefixWithSlash, removeTrailingSlash } from "@app/lib/fn";
import { isValidFolderName } from "@app/lib/validator";
import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
import { readLimit, secretsLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@ -84,6 +84,50 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}
});
server.route({
url: "/:folderId/move",
method: "PATCH",
config: {
rateLimit: writeLimit
},
schema: {
description: "Move folder to new path",
security: [{ bearerAuth: [] }],
params: z.object({
folderId: z.string().describe(FOLDERS.MOVE.folderId)
}),
body: z.object({
projectId: z.string().trim().describe(FOLDERS.MOVE.projectId),
newPath: z
.string()
.trim()
.describe(FOLDERS.MOVE.newPath)
.transform(prefixWithSlash)
.transform(removeTrailingSlash)
}),
response: {
200: z.object({
folder: SecretFoldersSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const folder = await server.services.folder.moveFolder({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
folderId: req.params.folderId,
projectId: req.body.projectId,
newPath: req.body.newPath
});
return { folder };
}
});
server.route({
url: "/:folderId",
method: "PATCH",

View File

@ -19,6 +19,7 @@ import {
TGetFolderDTO,
TGetFoldersDeepByEnvsDTO,
TUpdateFolderDTO,
TUpdateFolderPathDTO,
TUpdateManyFoldersDTO
} from "./secret-folder-types";
import { TSecretFolderVersionDALFactory } from "./secret-folder-version-dal";
@ -329,6 +330,145 @@ export const secretFolderServiceFactory = ({
return { folder: newFolder, old: folder };
};
const moveFolder = async ({
folderId,
newPath: secretPath,
projectId,
actor,
actorAuthMethod,
actorId,
actorOrgId
}: TUpdateFolderPathDTO) => {
const [folder] = await folderDAL.findSecretPathByFolderIds(projectId, [folderId]);
if (!folder) {
throw new NotFoundError({ message: `Folder with ID '${folderId}' not found` });
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
// Has permission for source folder
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.SecretFolders, { environment: folder.environmentSlug, secretPath: folder.path })
);
// Has permission for destination folder
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.SecretFolders, { environment: folder.environmentSlug, secretPath })
);
if (!folder.parentId) {
throw new BadRequestError({ message: `Cannot move root folder` });
}
const parentFolder = await folderDAL.findById(folder.parentId);
if (!parentFolder) {
throw new NotFoundError({ message: `Parent folder with ID '${folder.parentId}' not found` });
}
if (secretPath !== "/") {
const newPathSegments = secretPath.split("/").filter(Boolean);
if (newPathSegments.length === 0) {
throw new BadRequestError({ message: `Invalid new path '${secretPath}'` });
}
}
if (secretPath === folder.path) {
return folder;
}
const existingFolder = await folderDAL.findBySecretPath(
projectId,
folder.environmentSlug,
`${secretPath}/${folder.name}`
);
if (existingFolder) {
throw new BadRequestError({
message: `Folder with name '${existingFolder.name}' already exists in the new path '${secretPath}'`
});
}
const movedFolder = await folderDAL.transaction(async (tx) => {
const newParentFolder = await folderDAL.findClosestFolder(projectId, folder.environmentSlug, secretPath, tx);
if (!newParentFolder) {
throw new NotFoundError({ message: `Parent folder with path '${secretPath}' not found` });
}
let parentFolderId = newParentFolder.id;
// create any missing intermediate folders in the new path
if (newParentFolder.path !== secretPath) {
const missingSegments = secretPath.substring(newParentFolder.path.length).split("/").filter(Boolean);
if (missingSegments.length) {
const newFolders: Array<TSecretFoldersInsert & { id: string }> = missingSegments.map((segment) => {
const newFolder = {
name: segment,
parentId: parentFolderId,
id: uuidv4(),
envId: folder.envId,
version: 1
};
parentFolderId = newFolder.id;
return newFolder;
});
parentFolderId = newFolders.at(-1)?.id as string;
const docs = await folderDAL.insertMany(newFolders, tx);
await folderVersionDAL.insertMany(
docs.map((doc) => ({
name: doc.name,
envId: doc.envId,
version: doc.version,
folderId: doc.id
})),
tx
);
}
}
if (parentFolderId === folder.id) {
throw new BadRequestError({ message: `Cannot move folder into itself` });
}
// update the folder's parent id to point to the new location
const [updatedFolder] = await folderDAL.update(
{ id: folder.id },
{
parentId: parentFolderId,
version: (folder.version || 0) + 1
},
tx
);
// create a new version record
await folderVersionDAL.create(
{
name: updatedFolder.name,
envId: updatedFolder.envId,
version: updatedFolder.version,
folderId: updatedFolder.id
},
tx
);
return updatedFolder;
});
await snapshotService.performSnapshot(movedFolder.id);
return movedFolder;
};
const deleteFolder = async ({
projectId,
actor,
@ -540,6 +680,7 @@ export const secretFolderServiceFactory = ({
createFolder,
updateFolder,
updateManyFolders,
moveFolder,
deleteFolder,
getFolders,
getFolderById,

View File

@ -18,6 +18,11 @@ export type TUpdateFolderDTO = {
name: string;
} & TProjectPermission;
export type TUpdateFolderPathDTO = {
folderId: string;
newPath: string;
} & TProjectPermission;
export type TUpdateManyFoldersDTO = {
projectSlug: string;
folders: {