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 (
<>
-
@@ -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 (
Name
- Members
- Projects
+ Name
+ Members
+ Projects
{org.name}
+ ) : ( + Not Set + )} + {isMember && ( +