From 435bcd03d38aec52b69c6589e21af4844f821a56 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 2 Sep 2025 13:33:28 -0700 Subject: [PATCH] feature: add ability to join org as super admin --- backend/src/server/routes/v1/admin-router.ts | 30 +++++++ .../super-admin/super-admin-service.ts | 31 +++++++ frontend/src/hooks/api/admin/mutation.ts | 17 ++++ .../components/OrganizationsTable.tsx | 88 ++++++++++++++++--- 4 files changed, 154 insertions(+), 12 deletions(-) diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 1d999ba36..d4f5eb8b0 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -896,4 +896,34 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { return { organizationMembership }; } }); + + server.route({ + method: "POST", + url: "/organization-management/organizations/:organizationId/access", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + organizationId: z.string() + }), + response: { + 200: z.object({ + organizationMembership: OrgMembershipsSchema + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + const organizationMembership = await server.services.superAdmin.joinOrganization( + req.params.organizationId, + req.permission + ); + return { organizationMembership }; + } + }); }; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 8848f8638..b43e4e0a2 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -934,6 +934,36 @@ export const superAdminServiceFactory = ({ return organizationMembership; }; + const joinOrganization = async (orgId: string, actor: OrgServiceActor) => { + const serverAdmin = await userDAL.findById(actor.id); + + if (!serverAdmin) { + throw new NotFoundError({ message: "Could not find server admin user" }); + } + + const org = await orgDAL.findById(orgId); + + if (!org) { + throw new NotFoundError({ message: `Could not organization with ID "${orgId}"` }); + } + + const existingOrgMembership = await orgMembershipDAL.findOne({ userId: serverAdmin.id, orgId }); + + if (existingOrgMembership) { + throw new BadRequestError({ message: `You are already a part of the organization with ID ${orgId}` }); + } + + const orgMembership = await orgDAL.createMembership({ + userId: serverAdmin.id, + orgId: org.id, + role: OrgMembershipRole.Admin, + status: OrgMembershipStatus.Accepted, + isActive: true + }); + + return orgMembership; + }; + const resendOrgInvite = async ({ organizationId, membershipId }: TResendOrgInviteDTO, actor: OrgServiceActor) => { const orgMembership = await orgMembershipDAL.findOne({ id: membershipId, orgId: organizationId }); @@ -1124,6 +1154,7 @@ export const superAdminServiceFactory = ({ getEnvOverridesOrganized, deleteUsers, createOrganization, + joinOrganization, resendOrgInvite }; }; diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts index 880abc8b0..b7ab0ea6f 100644 --- a/frontend/src/hooks/api/admin/mutation.ts +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -223,3 +223,20 @@ export const useServerAdminResendOrgInvite = () => { } }); }; + +export const useServerAdminAccessOrg = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (orgId: string) => { + const { data } = await apiRequest.post( + `/api/v1/admin/organization-management/organizations/${orgId}/access` + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: organizationKeys.getUserOrganizations }); + queryClient.invalidateQueries({ queryKey: adminQueryKeys.getOrganizations() }); + } + }); +}; diff --git a/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx b/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx index ad7a0dcb8..c5f5c9872 100644 --- a/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx +++ b/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx @@ -10,12 +10,15 @@ import { faMagnifyingGlass, faPlus, faTrash, + faUserCheck, faUserMinus, + faUserPlus, faUsers, faUserXmark, faWarning } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate } from "@tanstack/react-router"; import { createNotification } from "@app/components/notifications"; import { @@ -42,6 +45,7 @@ import { Tooltip, Tr } from "@app/components/v2"; +import { useUser } from "@app/context"; import { OrgMembershipRole } from "@app/helpers/roles"; import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { @@ -49,6 +53,7 @@ import { useAdminDeleteOrganizationMembership, useAdminDeleteUser, useAdminGetOrganizations, + useServerAdminAccessOrg, useServerAdminResendOrgInvite } from "@app/hooks/api"; import { OrganizationWithProjects } from "@app/hooks/api/admin/types"; @@ -429,6 +434,9 @@ const OrganizationsPanelTable = ({ const [searchOrganizationsFilter, setSearchOrganizationsFilter] = useState(""); const [debouncedSearchTerm] = useDebounce(searchOrganizationsFilter, 500); + const { user } = useUser(); + + const navigate = useNavigate(); const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetOrganizations({ limit: 20, @@ -437,6 +445,31 @@ const OrganizationsPanelTable = ({ const isEmpty = !isPending && !data?.pages?.[0].length; + const { mutateAsync: accessOrganization } = useServerAdminAccessOrg(); + + const handleAccessOrg = async (orgId: string) => { + try { + await accessOrganization(orgId); + + navigate({ + to: "/login/select-organization", + search: { + org_id: orgId + } + }); + + createNotification({ + text: "Successfully joined organization", + type: "success" + }); + } catch { + createNotification({ + text: "Failed to join organization", + type: "error" + }); + } + }; + return ( <> - Name - Members - Projects + Name + Members + Projects @@ -462,19 +495,39 @@ const OrganizationsPanelTable = ({ {!isPending && data?.pages?.map((orgs) => orgs.map((org) => { + const isMember = org.members.find((member) => member.user.id === user.id); + return ( - - {org.name ? ( - org.name - ) : ( - Not Set - )} + +
+ {org.name ? ( +

{org.name}

+ ) : ( + Not Set + )} + {isMember && ( + +
+ + + Member + +
+
+ )} +
- +
{org.members.length} {org.members.length === 1 ? "Member" : "Members"} - + - + {org.projects.length} {org.projects.length === 1 ? "Project" : "Projects"} @@ -519,6 +572,17 @@ const OrganizationsPanelTable = ({ + {!isMember && ( + { + e.stopPropagation(); + handleAccessOrg(org.id); + }} + icon={} + > + Join Organization + + )} { e.stopPropagation();