From 3670b16657e5f607dc365cf8a2065358a9f33fc6 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Mon, 11 Dec 2023 16:20:32 +0530 Subject: [PATCH] feat(infisical-pg): new org role routes completed --- backend-pg/package.json | 3 +- .../20231204092747_org-membership.ts | 2 +- .../src/ee/routes/v1/org-role-router.ts | 15 +++--- .../src/services/org/org-role-service.ts | 15 +++--- frontend/src/hooks/api/roles/index.tsx | 16 +++++- frontend/src/hooks/api/roles/mutation.tsx | 54 ++++++++++++++++++- frontend/src/hooks/api/roles/queries.tsx | 20 +++++++ frontend/src/hooks/api/roles/types.ts | 30 +++++++++++ .../OrgMembersSection/OrgMembersTable.tsx | 5 +- .../OrgRoleModifySection.tsx | 13 ++--- .../OrgRoleModifySection.utils.ts | 2 +- .../OrgRoleTabSection/OrgRoleTabSection.tsx | 4 +- .../OrgRoleTabSection/OrgRoleTable.tsx | 20 ++++--- 13 files changed, 157 insertions(+), 42 deletions(-) diff --git a/backend-pg/package.json b/backend-pg/package.json index 717f74acd9..872350f119 100644 --- a/backend-pg/package.json +++ b/backend-pg/package.json @@ -19,7 +19,8 @@ "migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest", "migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback", "seed:new": "tsx ./scripts/create-seed-file.ts", - "seed:run": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run" + "seed:run": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run", + "db:reset": "npm run migration:rollback -- --all && npm run migration:latest && npm run seed:run" }, "keywords": [], "author": "", diff --git a/backend-pg/src/db/migrations/20231204092747_org-membership.ts b/backend-pg/src/db/migrations/20231204092747_org-membership.ts index 895ec088ba..ea8a42ac77 100644 --- a/backend-pg/src/db/migrations/20231204092747_org-membership.ts +++ b/backend-pg/src/db/migrations/20231204092747_org-membership.ts @@ -12,7 +12,7 @@ export async function up(knex: Knex): Promise { t.string("name").notNullable(); t.string("description"); t.string("slug").notNullable(); - t.json("permissions").notNullable(); + t.jsonb("permissions").notNullable(); // does not need update trigger we will do it manually t.timestamps(true, true, true); t.uuid("orgId").notNullable(); diff --git a/backend-pg/src/ee/routes/v1/org-role-router.ts b/backend-pg/src/ee/routes/v1/org-role-router.ts index 27fe6d21fc..65af3cebdd 100644 --- a/backend-pg/src/ee/routes/v1/org-role-router.ts +++ b/backend-pg/src/ee/routes/v1/org-role-router.ts @@ -17,7 +17,6 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { name: z.string().trim(), description: z.string().trim().optional(), workspaceId: z.string().trim().optional(), - orgId: z.string().trim(), permissions: z.any().array() }), response: { @@ -31,7 +30,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { const role = await server.services.orgRole.createRole( req.auth.userId, req.params.organizationId, - { ...req.body, permissions: JSON.stringify(req.body.permissions) } + req.body ); return { role }; } @@ -49,8 +48,6 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { slug: z.string().trim().optional(), name: z.string().trim().optional(), description: z.string().trim().optional(), - workspaceId: z.string().trim().optional(), - orgId: z.string().trim(), permissions: z.any().array() }), response: { @@ -108,9 +105,11 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - roles: OrgRolesSchema.omit({ permissions: true }) - .merge(z.object({ permissions: z.unknown() })) - .array() + data: z.object({ + roles: OrgRolesSchema.omit({ permissions: true }) + .merge(z.object({ permissions: z.unknown() })) + .array() + }) }) } }, @@ -120,7 +119,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { req.auth.userId, req.params.organizationId ); - return { roles }; + return { data: { roles } }; } }); diff --git a/backend-pg/src/services/org/org-role-service.ts b/backend-pg/src/services/org/org-role-service.ts index f47f90ff08..1047ade608 100644 --- a/backend-pg/src/services/org/org-role-service.ts +++ b/backend-pg/src/services/org/org-role-service.ts @@ -36,8 +36,11 @@ export const orgRoleServiceFactory = ({ ); const existingRole = await orgRoleDal.findOne({ slug: data.slug, orgId }); if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" }); - - const role = await orgRoleDal.create({ ...data, orgId }); + const role = await orgRoleDal.create({ + ...data, + orgId, + permissions: JSON.stringify(data.permissions) + }); return role; }; @@ -83,8 +86,8 @@ export const orgRoleServiceFactory = ({ const customRoles = await orgRoleDal.find({ orgId }); const roles = [ { - id: "admin", - orgId: "", + id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid + orgId, name: "Admin", slug: "admin", description: "Complete administration access over the organization", @@ -93,8 +96,8 @@ export const orgRoleServiceFactory = ({ updatedAt: new Date() }, { - id: "member", - orgId: "", + id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response + orgId, name: "Member", slug: "member", description: "Non-administrative role in an organization", diff --git a/frontend/src/hooks/api/roles/index.tsx b/frontend/src/hooks/api/roles/index.tsx index ff7314db86..6afd209ba3 100644 --- a/frontend/src/hooks/api/roles/index.tsx +++ b/frontend/src/hooks/api/roles/index.tsx @@ -1,2 +1,14 @@ -export { useCreateRole, useDeleteRole, useUpdateRole } from "./mutation"; -export { useGetRoles, useGetUserOrgPermissions,useGetUserProjectPermissions } from "./queries"; +export { + useCreateOrgRole, + useCreateRole, + useDeleteOrgRole, + useDeleteRole, + useUpdateOrgRole, + useUpdateRole +} from "./mutation"; +export { + useGetOrgRoles, + useGetRoles, + useGetUserOrgPermissions, + useGetUserProjectPermissions +} from "./queries"; diff --git a/frontend/src/hooks/api/roles/mutation.tsx b/frontend/src/hooks/api/roles/mutation.tsx index e5fda67b16..5a0ad31908 100644 --- a/frontend/src/hooks/api/roles/mutation.tsx +++ b/frontend/src/hooks/api/roles/mutation.tsx @@ -1,9 +1,17 @@ +import { packRules } from "@casl/ability/extra"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { roleQueryKeys } from "./queries"; -import { TCreateRoleDTO, TDeleteRoleDTO, TUpdateRoleDTO } from "./types"; +import { + TCreateOrgRoleDTO, + TCreateRoleDTO, + TDeleteOrgRoleDTO, + TDeleteRoleDTO, + TUpdateOrgRoleDTO, + TUpdateRoleDTO +} from "./types"; export const useCreateRole = () => { const queryClient = useQueryClient(); @@ -40,3 +48,47 @@ export const useDeleteRole = () => { } }); }; + +export const useCreateOrgRole = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ orgId, permissions, ...dto }: TCreateOrgRoleDTO) => + apiRequest.post(`/api/ee/v1/organization/${orgId}/roles`, { + ...dto, + permissions: permissions.length ? packRules(permissions) : [] + }), + onSuccess: (_, { orgId }) => { + queryClient.invalidateQueries(roleQueryKeys.getOrgRoles(orgId)); + } + }); +}; + +export const useUpdateOrgRole = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, orgId, permissions, ...dto }: TUpdateOrgRoleDTO) => + apiRequest.patch(`/api/ee/v1/organization/${orgId}/roles/${id}`, { + ...dto, + permissions: permissions?.length ? packRules(permissions) : undefined + }), + onSuccess: (_, { orgId }) => { + queryClient.invalidateQueries(roleQueryKeys.getOrgRoles(orgId)); + } + }); +}; + +export const useDeleteOrgRole = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ orgId, id }: TDeleteOrgRoleDTO) => + apiRequest.delete(`/api/ee/v1/organization/${orgId}/roles/${id}`, { + data: { orgId } + }), + onSuccess: (_, { orgId }) => { + queryClient.invalidateQueries(roleQueryKeys.getOrgRoles(orgId)); + } + }); +}; diff --git a/frontend/src/hooks/api/roles/queries.tsx b/frontend/src/hooks/api/roles/queries.tsx index f4d29073fa..336b9da062 100644 --- a/frontend/src/hooks/api/roles/queries.tsx +++ b/frontend/src/hooks/api/roles/queries.tsx @@ -13,6 +13,8 @@ import { TGetRolesDTO, TGetUserOrgPermissionsDTO, TGetUserProjectPermissionDTO, + TOrgRole, + TPermission, TRole } from "./types"; @@ -36,6 +38,7 @@ const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob }); export const roleQueryKeys = { getRoles: ({ orgId, workspaceId }: TGetRolesDTO) => ["roles", { orgId, workspaceId }] as const, + getOrgRoles: (orgId: string) => ["org-roles", { orgId }] as const, getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) => ["user-permissions", { orgId }] as const, getUserProjectPermissions: ({ workspaceId }: TGetUserProjectPermissionDTO) => @@ -63,6 +66,23 @@ export const useGetRoles = ({ orgId, workspaceId }: TGetRolesDTO) => enabled: Boolean(orgId) }); +const getOrgRoles = async (orgId: string) => { + const { data } = await apiRequest.get<{ + data: { roles: Array & { permissions: unknown }> }; + }>(`/api/ee/v1/organization/${orgId}/roles`); + return data.data.roles.map(({ permissions, ...el }) => ({ + ...el, + permissions: unpackRules(permissions as PackRule[]) + })); +}; + +export const useGetOrgRoles = (orgId: string, enable = true) => + useQuery({ + queryKey: roleQueryKeys.getOrgRoles(orgId), + queryFn: () => getOrgRoles(orgId), + enabled: Boolean(orgId) && enable + }); + const getUserOrgPermissions = async ({ orgId }: TGetUserOrgPermissionsDTO) => { if (orgId === "") return { permissions: [], membership: null }; diff --git a/frontend/src/hooks/api/roles/types.ts b/frontend/src/hooks/api/roles/types.ts index 05d0790fc3..71c7f2a6d1 100644 --- a/frontend/src/hooks/api/roles/types.ts +++ b/frontend/src/hooks/api/roles/types.ts @@ -3,6 +3,7 @@ export type TGetRolesDTO = { workspaceId?: string; }; +// @depreciated export type TRole = { id: string; organization: string; @@ -15,6 +16,17 @@ export type TRole = { updatedAt: string; }; +export type TOrgRole = { + slug: string; + name: string; + orgId: string; + id: string; + createdAt: string; + updatedAt: string; + description?: string; + permissions: TPermission[]; +}; + export type TPermission = { conditions?: Record; action: string; @@ -55,3 +67,21 @@ export type TGetUserOrgPermissionsDTO = { export type TGetUserProjectPermissionDTO = { workspaceId: string; }; + +export type TCreateOrgRoleDTO = { + orgId: string; + name: string; + description?: string; + slug: string; + permissions: TPermission[]; +}; + +export type TUpdateOrgRoleDTO = { + orgId: string; + id: string; +} & Partial>; + +export type TDeleteOrgRoleDTO = { + orgId: string; + id: string; +}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx index 7647aa7dec..7959621791 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx @@ -30,6 +30,7 @@ import { import { useAddUserToOrg, useFetchServerStatus, + useGetOrgRoles, useGetOrgUsers, useGetRoles, useUpdateOrgUserRole @@ -56,9 +57,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop const userId = user?.id || ""; const orgId = currentOrg?.id || ""; - const { data: roles, isLoading: isRolesLoading } = useGetRoles({ - orgId - }); + const { data: roles, isLoading: isRolesLoading } = useGetOrgRoles(orgId); const [searchMemberFilter, setSearchMemberFilter] = useState(""); diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.tsx b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.tsx index d5e97457b9..4706c94c99 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.tsx @@ -8,15 +8,16 @@ import { faServer, faSignIn, faUserCog, - faUsers} from "@fortawesome/free-solid-svg-icons"; + faUsers +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { Button, FormControl, Input } from "@app/components/v2"; import { useOrganization } from "@app/context"; -import { useCreateRole, useUpdateRole } from "@app/hooks/api"; -import { TRole } from "@app/hooks/api/roles/types"; +import { useCreateOrgRole, useUpdateOrgRole } from "@app/hooks/api"; +import { TOrgRole } from "@app/hooks/api/roles/types"; import { formRolePermission2API, @@ -28,7 +29,7 @@ import { SimpleLevelPermissionOption } from "./SimpleLevelPermissionOptions"; import { WorkspacePermission } from "./WorkspacePermission"; type Props = { - role?: TRole; + role?: TOrgRole; onGoBack: VoidFunction; }; @@ -101,8 +102,8 @@ export const OrgRoleModifySection = ({ role, onGoBack }: Props) => { resolver: zodResolver(formSchema) }); - const { mutateAsync: createRole } = useCreateRole(); - const { mutateAsync: updateRole } = useUpdateRole(); + const { mutateAsync: createRole } = useCreateOrgRole(); + const { mutateAsync: updateRole } = useUpdateOrgRole(); const handleRoleUpdate = async (el: TFormSchema) => { if (!role?.id) return; diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts index 18ffa174b4..8bc4d86335 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts @@ -32,7 +32,7 @@ export const formSchema = z.object({ "secret-scanning": generalPermissionSchema, sso: generalPermissionSchema, billing: generalPermissionSchema, - "identity": generalPermissionSchema + identity: generalPermissionSchema }) .optional() }); diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTabSection.tsx b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTabSection.tsx index e3ff11e6ae..2d4dcd3585 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTabSection.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTabSection.tsx @@ -1,7 +1,7 @@ import { motion } from "framer-motion"; import { usePopUp } from "@app/hooks"; -import { TRole } from "@app/hooks/api/roles/types"; +import { TOrgRole } from "@app/hooks/api/roles/types"; import { OrgRoleModifySection } from "./OrgRoleModifySection"; import { OrgRoleTable } from "./OrgRoleTable"; @@ -17,7 +17,7 @@ export const OrgRoleTabSection = () => { exit={{ opacity: 0, translateX: 30 }} > } + role={popUp.editRole.data as TOrgRole} onGoBack={() => handlePopUpClose("editRole")} /> diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx index 13688cd4b5..0b64b5674d 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx @@ -20,11 +20,11 @@ import { } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { usePopUp } from "@app/hooks"; -import { useDeleteRole, useGetRoles } from "@app/hooks/api"; -import { TRole } from "@app/hooks/api/roles/types"; +import { useDeleteOrgRole, useGetOrgRoles } from "@app/hooks/api"; +import { TOrgRole } from "@app/hooks/api/roles/types"; type Props = { - onSelectRole: (role?: TRole) => void; + onSelectRole: (role?: TOrgRole) => void; }; export const OrgRoleTable = ({ onSelectRole }: Props) => { @@ -34,14 +34,12 @@ export const OrgRoleTable = ({ onSelectRole }: Props) => { const { createNotification } = useNotificationContext(); const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const); - const { data: roles, isLoading: isRolesLoading } = useGetRoles({ - orgId - }); + const { data: roles, isLoading: isRolesLoading } = useGetOrgRoles(orgId); - const { mutateAsync: deleteRole } = useDeleteRole(); + const { mutateAsync: deleteRole } = useDeleteOrgRole(); const handleRoleDelete = async () => { - const { id } = popUp?.deleteRole?.data as TRole; + const { id } = popUp?.deleteRole?.data as TOrgRole; try { await deleteRole({ orgId, @@ -90,7 +88,7 @@ export const OrgRoleTable = ({ onSelectRole }: Props) => { {isRolesLoading && } - {(roles as TRole[])?.map((role) => { + {roles?.map((role) => { const { id, name, slug } = role; const isNonMutatable = ["owner", "admin", "member", "no-access"].includes(slug); @@ -148,9 +146,9 @@ export const OrgRoleTable = ({ onSelectRole }: Props) => { )?.name || " " + (popUp?.deleteRole?.data as TOrgRole)?.name || " " } role?`} - deleteKey={(popUp?.deleteRole?.data as TRole)?.slug || ""} + deleteKey={(popUp?.deleteRole?.data as TOrgRole)?.slug || ""} onClose={() => handlePopUpClose("deleteRole")} onDeleteApproved={handleRoleDelete} />