diff --git a/backend/src/ee/routes/v1/org-role-router.ts b/backend/src/ee/routes/v1/org-role-router.ts index 6691032a8..f96b89609 100644 --- a/backend/src/ee/routes/v1/org-role-router.ts +++ b/backend/src/ee/routes/v1/org-role-router.ts @@ -52,6 +52,37 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + // new: note that doesn't work for default roles + method: "GET", + url: "/:organizationId/roles/:roleId", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + organizationId: z.string().trim(), + roleId: z.string().trim() + }), + response: { + 200: z.object({ + role: OrgRolesSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const role = await server.services.orgRole.getRole( + req.permission.id, + req.params.organizationId, + req.params.roleId, + req.permission.authMethod, + req.permission.orgId + ); + return { role }; + } + }); + server.route({ method: "PATCH", url: "/:organizationId/roles/:roleId", diff --git a/backend/src/services/org/org-role-service.ts b/backend/src/services/org/org-role-service.ts index 70c54ff18..262f98bc6 100644 --- a/backend/src/services/org/org-role-service.ts +++ b/backend/src/services/org/org-role-service.ts @@ -42,6 +42,20 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol return role; }; + const getRole = async ( + userId: string, + orgId: string, + roleId: string, + actorAuthMethod: ActorAuthMethod, + actorOrgId: string | undefined + ) => { + const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role); + const role = await orgRoleDAL.findOne({ id: roleId, orgId }); + if (!role) throw new BadRequestError({ message: "Role not found", name: "Get role" }); + return role; + }; + const updateRole = async ( userId: string, orgId: string, @@ -144,5 +158,5 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol return { permissions: packRules(permission.rules), membership }; }; - return { createRole, updateRole, deleteRole, listRoles, getUserPermission }; + return { createRole, getRole, updateRole, deleteRole, listRoles, getUserPermission }; }; diff --git a/frontend/src/hooks/api/roles/index.tsx b/frontend/src/hooks/api/roles/index.tsx index 53c05a7b6..fd8e4e705 100644 --- a/frontend/src/hooks/api/roles/index.tsx +++ b/frontend/src/hooks/api/roles/index.tsx @@ -7,6 +7,7 @@ export { useUpdateProjectRole } from "./mutation"; export { + useGetOrgRole, useGetOrgRoles, useGetProjectRoleBySlug, useGetProjectRoles, diff --git a/frontend/src/hooks/api/roles/queries.tsx b/frontend/src/hooks/api/roles/queries.tsx index f04af697d..47685b749 100644 --- a/frontend/src/hooks/api/roles/queries.tsx +++ b/frontend/src/hooks/api/roles/queries.tsx @@ -40,6 +40,7 @@ export const roleQueryKeys = { getProjectRoleBySlug: (projectSlug: string, roleSlug: string) => ["roles", { projectSlug, roleSlug }] as const, getOrgRoles: (orgId: string) => ["org-roles", { orgId }] as const, + getOrgRole: (orgId: string, roleId: string) => [{ orgId, roleId }, "org-role"] as const, getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) => ["user-permissions", { orgId }] as const, getUserProjectPermissions: ({ workspaceId }: TGetUserProjectPermissionDTO) => @@ -89,6 +90,18 @@ export const useGetOrgRoles = (orgId: string, enable = true) => enabled: Boolean(orgId) && enable }); +export const useGetOrgRole = (orgId: string, roleId: string) => + useQuery({ + queryKey: roleQueryKeys.getOrgRole(orgId, roleId), + queryFn: async () => { + const { data } = await apiRequest.get<{ role: TProjectRole }>( + `/api/v1/organization/${orgId}/roles/${roleId}` // TODO: implement + ); + return data.role; + }, + enabled: Boolean(orgId && roleId) + }); + const getUserOrgPermissions = async ({ orgId }: TGetUserOrgPermissionsDTO) => { if (orgId === "") return { permissions: [], membership: null }; diff --git a/frontend/src/pages/org/[id]/roles/[roleId]/index.tsx b/frontend/src/pages/org/[id]/roles/[roleId]/index.tsx new file mode 100644 index 000000000..082f2d885 --- /dev/null +++ b/frontend/src/pages/org/[id]/roles/[roleId]/index.tsx @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { RolePage } from "@app/views/Org/RolePage"; + +export default function Role() { + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: t("settings.org.title") })} + + + + + ); +} + +Role.requireAuth = true; diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx index 8046dd948..dab3e8e74 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx @@ -1,4 +1,5 @@ -import { faEllipsis,faPlus } from "@fortawesome/free-solid-svg-icons"; +import { useRouter } from "next/router"; +import { faEllipsis, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; @@ -30,6 +31,7 @@ type Props = { }; export const OrgRoleTable = ({ onSelectRole }: Props) => { + const router = useRouter(); const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; @@ -92,6 +94,7 @@ export const OrgRoleTable = ({ onSelectRole }: Props) => { router.push(`/org/${orgId}/roles/${id}`)} > {name} {slug} @@ -114,7 +117,10 @@ export const OrgRoleTable = ({ onSelectRole }: Props) => { className={twMerge( !isAllowed && "pointer-events-none cursor-not-allowed opacity-50" )} - onClick={() => onSelectRole(role)} + onClick={(e) => { + e.stopPropagation(); + onSelectRole(role); + }} disabled={!isAllowed} > Edit Role diff --git a/frontend/src/views/Org/RolePage/RolePage.tsx b/frontend/src/views/Org/RolePage/RolePage.tsx new file mode 100644 index 000000000..d290b45b4 --- /dev/null +++ b/frontend/src/views/Org/RolePage/RolePage.tsx @@ -0,0 +1,285 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useRouter } from "next/router"; +import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { + Button, + DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + Tooltip, + UpgradePlanModal +} from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; +import { withPermission } from "@app/hoc"; +import { + // useDeleteIdentity, + // useGetIdentityById, + // useRevokeIdentityTokenAuthToken, + // useRevokeIdentityUniversalAuthClientSecret, + useGetOrgRole +} from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { RoleDetailsSection } from "./components"; + +// import { IdentityAuthMethodModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal"; +// import { IdentityModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal"; +// import { IdentityUniversalAuthClientSecretModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal"; +// import { +// IdentityAuthenticationSection, +// IdentityClientSecretModal, +// IdentityDetailsSection, +// IdentityProjectsSection, +// IdentityTokenListModal, +// IdentityTokenModal +// } from "./components"; + +export const RolePage = withPermission( + () => { + const router = useRouter(); + const roleId = router.query.roleId as string; + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { data } = useGetOrgRole(orgId, roleId); + console.log("useGetOrgRole data: ", data); + + // const { data } = useGetIdentityById(identityId); // TODO: get role by id + // const { mutateAsync: deleteIdentity } = useDeleteIdentity(); + // const { mutateAsync: revokeToken } = useRevokeIdentityTokenAuthToken(); + // const { mutateAsync: revokeClientSecret } = useRevokeIdentityUniversalAuthClientSecret(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "identity", + "deleteIdentity", + "identityAuthMethod", + "revokeAuthMethod", + "token", + "tokenList", + "revokeToken", + "clientSecret", + "revokeClientSecret", + "universalAuthClientSecret", // list of client secrets + "upgradePlan" + ] as const); + + // const onDeleteIdentitySubmit = async (id: string) => { + // try { + // await deleteIdentity({ + // identityId: id, + // organizationId: orgId + // }); + + // createNotification({ + // text: "Successfully deleted identity", + // type: "success" + // }); + + // handlePopUpClose("deleteIdentity"); + // router.push(`/org/${orgId}/members`); + // } catch (err) { + // console.error(err); + // const error = err as any; + // const text = error?.response?.data?.message ?? "Failed to delete identity"; + + // createNotification({ + // text, + // type: "error" + // }); + // } + // }; + + // const onRevokeTokenSubmit = async ({ + // identityId: parentIdentityId, + // tokenId, + // name + // }: { + // identityId: string; + // tokenId: string; + // name: string; + // }) => { + // try { + // await revokeToken({ + // identityId: parentIdentityId, + // tokenId + // }); + + // handlePopUpClose("revokeToken"); + + // createNotification({ + // text: `Successfully revoked token ${name ?? ""}`, + // type: "success" + // }); + // } catch (err) { + // console.error(err); + // const error = err as any; + // const text = error?.response?.data?.message ?? "Failed to delete identity"; + + // createNotification({ + // text, + // type: "error" + // }); + // } + // }; + + return ( +
+ {data && ( +
+ +
+

{data.name}

+ + +
+ + + +
+
+ + + {(isAllowed) => ( + { + // handlePopUpOpen("identity", { + // identityId, + // name: data.identity.name, + // role: data.role, + // customRole: data.customRole + // }); + }} + disabled={!isAllowed} + > + Edit Role + + )} + + + {(isAllowed) => ( + { + // handlePopUpOpen("deleteIdentity", { + // identityId, + // name: data.identity.name + // }); + }} + disabled={!isAllowed} + > + Delete Role + + )} + + +
+
+
+
+ +
+ Permissions Section + {/* */} +
+
+ )} + {/* + + + + + + handlePopUpToggle("upgradePlan", isOpen)} + text={(popUp.upgradePlan?.data as { description: string })?.description} + /> + handlePopUpToggle("deleteIdentity", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onDeleteIdentitySubmit( + (popUp?.deleteIdentity?.data as { identityId: string })?.identityId + ) + } + /> + handlePopUpToggle("revokeToken", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => { + const revokeTokenData = popUp?.revokeToken?.data as { + identityId: string; + tokenId: string; + name: string; + }; + + return onRevokeTokenSubmit(revokeTokenData); + }} + /> + handlePopUpToggle("revokeClientSecret", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => { + const deleteClientSecretData = popUp?.revokeClientSecret?.data as { + clientSecretId: string; + clientSecretPrefix: string; + }; + + return onDeleteClientSecretSubmit({ + clientSecretId: deleteClientSecretData.clientSecretId + }); + }} + /> */} +
+ ); + }, + { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Role } +); diff --git a/frontend/src/views/Org/RolePage/components/RoleDetailsSection.tsx b/frontend/src/views/Org/RolePage/components/RoleDetailsSection.tsx new file mode 100644 index 000000000..42a3a0e67 --- /dev/null +++ b/frontend/src/views/Org/RolePage/components/RoleDetailsSection.tsx @@ -0,0 +1,87 @@ +import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { IconButton, Tooltip } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects,useOrganization } from "@app/context"; +import { useTimedReset } from "@app/hooks"; +import { useGetOrgRole } from "@app/hooks/api"; +// import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + roleId: string; + // handlePopUpOpen: (popUpName: keyof UsePopUpState<[]>, data?: {}) => void; +}; + +export const RoleDetailsSection = ({ roleId }: Props) => { + const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset({ + initialState: "Copy ID to clipboard" + }); + + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { data } = useGetOrgRole(orgId, roleId); + + console.log("useGetOrgRole data: ", data); + + return data ? ( +
+
+

Details

+ + {(isAllowed) => { + return ( + + { + // handlePopUpOpen("identity", { + // identityId, + // name: data.identity.name, + // role: data.role, + // customRole: data.customRole + // }); + }} + > + + + + ); + }} + +
+
+
+

Role ID

+
+

{roleId}

+
+ + { + navigator.clipboard.writeText(roleId); + setCopyTextId("Copied"); + }} + > + + + +
+
+
+
+

Name

+

{data.name}

+
+
+
+ ) : ( +
+ ); +}; diff --git a/frontend/src/views/Org/RolePage/components/index.tsx b/frontend/src/views/Org/RolePage/components/index.tsx new file mode 100644 index 000000000..0432a2cba --- /dev/null +++ b/frontend/src/views/Org/RolePage/components/index.tsx @@ -0,0 +1 @@ +export { RoleDetailsSection } from "./RoleDetailsSection"; diff --git a/frontend/src/views/Org/RolePage/index.tsx b/frontend/src/views/Org/RolePage/index.tsx new file mode 100644 index 000000000..71e7114fc --- /dev/null +++ b/frontend/src/views/Org/RolePage/index.tsx @@ -0,0 +1 @@ +export { RolePage } from "./RolePage";