Weave roles into groups

This commit is contained in:
Tuan Dang
2024-03-19 11:34:28 -07:00
parent d8d63ecaec
commit 2a134b9dc2
10 changed files with 132 additions and 50 deletions

View File

@@ -1,11 +1,46 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TGroupDALFactory = ReturnType<typeof groupDALFactory>; export type TGroupDALFactory = ReturnType<typeof groupDALFactory>;
export const groupDALFactory = (db: TDbClient) => { export const groupDALFactory = (db: TDbClient) => {
const groupOrm = ormify(db, TableName.Groups); const groupOrm = ormify(db, TableName.Groups);
return { ...groupOrm }; const findByOrgId = async (orgId: string, tx?: Knex) => {
try {
const docs = await (tx || db)(TableName.Groups)
.where(`${TableName.Groups}.orgId`, orgId)
.leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`)
.select(selectAllTableCols(TableName.Groups))
// cr stands for custom role
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
.select(db.ref("name").as("crName").withSchema(TableName.OrgRoles))
.select(db.ref("slug").as("crSlug").withSchema(TableName.OrgRoles))
.select(db.ref("description").as("crDescription").withSchema(TableName.OrgRoles))
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles));
return docs.map(({ crId, crDescription, crSlug, crPermission, crName, ...el }) => ({
...el,
customRole: el.roleId
? {
id: crId,
name: crName,
slug: crSlug,
permissions: crPermission,
description: crDescription
}
: undefined
}));
} catch (error) {
throw new DatabaseError({ error, name: "FindByOrgId" });
}
};
return {
findByOrgId,
...groupOrm
};
}; };

View File

@@ -105,7 +105,7 @@ export const groupServiceFactory = ({ groupDAL, permissionService, licenseServic
...(role ...(role
? { ? {
role: customRole ? OrgMembershipRole.Custom : role, role: customRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id roleId: customRole?.id ?? null
} }
: {}) : {})
} }

View File

@@ -5,6 +5,7 @@ import {
IncidentContactsSchema, IncidentContactsSchema,
OrganizationsSchema, OrganizationsSchema,
OrgMembershipsSchema, OrgMembershipsSchema,
OrgRolesSchema,
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -212,7 +213,17 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
groups: GroupsSchema.array() groups: GroupsSchema.merge(
z.object({
customRole: OrgRolesSchema.pick({
id: true,
name: true,
slug: true,
permissions: true,
description: true
}).optional()
})
).array()
}) })
} }
}, },

View File

@@ -46,6 +46,7 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
orgId: req.params.orgId orgId: req.params.orgId
}); });
return { identityMemberships }; return { identityMemberships };
} }
}); });

View File

@@ -119,7 +119,7 @@ export const orgServiceFactory = ({
const getOrgGroups = async ({ actor, actorId, orgId, actorAuthMethod, actorOrgId }: TGetOrgGroupsDTO) => { const getOrgGroups = async ({ actor, actorId, orgId, actorAuthMethod, actorOrgId }: TGetOrgGroupsDTO) => {
await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const groups = await groupDAL.find({ orgId }); const groups = await groupDAL.findByOrgId(orgId);
return groups; return groups;
}; };

View File

@@ -1,3 +1,9 @@
import { TOrgRole } from "../roles/types";
export type TGroupOrgMembership = TGroup & {
customRole?: TOrgRole;
}
export type TGroup = { export type TGroup = {
id: string; id: string;
name: string; name: string;

View File

@@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { TGroup } from "../groups/types"; import { TGroupOrgMembership } from "../groups/types";
import { IdentityMembershipOrg } from "../identities/types"; import { IdentityMembershipOrg } from "../identities/types";
import { import {
BillingDetails, BillingDetails,
@@ -412,7 +412,7 @@ export const useGetOrganizationGroups = (organizationId: string) => {
queryFn: async () => { queryFn: async () => {
const { const {
data: { groups } data: { groups }
} = await apiRequest.get<{ groups: TGroup[] }>(`/api/v1/organization/${organizationId}/groups`); } = await apiRequest.get<{ groups: TGroupOrgMembership[] }>(`/api/v1/organization/${organizationId}/groups`);
return groups; return groups;
} }

View File

@@ -1,16 +1,15 @@
import { useState } from "react"; import { useState } from "react";
// import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { faMagnifyingGlass, faPencil, faUsers, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faMagnifyingGlass, faPencil, faUsers, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { import {
// Button,
EmptyState, EmptyState,
IconButton, IconButton,
Input, Input,
// Select, Select,
// SelectItem, SelectItem,
Table, Table,
TableContainer, TableContainer,
TableSkeleton, TableSkeleton,
@@ -19,14 +18,16 @@ import {
Th, Th,
THead, THead,
Tooltip, Tooltip,
Tr} from "@app/components/v2"; Tr
} from "@app/components/v2";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects, OrgPermissionSubjects,
useOrganization} from "@app/context"; useOrganization} from "@app/context";
import { import {
useGetOrganizationGroups, useGetOrganizationGroups,
// useGetOrgRoles useGetOrgRoles,
useUpdateGroup
} from "@app/hooks/api"; } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -39,6 +40,11 @@ type Props = {
groupId?: string; groupId?: string;
name?: string; name?: string;
slug?: string; slug?: string;
role?: string;
customRole?: {
name: string;
slug: string;
}
} }
) => void; ) => void;
}; };
@@ -46,41 +52,40 @@ type Props = {
export const OrgGroupsTable = ({ export const OrgGroupsTable = ({
handlePopUpOpen handlePopUpOpen
}: Props) => { }: Props) => {
// const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const [searchGroupsFilter, setSearchGroupsFilter] = useState(""); const [searchGroupsFilter, setSearchGroupsFilter] = useState("");
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
const { isLoading, data: groups } = useGetOrganizationGroups(orgId); const { isLoading, data: groups } = useGetOrganizationGroups(orgId);
const { mutateAsync: updateMutateAsync } = useUpdateGroup();
// const { data: roles } = useGetOrgRoles(orgId); const { data: roles } = useGetOrgRoles(orgId);
console.log("OrgGroupsTable groups: ", groups); const handleChangeRole = async ({
console.log("OrgGroupsTable roles: ", groups); currentSlug,
role
// const handleChangeRole = ({ }: {
// groupId, currentSlug: string;
// role role: string;
// }: { }) => {
// groupId: string; try {
// role: string; await updateMutateAsync({
// }) => { currentSlug,
// try { role
});
// // TODO createNotification({
text: "Successfully updated group role",
// createNotification({ type: "success"
// text: "Successfully updated group role", });
// type: "success" } catch (err) {
// }); console.error(err);
// } catch (err) { createNotification({
// console.error(err); text: "Failed to update group role",
type: "error"
// createNotification({ });
// text: "Failed to update group role", }
// type: "error" }
// });
// }
// }
return ( return (
<div> <div>
@@ -102,12 +107,40 @@ export const OrgGroupsTable = ({
</THead> </THead>
<TBody> <TBody>
{isLoading && <TableSkeleton columns={4} innerKey="org-groups" />} {isLoading && <TableSkeleton columns={4} innerKey="org-groups" />}
{!isLoading && groups?.map(({ id, name, slug }) => { {!isLoading && groups?.map(({ id, name, slug, role, customRole }) => {
return ( return (
<Tr className="h-10" key={`org-group-${id}`}> <Tr className="h-10" key={`org-group-${id}`}>
<Td>{name}</Td> <Td>{name}</Td>
<Td>{slug}</Td> <Td>{slug}</Td>
<Td>N/A</Td> <Td>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => {
return (
<Select
value={role === "custom" ? (customRole?.slug as string) : role}
isDisabled={!isAllowed}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) =>
handleChangeRole({
currentSlug: slug,
role: selectedRole
})
}
>
{(roles || []).map(({ slug: roleSlug, name: roleName }) => (
<SelectItem value={roleSlug} key={`role-option-${roleSlug}`}>
{roleName}
</SelectItem>
))}
</Select>
);
}}
</OrgPermissionCan>
</Td>
<Td> <Td>
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">
<OrgPermissionCan <OrgPermissionCan
@@ -121,7 +154,9 @@ export const OrgGroupsTable = ({
handlePopUpOpen("group", { handlePopUpOpen("group", {
groupId: id, groupId: id,
name, name,
slug slug,
role,
customRole
}); });
}} }}
size="lg" size="lg"
@@ -143,7 +178,6 @@ export const OrgGroupsTable = ({
<Tooltip content="Delete group"> <Tooltip content="Delete group">
<IconButton <IconButton
onClick={() => { onClick={() => {
console.log("Delete group");
handlePopUpOpen("deleteGroup", { handlePopUpOpen("deleteGroup", {
slug, slug,
name name

View File

@@ -52,9 +52,6 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
const { data, isLoading } = useGetIdentityMembershipOrgs(orgId); const { data, isLoading } = useGetIdentityMembershipOrgs(orgId);
const { data: roles } = useGetOrgRoles(orgId); const { data: roles } = useGetOrgRoles(orgId);
console.log("IdentityTable data: ", data);
console.log("IdentityTable roles: ", roles);
const handleChangeRole = async ({ identityId, role }: { identityId: string; role: string }) => { const handleChangeRole = async ({ identityId, role }: { identityId: string; role: string }) => {
try { try {

View File

@@ -62,8 +62,6 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
const { data: serverDetails } = useFetchServerStatus(); const { data: serverDetails } = useFetchServerStatus();
const { data: members, isLoading: isMembersLoading } = useGetOrgUsers(orgId); const { data: members, isLoading: isMembersLoading } = useGetOrgUsers(orgId);
console.log("OrgGroupsTable members: ", members);
const { mutateAsync: addUserMutateAsync } = useAddUserToOrg(); const { mutateAsync: addUserMutateAsync } = useAddUserToOrg();
const { mutateAsync: updateUserOrgRole } = useUpdateOrgUserRole(); const { mutateAsync: updateUserOrgRole } = useUpdateOrgUserRole();