From efc186ae6c7833bbca82a1be441b273180ebf4b6 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 19 Mar 2024 10:20:51 -0700 Subject: [PATCH] Finish basic CRUD groups --- backend/src/@types/fastify.d.ts | 2 + backend/src/@types/knex.d.ts | 4 + .../src/db/migrations/20240318183910_group.ts | 28 +++ backend/src/db/schemas/groups.ts | 23 ++ backend/src/db/schemas/index.ts | 1 + backend/src/db/schemas/models.ts | 1 + backend/src/ee/routes/v1/group-router.ts | 96 ++++++++ backend/src/ee/routes/v1/index.ts | 2 + backend/src/ee/services/group/group-dal.ts | 11 + .../src/ee/services/group/group-service.ts | 141 ++++++++++++ backend/src/ee/services/group/group-types.ts | 20 ++ .../services/license/__mocks__/licence-fns.ts | 1 + .../src/ee/services/license/licence-fns.ts | 1 + .../src/ee/services/license/license-types.ts | 1 + .../ee/services/permission/org-permission.ts | 7 + backend/src/server/routes/index.ts | 10 + .../server/routes/v1/organization-router.ts | 35 ++- backend/src/services/org/org-service.ts | 13 +- backend/src/services/org/org-types.ts | 2 + .../src/context/OrgPermissionContext/types.ts | 2 + frontend/src/hooks/api/groups/index.tsx | 4 + frontend/src/hooks/api/groups/mutations.tsx | 87 +++++++ frontend/src/hooks/api/groups/queries.tsx | 0 frontend/src/hooks/api/groups/types.ts | 9 + frontend/src/hooks/api/index.tsx | 1 + frontend/src/hooks/api/organization/index.ts | 6 +- .../src/hooks/api/organization/queries.tsx | 18 +- frontend/src/hooks/api/subscriptions/types.ts | 1 + .../src/views/Org/MembersPage/MembersPage.tsx | 12 +- .../components/OrgGroupsTab/OrgGroupsTab.tsx | 17 ++ .../OrgGroupsSection/OrgGroupModal.tsx | 214 ++++++++++++++++++ .../OrgGroupsSection/OrgGroupsSection.tsx | 115 ++++++++++ .../OrgGroupsSection/OrgGroupsTable.tsx | 177 +++++++++++++++ .../components/OrgGroupsSection/index.tsx | 1 + .../OrgGroupsTab/components/index.tsx | 1 + .../components/OrgGroupsTab/index.tsx | 1 + .../IdentitySection/IdentityModal.tsx | 2 +- .../IdentitySection/IdentityTable.tsx | 3 + .../OrgMembersTab/OrgMembersTab.tsx | 2 +- .../OrgMembersSection/OrgMembersTable.tsx | 4 +- .../Org/MembersPage/components/index.tsx | 1 + 41 files changed, 1066 insertions(+), 11 deletions(-) create mode 100644 backend/src/db/migrations/20240318183910_group.ts create mode 100644 backend/src/db/schemas/groups.ts create mode 100644 backend/src/ee/routes/v1/group-router.ts create mode 100644 backend/src/ee/services/group/group-dal.ts create mode 100644 backend/src/ee/services/group/group-service.ts create mode 100644 backend/src/ee/services/group/group-types.ts create mode 100644 frontend/src/hooks/api/groups/index.tsx create mode 100644 frontend/src/hooks/api/groups/mutations.tsx create mode 100644 frontend/src/hooks/api/groups/queries.tsx create mode 100644 frontend/src/hooks/api/groups/types.ts create mode 100644 frontend/src/views/Org/MembersPage/components/OrgGroupsTab/OrgGroupsTab.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/index.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/index.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/OrgGroupsTab/index.tsx diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 0b59aa8a9a..8d36444c05 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -3,6 +3,7 @@ import "fastify"; import { TUsers } from "@app/db/schemas"; import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; +import { TGroupServiceFactory } from "@app/ee/services/group/group-service"; import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; @@ -85,6 +86,7 @@ declare module "fastify" { orgRole: TOrgRoleServiceFactory; superAdmin: TSuperAdminServiceFactory; user: TUserServiceFactory; + group: TGroupServiceFactory; apiKey: TApiKeyServiceFactory; project: TProjectServiceFactory; projectMembership: TProjectMembershipServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 31b80e0e33..549eb12bd1 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -23,6 +23,9 @@ import { TGitAppOrg, TGitAppOrgInsert, TGitAppOrgUpdate, + TGroups, + TGroupsInsert, + TGroupsUpdate, TIdentities, TIdentitiesInsert, TIdentitiesUpdate, @@ -187,6 +190,7 @@ import { declare module "knex/types/tables" { interface Tables { [TableName.Users]: Knex.CompositeTableType; + [TableName.Groups]: Knex.CompositeTableType; [TableName.UserAliases]: Knex.CompositeTableType; [TableName.UserEncryptionKey]: Knex.CompositeTableType< TUserEncryptionKeys, diff --git a/backend/src/db/migrations/20240318183910_group.ts b/backend/src/db/migrations/20240318183910_group.ts new file mode 100644 index 0000000000..cc639504c4 --- /dev/null +++ b/backend/src/db/migrations/20240318183910_group.ts @@ -0,0 +1,28 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.Groups))) { + await knex.schema.createTable(TableName.Groups, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("orgId").notNullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.string("name").notNullable(); + t.string("slug").notNullable(); + t.unique(["orgId", "slug"]); + t.string("role").notNullable(); + t.uuid("roleId"); + t.foreign("roleId").references("id").inTable(TableName.OrgRoles); + t.timestamps(true, true, true); + }); + } + + await createOnUpdateTrigger(knex, TableName.Groups); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.Groups); + await dropOnUpdateTrigger(knex, TableName.Groups); +} diff --git a/backend/src/db/schemas/groups.ts b/backend/src/db/schemas/groups.ts new file mode 100644 index 0000000000..9733d253e3 --- /dev/null +++ b/backend/src/db/schemas/groups.ts @@ -0,0 +1,23 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const GroupsSchema = z.object({ + id: z.string().uuid(), + orgId: z.string().uuid(), + name: z.string(), + slug: z.string(), + role: z.string(), + roleId: z.string().uuid().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TGroups = z.infer; +export type TGroupsInsert = Omit, TImmutableDBKeys>; +export type TGroupsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 001fdbf180..33f02cf63f 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -5,6 +5,7 @@ export * from "./auth-tokens"; export * from "./backup-private-key"; export * from "./git-app-install-sessions"; export * from "./git-app-org"; +export * from "./groups"; export * from "./identities"; export * from "./identity-access-tokens"; export * from "./identity-org-memberships"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index f85feff9c7..7d0900a807 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -2,6 +2,7 @@ import { z } from "zod"; export enum TableName { Users = "users", + Groups = "groups", UserAliases = "user_aliases", UserEncryptionKey = "user_encryption_keys", AuthTokens = "auth_tokens", diff --git a/backend/src/ee/routes/v1/group-router.ts b/backend/src/ee/routes/v1/group-router.ts new file mode 100644 index 0000000000..7ca451847a --- /dev/null +++ b/backend/src/ee/routes/v1/group-router.ts @@ -0,0 +1,96 @@ +import { z } from "zod"; + +import { GroupsSchema, OrgMembershipRole } from "@app/db/schemas"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerGroupRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/", + method: "POST", + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + body: z.object({ + organizationId: z.string().trim(), + name: z.string().trim().min(1), + slug: z.string().trim().min(1), + role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess) // TODO: add describe + }), + response: { + 200: GroupsSchema + } + }, + handler: async (req) => { + const group = await server.services.group.createGroup({ + actor: req.permission.type, + actorId: req.permission.id, + orgId: req.body.organizationId, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + return group; + } + }); + + server.route({ + url: "/:currentSlug", + method: "PATCH", + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + currentSlug: z.string().trim() + }), + body: z + .object({ + name: z.string().trim().min(1), + slug: z.string().trim().min(1), + role: z.string().trim().min(1) + }) + .partial(), + response: { + 200: GroupsSchema + } + }, + handler: async (req) => { + const group = await server.services.group.updateGroup({ + currentSlug: req.params.currentSlug, + actor: req.permission.type, + actorId: req.permission.id, + orgId: req.permission.orgId as string, // note + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + return group; + } + }); + + server.route({ + url: "/:slug", + method: "DELETE", + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + slug: z.string().trim() + }), + response: { + 200: GroupsSchema + } + }, + handler: async (req) => { + const group = await server.services.group.deleteGroup({ + slug: req.params.slug, + actor: req.permission.type, + actorId: req.permission.id, + orgId: req.permission.orgId as string, // note + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + return group; + } + }); +}; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 7d1492f84c..2f48129af3 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,3 +1,4 @@ +import { registerGroupRouter } from "./group-router"; import { registerLdapRouter } from "./ldap-router"; import { registerLicenseRouter } from "./license-router"; import { registerOrgRoleRouter } from "./org-role-router"; @@ -40,4 +41,5 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" }); await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" }); await server.register(registerSecretVersionRouter, { prefix: "/secret" }); + await server.register(registerGroupRouter, { prefix: "/groups" }); }; diff --git a/backend/src/ee/services/group/group-dal.ts b/backend/src/ee/services/group/group-dal.ts new file mode 100644 index 0000000000..e9fae39823 --- /dev/null +++ b/backend/src/ee/services/group/group-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TGroupDALFactory = ReturnType; + +export const groupDALFactory = (db: TDbClient) => { + const groupOrm = ormify(db, TableName.Groups); + + return { ...groupOrm }; +}; diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts new file mode 100644 index 0000000000..f0c5514bc3 --- /dev/null +++ b/backend/src/ee/services/group/group-service.ts @@ -0,0 +1,141 @@ +import { ForbiddenError } from "@casl/ability"; + +import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; +import { isAtLeastAsPrivileged } from "@app/lib/casl"; +import { BadRequestError } from "@app/lib/errors"; + +import { TLicenseServiceFactory } from "../license/license-service"; +import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TGroupDALFactory } from "./group-dal"; +import { TCreateGroupDTO, TDeleteGroupDTO, TUpdateGroupDTO } from "./group-types"; + +type TGroupServiceFactoryDep = { + groupDAL: TGroupDALFactory; + permissionService: Pick; + licenseService: Pick; +}; + +export type TGroupServiceFactory = ReturnType; + +export const groupServiceFactory = ({ groupDAL, permissionService, licenseService }: TGroupServiceFactoryDep) => { + const createGroup = async ({ + name, + slug, + role, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }: TCreateGroupDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Groups); + + const plan = await licenseService.getPlan(orgId); + if (!plan.groups) + throw new BadRequestError({ + message: "Failed to create group due to plan restriction. Upgrade plan to create group." + }); + + const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole( + role, + orgId + ); + const isCustomRole = Boolean(customRole); + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission); + if (!hasRequiredPriviledges) throw new BadRequestError({ message: "Failed to create a more privileged group" }); + + const group = await groupDAL.create({ + name, + slug, + orgId, + role: isCustomRole ? OrgMembershipRole.Custom : role, + roleId: customRole?.id + }); + + return group; + }; + + const updateGroup = async ({ + currentSlug, + name, + slug, + role, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }: TUpdateGroupDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups); + + const plan = await licenseService.getPlan(orgId); + if (!plan.groups) + throw new BadRequestError({ + message: "Failed to update group due to plan restrictio Upgrade plan to update group." + }); + + const group = await groupDAL.findOne({ orgId, slug: currentSlug }); + if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${currentSlug}` }); + + let customRole: TOrgRoles | undefined; + if (role) { + const { permission: rolePermission, role: customOrgRole } = await permissionService.getOrgPermissionByRole( + role, + group.orgId + ); + + const isCustomRole = Boolean(customOrgRole); + const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission); + if (!hasRequiredNewRolePermission) + throw new BadRequestError({ message: "Failed to create a more privileged group" }); + if (isCustomRole) customRole = customOrgRole; + } + + const [updatedGroup] = await groupDAL.update( + { + orgId, + slug: currentSlug + }, + { + name, + slug, + ...(role + ? { + role: customRole ? OrgMembershipRole.Custom : role, + roleId: customRole?.id + } + : {}) + } + ); + + return updatedGroup; + }; + + const deleteGroup = async ({ slug, actor, actorId, orgId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups); + + const plan = await licenseService.getPlan(orgId); + + if (!plan.groups) + throw new BadRequestError({ + message: "Failed to delete group due to plan restriction. Upgrade plan to delete group." + }); + + const [group] = await groupDAL.delete({ + orgId, + slug + }); + + return group; + }; + + return { + createGroup, + updateGroup, + deleteGroup + }; +}; diff --git a/backend/src/ee/services/group/group-types.ts b/backend/src/ee/services/group/group-types.ts new file mode 100644 index 0000000000..cb3399dc54 --- /dev/null +++ b/backend/src/ee/services/group/group-types.ts @@ -0,0 +1,20 @@ +import { TOrgPermission } from "@app/lib/types"; + +export type TCreateGroupDTO = { + name: string; + slug: string; + role: string; +} & TOrgPermission; + +export type TUpdateGroupDTO = { + currentSlug: string; +} & Partial<{ + name: string; + slug: string; + role: string; +}> & + TOrgPermission; + +export type TDeleteGroupDTO = { + slug: string; +} & TOrgPermission; diff --git a/backend/src/ee/services/license/__mocks__/licence-fns.ts b/backend/src/ee/services/license/__mocks__/licence-fns.ts index 8f52939c5b..04c8e59dd2 100644 --- a/backend/src/ee/services/license/__mocks__/licence-fns.ts +++ b/backend/src/ee/services/license/__mocks__/licence-fns.ts @@ -20,6 +20,7 @@ export const getDefaultOnPremFeatures = () => { samlSSO: false, scim: false, ldap: false, + groups: true, status: null, trial_end: null, has_used_trial: true, diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 8dca967370..291e1d5904 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -26,6 +26,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ samlSSO: false, scim: false, ldap: false, + groups: true, status: null, trial_end: null, has_used_trial: true, diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index f8ed8aff34..ecc53a452e 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -42,6 +42,7 @@ export type TFeatureSet = { samlSSO: false; scim: false; ldap: false; + groups: true; status: null; trial_end: null; has_used_trial: true; diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index 30b601c2c0..ec6410037c 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -18,6 +18,7 @@ export enum OrgPermissionSubjects { Sso = "sso", Scim = "scim", Ldap = "ldap", + Groups = "groups", Billing = "billing", SecretScanning = "secret-scanning", Identity = "identity" @@ -33,6 +34,7 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.Scim] | [OrgPermissionActions, OrgPermissionSubjects.Ldap] + | [OrgPermissionActions, OrgPermissionSubjects.Groups] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Identity]; @@ -83,6 +85,11 @@ const buildAdminPermission = () => { can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap); + can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Groups); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups); + can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing); diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index c452bfbd37..768aa9bc50 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -5,6 +5,8 @@ import { registerV1EERoutes } from "@app/ee/routes/v1"; import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal"; import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue"; import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; +import { groupDALFactory } from "@app/ee/services/group/group-dal"; +import { groupServiceFactory } from "@app/ee/services/group/group-service"; import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal"; import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { licenseDALFactory } from "@app/ee/services/license/license-dal"; @@ -194,6 +196,7 @@ export const registerRoutes = async ( const gitAppInstallSessionDAL = gitAppInstallSessionDALFactory(db); const gitAppOrgDAL = gitAppDALFactory(db); + const groupDAL = groupDALFactory(db); const secretScanningDAL = secretScanningDALFactory(db); const licenseDAL = licenseDALFactory(db); @@ -234,6 +237,11 @@ export const registerRoutes = async ( samlConfigDAL, licenseService }); + const groupService = groupServiceFactory({ + groupDAL, + permissionService, + licenseService + }); const scimService = scimServiceFactory({ licenseService, scimDAL, @@ -287,6 +295,7 @@ export const registerRoutes = async ( projectKeyDAL, smtpService, userDAL, + groupDAL, orgBotDAL }); const signupService = authSignupServiceFactory({ @@ -564,6 +573,7 @@ export const registerRoutes = async ( password: passwordService, signup: signupService, user: userService, + group: groupService, permission: permissionService, org: orgService, orgRole: orgRoleService, diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index 42e64e9d77..4a008caa17 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -1,6 +1,12 @@ import { z } from "zod"; -import { IncidentContactsSchema, OrganizationsSchema, OrgMembershipsSchema, UsersSchema } from "@app/db/schemas"; +import { + GroupsSchema, + IncidentContactsSchema, + OrganizationsSchema, + OrgMembershipsSchema, + UsersSchema +} from "@app/db/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -196,4 +202,31 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { return { incidentContactsOrg }; } }); + + server.route({ + method: "GET", + url: "/:organizationId/groups", + schema: { + params: z.object({ + organizationId: z.string().trim() + }), + response: { + 200: z.object({ + groups: GroupsSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const groups = await server.services.org.getOrgGroups({ + actor: req.permission.type, + actorId: req.permission.id, + orgId: req.permission.orgId as string, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + return { groups }; + } + }); }; diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 64cd51e098..da8d4e5613 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -6,6 +6,7 @@ import { Knex } from "knex"; import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas"; import { TProjects } from "@app/db/schemas/projects"; +import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; @@ -34,6 +35,7 @@ import { TDeleteOrgMembershipDTO, TFindAllWorkspacesDTO, TFindOrgMembersByEmailDTO, + TGetOrgGroupsDTO, TInviteUserToOrgDTO, TUpdateOrgDTO, TUpdateOrgMembershipDTO, @@ -45,6 +47,7 @@ type TOrgServiceFactoryDep = { orgBotDAL: TOrgBotDALFactory; orgRoleDAL: TOrgRoleDALFactory; userDAL: TUserDALFactory; + groupDAL: TGroupDALFactory; projectDAL: TProjectDALFactory; projectMembershipDAL: Pick; projectKeyDAL: Pick; @@ -64,6 +67,7 @@ export type TOrgServiceFactory = ReturnType; export const orgServiceFactory = ({ orgDAL, userDAL, + groupDAL, orgRoleDAL, incidentContactDAL, permissionService, @@ -113,6 +117,12 @@ export const orgServiceFactory = ({ return members; }; + const getOrgGroups = async ({ actor, actorId, orgId, actorAuthMethod, actorOrgId }: TGetOrgGroupsDTO) => { + await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const groups = await groupDAL.find({ orgId }); + return groups; + }; + const findOrgMembersByUsername = async ({ actor, actorId, @@ -674,6 +684,7 @@ export const orgServiceFactory = ({ // incident contacts findIncidentContacts, createIncidentContact, - deleteIncidentContact + deleteIncidentContact, + getOrgGroups }; }; diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts index c811f9bc0b..0efc7ffe13 100644 --- a/backend/src/services/org/org-types.ts +++ b/backend/src/services/org/org-types.ts @@ -53,3 +53,5 @@ export type TFindAllWorkspacesDTO = { export type TUpdateOrgDTO = { data: Partial<{ name: string; slug: string; authEnforced: boolean; scimEnabled: boolean }>; } & TOrgPermission; + +export type TGetOrgGroupsDTO = TOrgPermission; diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index 8127cb772e..95a9d00acf 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -16,6 +16,7 @@ export enum OrgPermissionSubjects { Scim = "scim", Sso = "sso", Ldap = "ldap", + Groups = "groups", Billing = "billing", SecretScanning = "secret-scanning", Identity = "identity" @@ -31,6 +32,7 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.Scim] | [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.Ldap] + | [OrgPermissionActions, OrgPermissionSubjects.Groups] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Identity]; diff --git a/frontend/src/hooks/api/groups/index.tsx b/frontend/src/hooks/api/groups/index.tsx new file mode 100644 index 0000000000..996425efc3 --- /dev/null +++ b/frontend/src/hooks/api/groups/index.tsx @@ -0,0 +1,4 @@ +export { + useCreateGroup, + useDeleteGroup, + useUpdateGroup} from "./mutations"; \ No newline at end of file diff --git a/frontend/src/hooks/api/groups/mutations.tsx b/frontend/src/hooks/api/groups/mutations.tsx new file mode 100644 index 0000000000..298fb3e97b --- /dev/null +++ b/frontend/src/hooks/api/groups/mutations.tsx @@ -0,0 +1,87 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { organizationKeys } from "../organization/queries"; +import { TGroup } from "./types"; + +export const useCreateGroup = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + name, + slug, + organizationId, + role + }: { + name: string; + slug: string; + organizationId: string; + role?: string; + }) => { + const { + data: group + } = await apiRequest.post("/api/v1/groups", { + name, + slug, + organizationId, + role + }); + + return group; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgGroups(organizationId)); + } + }); +}; + +export const useUpdateGroup = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + currentSlug, + name, + slug, + role + }: { + currentSlug: string; + name?: string; + slug?: string; + role?: string; + }) => { + const { + data: group + } = await apiRequest.patch(`/api/v1/groups/${currentSlug}`, { + name, + slug, + role + }); + + return group; + }, + onSuccess: ({ orgId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId)); + } + }); +}; + +export const useDeleteGroup = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + slug + }: { + slug: string; + }) => { + const { + data: group + } = await apiRequest.delete(`/api/v1/groups/${slug}`); + + return group; + }, + onSuccess: ({ orgId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId)); + } + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/groups/queries.tsx b/frontend/src/hooks/api/groups/queries.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/hooks/api/groups/types.ts b/frontend/src/hooks/api/groups/types.ts new file mode 100644 index 0000000000..6fb88c4b8b --- /dev/null +++ b/frontend/src/hooks/api/groups/types.ts @@ -0,0 +1,9 @@ +export type TGroup = { + id: string; + name: string; + slug: string; + orgId: string; + createAt: string; + updatedAt: string; + role: string; +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 84c1632c1e..00ccb1cb9a 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -3,6 +3,7 @@ export * from "./apiKeys"; export * from "./auditLogs"; export * from "./auth"; export * from "./bots"; +export * from "./groups"; export * from "./identities"; export * from "./incidentContacts"; export * from "./integrationAuth"; diff --git a/frontend/src/hooks/api/organization/index.ts b/frontend/src/hooks/api/organization/index.ts index 6a8af3b677..c458c67368 100644 --- a/frontend/src/hooks/api/organization/index.ts +++ b/frontend/src/hooks/api/organization/index.ts @@ -7,6 +7,7 @@ export { useDeleteOrgPmtMethod, useDeleteOrgTaxId, useGetIdentityMembershipOrgs, + useGetOrganizationGroups, useGetOrganizations, useGetOrgBillingDetails, useGetOrgInvoices, @@ -17,6 +18,5 @@ export { useGetOrgPmtMethods, useGetOrgTaxIds, useGetOrgTrialUrl, - useUpdateOrg, - useUpdateOrgBillingDetails -} from "./queries"; + useUpdateOrg, + useUpdateOrgBillingDetails} from "./queries"; diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 9e763c7718..0140388e2d 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { TGroup } from "../groups/types"; import { IdentityMembershipOrg } from "../identities/types"; import { BillingDetails, @@ -27,8 +28,8 @@ export const organizationKeys = { getOrgTaxIds: (orgId: string) => [{ orgId }, "organization-tax-ids"] as const, getOrgInvoices: (orgId: string) => [{ orgId }, "organization-invoices"] as const, getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const, - getOrgIdentityMemberships: (orgId: string) => - [{ orgId }, "organization-identity-memberships"] as const + getOrgIdentityMemberships: (orgId: string) => [{ orgId }, "organization-identity-memberships"] as const, + getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const }; export const fetchOrganizations = async () => { @@ -404,3 +405,16 @@ export const useDeleteOrgById = () => { } }); }; + +export const useGetOrganizationGroups = (organizationId: string) => { + return useQuery({ + queryKey: organizationKeys.getOrgGroups(organizationId), + queryFn: async () => { + const { + data: { groups } + } = await apiRequest.get<{ groups: TGroup[] }>(`/api/v1/organization/${organizationId}/groups`); + + return groups; + } + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 97b287084c..393a2fcbfe 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -20,6 +20,7 @@ export type SubscriptionPlan = { samlSSO: boolean; scim: boolean; ldap: boolean; + groups: boolean; status: | "incomplete" | "incomplete_expired" diff --git a/frontend/src/views/Org/MembersPage/MembersPage.tsx b/frontend/src/views/Org/MembersPage/MembersPage.tsx index 673dc28c33..8cc2e6b8dc 100644 --- a/frontend/src/views/Org/MembersPage/MembersPage.tsx +++ b/frontend/src/views/Org/MembersPage/MembersPage.tsx @@ -3,10 +3,16 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { withPermission } from "@app/hoc"; -import { OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from "./components"; +import { + OrgGroupsTab, + OrgIdentityTab, + OrgMembersTab, + OrgRoleTabSection +} from "./components"; enum TabSections { Member = "members", + Groups = "groups", Roles = "roles", Identities = "identities" } @@ -20,6 +26,7 @@ export const MembersPage = withPermission( People + Groups

Machine Identities

@@ -33,6 +40,9 @@ export const MembersPage = withPermission( + + + diff --git a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/OrgGroupsTab.tsx b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/OrgGroupsTab.tsx new file mode 100644 index 0000000000..1194d8696e --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/OrgGroupsTab.tsx @@ -0,0 +1,17 @@ +import { motion } from "framer-motion"; + +import { OrgGroupsSection } from "./components"; + +export const OrgGroupsTab = () => { + return ( + + + + ); +} \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx new file mode 100644 index 0000000000..7945a4d259 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx @@ -0,0 +1,214 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { Button, FormControl, Input, Modal, ModalContent, Select, SelectItem } from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + useCreateGroup, + useGetOrgRoles, + useUpdateGroup +} from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const GroupFormSchema = z.object({ + name: z.string().min(1, "Name cannot be empty").max(50, "Name must be 50 characters or fewer"), + slug: z.string().min(1, "Slug cannot be empty").max(36, "Slug must be 36 characters or fewer"), + role: z.string() +}); + +export type TGroupFormData = z.infer; + +type Props = { + popUp: UsePopUpState<["group"]>; + handlePopUpClose: (popUpName: keyof UsePopUpState<["group"]>) => void; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["group"]>, state?: boolean) => void; +}; + +export const OrgGroupModal = ({ + popUp, + handlePopUpClose, + handlePopUpToggle +}: Props) => { + const { currentOrg } = useOrganization(); + const { createNotification } = useNotificationContext(); + const { data: roles } = useGetOrgRoles(currentOrg?.id || ""); + const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateGroup(); + const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateGroup(); + + const { + control, + handleSubmit, + reset, + } = useForm({ + resolver: zodResolver(GroupFormSchema) + }); + + useEffect(() => { + const group = popUp?.group?.data as { + groupId: string; + name: string; + slug: string; + role: string; + customRole: { + name: string; + slug: string; + }; + }; + + if (!roles?.length) return; + + if (group) { + reset({ + name: group.name, + slug: group.slug, + role: group?.customRole?.slug ?? group.role + }); + } else { + reset({ + name: "", + slug: "", + role: roles[0].slug + }); + } + }, [popUp?.group?.data, roles]); + + const onGroupModalSubmit = async ({ + name, + slug, + role + }: TGroupFormData) => { + try { + if (!currentOrg?.id) return; + + const group = popUp?.group?.data as { + groupId: string; + name: string; + slug: string; + }; + + if (group) { + await updateMutateAsync({ + currentSlug: group.slug, + name, + slug, + role: role || undefined + }); + } else { + await createMutateAsync({ + name, + slug, + organizationId: currentOrg.id, + role: role || undefined + }); + } + handlePopUpToggle("group", false); + + createNotification({ + text: `Successfully ${popUp?.group?.data ? "updated" : "created"} group`, + type: "success" + }); + } catch (err) { + createNotification({ + text: `Failed to ${popUp?.group?.data ? "updated" : "created"} group`, + type: "error" + }); + } + } + + return ( + { + handlePopUpToggle("group", isOpen); + reset(); + }} + > + +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
+ +
+
+ + ); +} \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx new file mode 100644 index 0000000000..87cf926d5d --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx @@ -0,0 +1,115 @@ +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { + Button, + DeleteActionModal, + UpgradePlanModal +} from "@app/components/v2"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useSubscription +} from "@app/context"; +import { useDeleteGroup } from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { OrgGroupModal } from "./OrgGroupModal"; +import { OrgGroupsTable } from "./OrgGroupsTable"; + +export const OrgGroupsSection = () => { + const { createNotification } = useNotificationContext(); + const { subscription } = useSubscription(); + const { mutateAsync: deleteMutateAsync } = useDeleteGroup(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "group", + "deleteGroup", + "upgradePlan" + ] as const); + + const handleAddGroupModal = () => { + if (!subscription?.groups) { + handlePopUpOpen("upgradePlan", { + description: "You can manage users more efficiently with groups if you upgrade your Infisical plan." + }); + } else { + handlePopUpOpen("group"); + } + } + + const onDeleteGroupSubmit = async ({ + name, + slug + }: { + name: string; + slug: string; + }) => { + try { + await deleteMutateAsync({ + slug + }); + createNotification({ + text: `Successfully deleted the group named ${name}`, + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: `Failed to delete the group named ${name}`, + type: "error" + }); + } + + handlePopUpClose("deleteGroup"); + } + + return ( +
+
+

Groups

+ + {(isAllowed) => ( + + )} + +
+ + + handlePopUpToggle("deleteGroup", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onDeleteGroupSubmit( + (popUp?.deleteGroup?.data as { name: string; slug: string }) + ) + } + /> + handlePopUpToggle("upgradePlan", isOpen)} + text={(popUp.upgradePlan?.data as { description: string })?.description} + /> +
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx new file mode 100644 index 0000000000..3cc2175268 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx @@ -0,0 +1,177 @@ +import { useState } from "react"; +// import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { faMagnifyingGlass, faPencil, faUsers, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { +// Button, + EmptyState, + IconButton, + Input, +// Select, +// SelectItem, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tooltip, + Tr} from "@app/components/v2"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization} from "@app/context"; +import { + useGetOrganizationGroups, + // useGetOrgRoles +} from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + handlePopUpOpen: ( + popUpName: keyof UsePopUpState< + ["group", "deleteGroup"] + >, + data?: { + groupId?: string; + name?: string; + slug?: string; + } + ) => void; + }; + +export const OrgGroupsTable = ({ + handlePopUpOpen +}: Props) => { + // const { createNotification } = useNotificationContext(); + const [searchGroupsFilter, setSearchGroupsFilter] = useState(""); + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { isLoading, data: groups } = useGetOrganizationGroups(orgId); + + // const { data: roles } = useGetOrgRoles(orgId); + + console.log("OrgGroupsTable groups: ", groups); + console.log("OrgGroupsTable roles: ", groups); + + // const handleChangeRole = ({ + // groupId, + // role + // }: { + // groupId: string; + // role: string; + // }) => { + // try { + + // // TODO + + // createNotification({ + // text: "Successfully updated group role", + // type: "success" + // }); + // } catch (err) { + // console.error(err); + + // createNotification({ + // text: "Failed to update group role", + // type: "error" + // }); + // } + // } + + return ( +
+ setSearchGroupsFilter(e.target.value)} + leftIcon={} + placeholder="Search groups..." + /> + + + + + + + + + + + {isLoading && } + {!isLoading && groups?.map(({ id, name, slug }) => { + return ( + + + + + + + ); + })} + +
NameSlugRole +
{name}{slug}N/A +
+ + {(isAllowed) => ( + + { + handlePopUpOpen("group", { + groupId: id, + name, + slug + }); + }} + size="lg" + colorSchema="primary" + variant="plain" + ariaLabel="update" + isDisabled={!isAllowed} + > + + + + )} + + + {(isAllowed) => ( + + { + console.log("Delete group"); + handlePopUpOpen("deleteGroup", { + slug, + name + }); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="ml-4" + isDisabled={!isAllowed} + > + + + + )} + +
+
+ {groups?.length === 0 && ( + + )} +
+
+ ); +} diff --git a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/index.tsx b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/index.tsx new file mode 100644 index 0000000000..ca3caabea3 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/index.tsx @@ -0,0 +1 @@ +export { OrgGroupsSection } from "./OrgGroupsSection"; \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/index.tsx b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/index.tsx new file mode 100644 index 0000000000..ca3caabea3 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/index.tsx @@ -0,0 +1 @@ +export { OrgGroupsSection } from "./OrgGroupsSection"; \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/index.tsx b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/index.tsx new file mode 100644 index 0000000000..69936a978e --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/index.tsx @@ -0,0 +1 @@ +export { OrgGroupsTab } from "./OrgGroupsTab"; \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx index 6f582a96f9..695ac51204 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx @@ -140,7 +140,7 @@ export const IdentityModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Pro const error = err as any; const text = error?.response?.data?.message ?? - `Failed to ${popUp?.identity?.data ? "updated" : "created"} identity`; + `Failed to ${popUp?.identity?.data ? "update" : "create"} identity`; createNotification({ text, diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx index 9f58d7cdb6..268092b44e 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx @@ -52,6 +52,9 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { const { data, isLoading } = useGetIdentityMembershipOrgs(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 }) => { try { diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/OrgMembersTab.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/OrgMembersTab.tsx index 0455b4f7dd..a9ace0d076 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/OrgMembersTab.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/OrgMembersTab.tsx @@ -5,7 +5,7 @@ import { OrgMembersSection } from "./components"; export const OrgMembersTab = () => { return ( {!isLoading && filterdUser?.length === 0 && ( - + )}
diff --git a/frontend/src/views/Org/MembersPage/components/index.tsx b/frontend/src/views/Org/MembersPage/components/index.tsx index 9e13e717a0..d08d2f98f7 100644 --- a/frontend/src/views/Org/MembersPage/components/index.tsx +++ b/frontend/src/views/Org/MembersPage/components/index.tsx @@ -1,3 +1,4 @@ +export { OrgGroupsTab } from "./OrgGroupsTab"; export { OrgIdentityTab } from "./OrgIdentityTab"; export { OrgMembersTab } from "./OrgMembersTab"; export { OrgRoleTabSection } from "./OrgRoleTabSection"; \ No newline at end of file