mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 20:27:43 +00:00
feature: add ability to join org as super admin
This commit is contained in:
@@ -896,4 +896,34 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
return { organizationMembership };
|
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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -934,6 +934,36 @@ export const superAdminServiceFactory = ({
|
|||||||
return organizationMembership;
|
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 resendOrgInvite = async ({ organizationId, membershipId }: TResendOrgInviteDTO, actor: OrgServiceActor) => {
|
||||||
const orgMembership = await orgMembershipDAL.findOne({ id: membershipId, orgId: organizationId });
|
const orgMembership = await orgMembershipDAL.findOne({ id: membershipId, orgId: organizationId });
|
||||||
|
|
||||||
@@ -1124,6 +1154,7 @@ export const superAdminServiceFactory = ({
|
|||||||
getEnvOverridesOrganized,
|
getEnvOverridesOrganized,
|
||||||
deleteUsers,
|
deleteUsers,
|
||||||
createOrganization,
|
createOrganization,
|
||||||
|
joinOrganization,
|
||||||
resendOrgInvite
|
resendOrgInvite
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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() });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -10,12 +10,15 @@ import {
|
|||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faPlus,
|
faPlus,
|
||||||
faTrash,
|
faTrash,
|
||||||
|
faUserCheck,
|
||||||
faUserMinus,
|
faUserMinus,
|
||||||
|
faUserPlus,
|
||||||
faUsers,
|
faUsers,
|
||||||
faUserXmark,
|
faUserXmark,
|
||||||
faWarning
|
faWarning
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import {
|
||||||
@@ -42,6 +45,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
|
import { useUser } from "@app/context";
|
||||||
import { OrgMembershipRole } from "@app/helpers/roles";
|
import { OrgMembershipRole } from "@app/helpers/roles";
|
||||||
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
@@ -49,6 +53,7 @@ import {
|
|||||||
useAdminDeleteOrganizationMembership,
|
useAdminDeleteOrganizationMembership,
|
||||||
useAdminDeleteUser,
|
useAdminDeleteUser,
|
||||||
useAdminGetOrganizations,
|
useAdminGetOrganizations,
|
||||||
|
useServerAdminAccessOrg,
|
||||||
useServerAdminResendOrgInvite
|
useServerAdminResendOrgInvite
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { OrganizationWithProjects } from "@app/hooks/api/admin/types";
|
import { OrganizationWithProjects } from "@app/hooks/api/admin/types";
|
||||||
@@ -429,6 +434,9 @@ const OrganizationsPanelTable = ({
|
|||||||
const [searchOrganizationsFilter, setSearchOrganizationsFilter] = useState("");
|
const [searchOrganizationsFilter, setSearchOrganizationsFilter] = useState("");
|
||||||
const [debouncedSearchTerm] = useDebounce(searchOrganizationsFilter, 500);
|
const [debouncedSearchTerm] = useDebounce(searchOrganizationsFilter, 500);
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } =
|
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } =
|
||||||
useAdminGetOrganizations({
|
useAdminGetOrganizations({
|
||||||
limit: 20,
|
limit: 20,
|
||||||
@@ -437,6 +445,31 @@ const OrganizationsPanelTable = ({
|
|||||||
|
|
||||||
const isEmpty = !isPending && !data?.pages?.[0].length;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
@@ -451,9 +484,9 @@ const OrganizationsPanelTable = ({
|
|||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th className="w-5/12">Name</Th>
|
<Th className="w-1/2">Name</Th>
|
||||||
<Th className="w-5/12">Members</Th>
|
<Th className="w-1/3">Members</Th>
|
||||||
<Th className="w-5/12">Projects</Th>
|
<Th className="w-1/3">Projects</Th>
|
||||||
<Th className="w-5" />
|
<Th className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
@@ -462,19 +495,39 @@ const OrganizationsPanelTable = ({
|
|||||||
{!isPending &&
|
{!isPending &&
|
||||||
data?.pages?.map((orgs) =>
|
data?.pages?.map((orgs) =>
|
||||||
orgs.map((org) => {
|
orgs.map((org) => {
|
||||||
|
const isMember = org.members.find((member) => member.user.id === user.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr key={`org-${org.id}`} className="w-full">
|
<Tr key={`org-${org.id}`} className="w-full">
|
||||||
<Td className="w-5/12">
|
<Td className="w-1/2 max-w-0">
|
||||||
{org.name ? (
|
<div className="flex items-center gap-x-1.5">
|
||||||
org.name
|
{org.name ? (
|
||||||
) : (
|
<p className="truncate">{org.name}</p>
|
||||||
<span className="text-mineshaft-400">Not Set</span>
|
) : (
|
||||||
)}
|
<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>
|
||||||
<Td className="w-5/12">
|
<Td className="w-1/3">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{org.members.length} {org.members.length === 1 ? "Member" : "Members"}
|
{org.members.length} {org.members.length === 1 ? "Member" : "Members"}
|
||||||
<Tooltip content="View Members">
|
<Tooltip className="text-center" content="View Members">
|
||||||
<IconButton
|
<IconButton
|
||||||
ariaLabel="View Members"
|
ariaLabel="View Members"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
@@ -502,7 +555,7 @@ const OrganizationsPanelTable = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td className="w-5/12">
|
<Td className="w-1/3">
|
||||||
{org.projects.length} {org.projects.length === 1 ? "Project" : "Projects"}
|
{org.projects.length} {org.projects.length === 1 ? "Project" : "Projects"}
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
@@ -519,6 +572,17 @@ const OrganizationsPanelTable = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent sideOffset={2} align="end">
|
<DropdownMenuContent sideOffset={2} align="end">
|
||||||
|
{!isMember && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleAccessOrg(org.id);
|
||||||
|
}}
|
||||||
|
icon={<FontAwesomeIcon icon={faUserPlus} />}
|
||||||
|
>
|
||||||
|
Join Organization
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|||||||
Reference in New Issue
Block a user