diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts index 44fcb7825..6fe88ef49 100644 --- a/backend/src/ee/services/permission/default-roles.ts +++ b/backend/src/ee/services/permission/default-roles.ts @@ -113,7 +113,6 @@ const buildAdminPermissionRules = () => { can( [ - ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue, ProjectPermissionSecretActions.Create, @@ -194,7 +193,6 @@ const buildMemberPermissionRules = () => { can( [ - ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue, ProjectPermissionSecretActions.Edit, @@ -372,9 +370,10 @@ const buildMemberPermissionRules = () => { const buildViewerPermissionRules = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); - can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets); - can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); - can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets); + can( + [ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue], + ProjectPermissionSub.Secrets + ); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); diff --git a/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx b/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx index 550e0e1a1..493a7f126 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx @@ -25,13 +25,15 @@ import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@a import { usePopUp } from "@app/hooks"; import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api"; import { ProjectMembershipRole, TProjectRole } from "@app/hooks/api/roles/types"; +import { DuplicateRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateRoleModal"; import { RoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/RoleModal"; export const ProjectRoleList = () => { const navigate = useNavigate(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "role", - "deleteRole" + "deleteRole", + "duplicateRole" ] as const); const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace?.id || ""; @@ -139,6 +141,25 @@ export const ProjectRoleList = () => { )} + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("duplicateRole", role); + }} + disabled={!isAllowed} + > + Duplicate Role + + )} + {!isNonMutatable && ( { onClose={() => handlePopUpClose("deleteRole")} onDeleteApproved={handleRoleDelete} /> + handlePopUpToggle("duplicateRole", isOpen)} + roleSlug={(popUp?.duplicateRole?.data as TProjectRole)?.slug} + /> ); }; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx index 0431726da..d649e3ac6 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx @@ -19,6 +19,7 @@ import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@a import { useDeleteProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { usePopUp } from "@app/hooks/usePopUp"; +import { DuplicateRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateRoleModal"; import { ProjectAccessControlTabs } from "@app/types/project"; import { RoleDetailsSection } from "./components/RoleDetailsSection"; @@ -40,7 +41,8 @@ const Page = () => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "role", - "deleteRole" + "deleteRole", + "duplicateRole" ] as const); const onDeleteRoleSubmit = async () => { @@ -117,6 +119,24 @@ const Page = () => { )} + + {(isAllowed) => ( + { + handlePopUpOpen("duplicateRole"); + }} + disabled={!isAllowed} + > + Duplicate Role + + )} + { deleteKey="confirm" onDeleteApproved={() => onDeleteRoleSubmit()} /> + handlePopUpToggle("duplicateRole", isOpen)} + roleSlug={roleSlug} + /> ); }; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateRoleModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateRoleModal.tsx new file mode 100644 index 000000000..fd63a873b --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateRoleModal.tsx @@ -0,0 +1,158 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useNavigate } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input, Modal, ModalContent, Spinner } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useCreateProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api"; +import { TProjectRole } from "@app/hooks/api/roles/types"; +import { slugSchema } from "@app/lib/schemas"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + roleSlug?: string; +}; + +const schema = z + .object({ + name: z.string(), + description: z.string(), + slug: slugSchema({ min: 1 }) + }) + .required(); + +export type FormData = z.infer; + +type ContentProps = { + role: TProjectRole; + onClose: () => void; +}; + +const Content = ({ role, onClose }: ContentProps) => { + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + defaultValues: { + name: `${role.name} Duplicate` + }, + resolver: zodResolver(schema) + }); + + const { currentWorkspace } = useWorkspace(); + + const createRole = useCreateProjectRole(); + const navigate = useNavigate(); + + const handleDuplicateRole = async (form: FormData) => { + try { + const newRole = await createRole.mutateAsync({ + projectId: role.projectId, + permissions: role.permissions, + ...form + }); + + createNotification({ + type: "success", + text: "Role duplicated successfully" + }); + + navigate({ + to: `/${currentWorkspace.type}/$projectId/roles/$roleSlug` as const, + params: { + roleSlug: newRole.slug, + projectId: currentWorkspace.id + } + }); + + onClose(); + } catch { + createNotification({ + type: "error", + text: "Failed to duplicate role" + }); + } + }; + + return ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
+ + ); +}; + +export const DuplicateRoleModal = ({ isOpen, onOpenChange, roleSlug }: Props) => { + const { currentWorkspace } = useWorkspace(); + + const { data: role, isPending } = useGetProjectRoleBySlug(currentWorkspace.id, roleSlug ?? ""); + + if (!roleSlug) return null; + + return ( + + + {/* eslint-disable-next-line no-nested-ternary */} + {isPending ? ( +
+ +

Loading Role...

+
+ ) : role ? ( + onOpenChange(false)} /> + ) : ( + Error: could not find role with slug "{roleSlug}" + )} +
+
+ ); +};