feature: add ability to join org as super admin

This commit is contained in:
Scott Wilson
2025-09-02 13:33:28 -07:00
parent 4d6e12d6b2
commit 435bcd03d3
4 changed files with 154 additions and 12 deletions

View File

@@ -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 };
}
});
};

View File

@@ -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
};
};

View File

@@ -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() });
}
});
};

View File

@@ -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 (
<>
<Input
@@ -451,9 +484,9 @@ const OrganizationsPanelTable = ({
<Table>
<THead>
<Tr>
<Th className="w-5/12">Name</Th>
<Th className="w-5/12">Members</Th>
<Th className="w-5/12">Projects</Th>
<Th className="w-1/2">Name</Th>
<Th className="w-1/3">Members</Th>
<Th className="w-1/3">Projects</Th>
<Th className="w-5" />
</Tr>
</THead>
@@ -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 (
<Tr key={`org-${org.id}`} className="w-full">
<Td className="w-5/12">
{org.name ? (
org.name
) : (
<span className="text-mineshaft-400">Not Set</span>
)}
<Td className="w-1/2 max-w-0">
<div className="flex items-center gap-x-1.5">
{org.name ? (
<p className="truncate">{org.name}</p>
) : (
<span className="text-mineshaft-400">Not Set</span>
)}
{isMember && (
<Tooltip
className="text-center"
content="You are a member of this organization"
>
<div>
<Badge
variant="success"
className="flex items-center gap-x-1 whitespace-nowrap"
>
<FontAwesomeIcon icon={faUserCheck} />
<span>Member</span>
</Badge>
</div>
</Tooltip>
)}
</div>
</Td>
<Td className="w-5/12">
<Td className="w-1/3">
<div className="flex items-center">
{org.members.length} {org.members.length === 1 ? "Member" : "Members"}
<Tooltip content="View Members">
<Tooltip className="text-center" content="View Members">
<IconButton
ariaLabel="View Members"
variant="plain"
@@ -502,7 +555,7 @@ const OrganizationsPanelTable = ({
)}
</div>
</Td>
<Td className="w-5/12">
<Td className="w-1/3">
{org.projects.length} {org.projects.length === 1 ? "Project" : "Projects"}
</Td>
<Td>
@@ -519,6 +572,17 @@ const OrganizationsPanelTable = ({
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
{!isMember && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handleAccessOrg(org.id);
}}
icon={<FontAwesomeIcon icon={faUserPlus} />}
>
Join Organization
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();