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 && (
+
+
}
+ onClick={() => {
+ router.push(`/org/${orgId}/members`);
+ }}
+ className="mb-4"
+ >
+ Roles
+
+
+
{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");
+ }}
+ >
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ );
+};
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";