Merge pull request #4701 from Infisical/feat/sub-org

feat: sub organization
This commit is contained in:
Scott Wilson
2025-10-21 14:18:48 -07:00
committed by GitHub
200 changed files with 5621 additions and 2275 deletions
@@ -24,7 +24,7 @@ const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: Ico
[ProjectType.KMS]: { label: "Project", icon: faCube },
[ProjectType.PAM]: { label: "Project", icon: faCube },
[ProjectType.SecretScanning]: { label: "Project", icon: faCube },
namespace: { label: "Namespace", icon: faCubes },
namespace: { label: "Sub-Organization", icon: faCubes },
instance: { label: "Server", icon: faServer }
};
+13
View File
@@ -24,6 +24,8 @@ apiRequest.interceptors.request.use((config) => {
const token = getAuthToken();
const providerAuthToken = SecurityClient.getProviderAuthToken();
const params = new URLSearchParams(window.location.search);
if (config.headers) {
if (signupTempToken) {
// eslint-disable-next-line no-param-reassign
@@ -38,6 +40,17 @@ apiRequest.interceptors.request.use((config) => {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${providerAuthToken}`;
}
const rootOrgHeader = config.headers.get("x-root-org");
if (rootOrgHeader) {
config.headers.delete("x-root-org");
} else {
const subOrganization = params.get("subOrganization");
if (subOrganization) {
config.headers.set("x-infisical-org", subOrganization);
}
}
}
return config;
-4
View File
@@ -33,10 +33,6 @@ export const ROUTE_PATHS = Object.freeze({
"/organization/secret-sharing",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/"
),
SecretSharingSettings: setRoute(
"/organization/secret-sharing/settings",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings"
),
SettingsPage: setRoute(
"/organization/settings",
"/_authenticate/_inject-org-details/_org-layout/organization/settings/"
@@ -62,7 +62,8 @@ export enum OrgPermissionSubjects {
SecretShare = "secret-share",
GithubOrgSync = "github-org-sync",
GithubOrgSyncManual = "github-org-sync-manual",
MachineIdentityAuthTemplate = "machine-identity-auth-template"
MachineIdentityAuthTemplate = "machine-identity-auth-template",
SubOrganization = "sub-organization"
}
export enum OrgPermissionAdminConsoleAction {
@@ -112,6 +113,11 @@ export enum OrgPermissionGroupActions {
RemoveMembers = "remove-members"
}
export enum OrgPermissionSubOrgActions {
Create = "create",
DirectAccess = "direct-access"
}
export type AppConnectionSubjectFields = {
connectionId: string;
};
@@ -151,6 +157,7 @@ export type OrgPermissionSet =
| OrgPermissionSubjects.AppConnections
| (ForcedSubject<OrgPermissionSubjects.AppConnections> & AppConnectionSubjectFields)
)
];
]
| [OrgPermissionSubOrgActions, OrgPermissionSubjects.SubOrganization];
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
@@ -1,5 +1,6 @@
import { useMemo } from "react";
import { useSuspenseQuery } from "@tanstack/react-query";
import { useRouteContext } from "@tanstack/react-router";
import { useRouteContext, useSearch } from "@tanstack/react-router";
import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries";
@@ -9,11 +10,29 @@ export const useOrganization = () => {
select: (el) => el.organizationId
});
const subOrganization = useSearch({
strict: false,
select: (el) => el?.subOrganization
});
const { data: currentOrg } = useSuspenseQuery({
queryKey: organizationKeys.getOrgById(organizationId),
queryKey: organizationKeys.getOrgById(organizationId, subOrganization || "root"),
queryFn: () => fetchOrganizationById(organizationId),
staleTime: Infinity
});
return { currentOrg };
const org = useMemo(
() => ({
currentOrg: {
...currentOrg,
id: currentOrg?.subOrganization?.id || currentOrg?.id,
parentOrgId: currentOrg.id
},
isSubOrganization: Boolean(currentOrg.subOrganization),
isRootOrganization: !currentOrg.subOrganization
}),
[currentOrg, subOrganization]
);
return org;
};
@@ -24,7 +24,7 @@ export const withPermission = <T extends object, J extends TOrgPermission>(
// akhilmhdh: Set as any due to casl/react ts type bug
// REASON: casl due to its type checking can't seem to union even if union intersection is applied
if (permission.cannot(action as any, subject)) {
if (permission.cannot(action as any, subject as any)) {
return (
<div
className={twMerge(
@@ -20,6 +20,7 @@ export type Identity = {
createdAt: string;
updatedAt: string;
isInstanceAdmin?: boolean;
orgId: string;
};
export type IdentityAccessToken = {
+1
View File
@@ -48,6 +48,7 @@ export * from "./sshCertificateTemplates";
export * from "./sshHost";
export * from "./sshHostGroup";
export * from "./ssoConfig";
export * from "./subOrganizations";
export * from "./subscriptions";
export * from "./tags";
export * from "./trustedIps";
@@ -0,0 +1,6 @@
export { useCreateOrgIdentityMembership, useDeleteOrgIdentityMembership } from "./mutation";
export type {
TCreateOrgIdentityMembershipDTO,
TDeleteOrgIdentityMembershipDTO,
TOrgIdentityMembership
} from "./types";
@@ -0,0 +1,41 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { identitiesKeys } from "../identities";
import {
TCreateOrgIdentityMembershipDTO,
TDeleteOrgIdentityMembershipDTO,
TOrgIdentityMembership
} from "./types";
export const useCreateOrgIdentityMembership = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ identityId, roles }: TCreateOrgIdentityMembershipDTO) => {
const { data } = await apiRequest.post<{ identityMembership: TOrgIdentityMembership }>(
`/api/v1/organization/identity-memberships/${identityId}`,
{ roles }
);
return data.identityMembership;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentities({ search: {} }) });
}
});
};
export const useDeleteOrgIdentityMembership = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ identityId }: TDeleteOrgIdentityMembershipDTO) => {
const { data } = await apiRequest.delete<{ identityMembership: TOrgIdentityMembership }>(
`/api/v1/organization/identity-memberships/${identityId}`
);
return data.identityMembership;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentities({ search: {} }) });
}
});
};
@@ -0,0 +1,32 @@
export enum TemporaryPermissionMode {
Relative = "relative"
}
export type TOrgIdentityMembership = {
id: string;
orgId: string;
identityId: string;
createdAt: string;
updatedAt: string;
};
export type TCreateOrgIdentityMembershipDTO = {
identityId: string;
roles: Array<
| {
role: string;
isTemporary?: false;
}
| {
role: string;
isTemporary: true;
temporaryMode: TemporaryPermissionMode;
temporaryRange: string;
temporaryAccessStartTime: string;
}
>;
};
export type TDeleteOrgIdentityMembershipDTO = {
identityId: string;
};
@@ -6,6 +6,7 @@ export {
useDeleteOrgById,
useDeleteOrgPmtMethod,
useDeleteOrgTaxId,
useGetAvailableOrgIdentities,
useGetIdentityMembershipOrgs,
useGetOrganizationGroups,
useGetOrganizations,
@@ -42,7 +42,9 @@ export const organizationKeys = {
[...organizationKeys.getOrgIdentityMemberships(orgId), params] as const,
getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const,
getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const,
getOrgById: (orgId: string) => ["organization", { orgId }]
getOrgById: (orgId: string, subOrg?: string) => ["organization", { orgId, subOrg }],
getAvailableIdentities: () => ["available-identities"],
getAvailableUsers: () => ["available-users"]
};
export const fetchOrganizations = async () => {
@@ -64,7 +66,9 @@ export const useGetOrganizations = () => {
export const fetchOrganizationById = async (id: string) => {
const {
data: { organization }
} = await apiRequest.get<{ organization: Organization }>(`/api/v1/organization/${id}`);
} = await apiRequest.get<{
organization: Organization & { subOrganization?: { id: string; name: string } };
}>(`/api/v1/organization/${id}`);
return organization;
};
@@ -572,3 +576,29 @@ export const useGetOrgIntegrationAuths = <TData = IntegrationAuth[],>(
select
});
};
export const useGetAvailableOrgIdentities = (enabled = true) =>
useQuery({
queryKey: organizationKeys.getAvailableIdentities(),
queryFn: async () => {
const { data } = await apiRequest.get<{ identities: { name: string; id: string }[] }>(
"/api/v1/organization/identities/available"
);
return data.identities;
},
enabled
});
export const useGetAvailableOrgUsers = (enabled = true) =>
useQuery({
queryKey: organizationKeys.getAvailableUsers(),
queryFn: async () => {
const { data } = await apiRequest.get<{
users: { username: string; id: string; firstName: string; lastName: string }[];
}>("/api/v1/organization/users/available");
return data.users;
},
enabled
});
@@ -0,0 +1,8 @@
export { useCreateSubOrganization, useUpdateSubOrganization } from "./mutations";
export { subOrganizationsQuery } from "./queries";
export type {
TCreateSubOrganizationDTO,
TListSubOrganizationsDTO,
TSubOrganization,
TUpdateSubOrganizationDTO
} from "./types";
@@ -0,0 +1,41 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { subOrganizationsQuery } from "./queries";
import { TCreateSubOrganizationDTO, TSubOrganization, TUpdateSubOrganizationDTO } from "./types";
export const useCreateSubOrganization = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (dto: TCreateSubOrganizationDTO) => {
const { data } = await apiRequest.post<{ organization: TSubOrganization }>(
"/api/v1/sub-organizations",
dto,
{
headers: { "x-root-org": "discard" } // akhi/scott: this just tells the request to use the root org ID header
}
);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: subOrganizationsQuery.allKey() });
}
});
};
export const useUpdateSubOrganization = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ subOrgId, name }: TUpdateSubOrganizationDTO) => {
const { data } = await apiRequest.patch<{ organization: TSubOrganization }>(
`/api/v1/sub-organizations/${subOrgId}`,
{ name }
);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: subOrganizationsQuery.allKey() });
}
});
};
@@ -0,0 +1,28 @@
import { queryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TListSubOrganizationsDTO, TSubOrganization } from "./types";
export const subOrganizationsQuery = {
allKey: () => ["sub-organizations"] as const,
listKey: (params?: TListSubOrganizationsDTO) =>
[...subOrganizationsQuery.allKey(), "list", params] as const,
list: (params: TListSubOrganizationsDTO) =>
queryOptions({
queryKey: subOrganizationsQuery.listKey(params),
queryFn: async () => {
const { data } = await apiRequest.get<{ organizations: TSubOrganization[] }>(
"/api/v1/sub-organizations",
{
params: {
limit: params.limit,
offset: params.offset,
isAccessible: params.isAccessible
}
}
);
return data.organizations;
}
})
};
@@ -0,0 +1,23 @@
export type TSubOrganization = {
id: string;
name: string;
slug: string;
createdAt: string;
updatedAt: string;
parentOrgId: string;
};
export type TCreateSubOrganizationDTO = {
name: string;
};
export type TListSubOrganizationsDTO = {
limit?: number;
offset?: number;
isAccessible?: boolean;
};
export type TUpdateSubOrganizationDTO = {
subOrgId: string;
name: string;
};
@@ -13,6 +13,7 @@ export type SubscriptionPlan = {
customRateLimits: boolean;
pitRecovery: boolean;
githubOrgSync: boolean;
subOrganization?: boolean;
ipAllowlisting: boolean;
rbac: boolean;
secretVersioning: boolean;
@@ -6,12 +6,15 @@ import {
faBook,
faCaretDown,
faCheck,
faChevronRight,
faCubes,
faEnvelope,
faExclamationTriangle,
faGlobe,
faInfinity,
faInfo,
faInfoCircle,
faPlus,
faServer,
faSignOut,
faToolbox,
@@ -19,7 +22,7 @@ import {
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Link, useLocation, useNavigate, useRouter, useRouterState } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
@@ -34,6 +37,9 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownSubMenu,
DropdownSubMenuContent,
DropdownSubMenuTrigger,
IconButton,
Modal,
ModalContent,
@@ -44,15 +50,22 @@ import { envConfig } from "@app/config/env";
import { useOrganization, useSubscription, useUser } from "@app/context";
import { isInfisicalCloud } from "@app/helpers/platform";
import { useToggle } from "@app/hooks";
import { projectKeys, useGetOrganizations, useGetOrgTrialUrl, useLogoutUser } from "@app/hooks/api";
import {
projectKeys,
subOrganizationsQuery,
useGetOrganizations,
useGetOrgTrialUrl,
useLogoutUser
} from "@app/hooks/api";
import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { getAuthToken } from "@app/hooks/api/reactQuery";
import { SubscriptionPlan } from "@app/hooks/api/types";
import { Organization, SubscriptionPlan } from "@app/hooks/api/types";
import { AuthMethod } from "@app/hooks/api/users/types";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel";
import { NewSubOrganizationForm } from "./NewSubOrganizationForm";
import { NotificationDropdown } from "./NotificationDropdown";
const getPlan = (subscription: SubscriptionPlan) => {
@@ -119,10 +132,18 @@ export const INFISICAL_SUPPORT_OPTIONS = [
export const Navbar = () => {
const { user } = useUser();
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
const [showAdminsModal, setShowAdminsModal] = useState(false);
const [showSubOrgForm, setShowSubOrgForm] = useState(false);
const [showCardDeclinedModal, setShowCardDeclinedModal] = useState(false);
const subOrgQuery = subOrganizationsQuery.list({ limit: 500, isAccessible: true });
const { data: subOrganizations = [] } = useQuery({
...subOrgQuery,
enabled: Boolean(subscription.subOrganization)
});
useEffect(() => {
if (subscription?.cardDeclined && !sessionStorage.getItem("paymentFailed")) {
sessionStorage.setItem("paymentFailed", "true");
@@ -137,6 +158,7 @@ export const Navbar = () => {
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
const router = useRouter();
const queryClient = useQueryClient();
const [isOrgSelectOpen, setIsOrgSelectOpen] = useState(false);
const location = useLocation();
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
@@ -161,6 +183,7 @@ export const Navbar = () => {
}
await router.invalidate();
await navigateUserToOrg(navigate, orgId);
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey });
};
const { mutateAsync } = useGetOrgTrialUrl();
@@ -206,6 +229,33 @@ export const Navbar = () => {
const isOrgScope = location.pathname.startsWith("/organization"); // TODO: scott/akhil is this adequate?
const handleOrgNav = async (org: Organization) => {
if (currentOrg?.id === org.id) return;
if (org.authEnforced) {
// org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO
await logout.mutateAsync();
if (org.orgAuthMethod === AuthMethod.OIDC) {
window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`);
} else {
window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`);
}
window.close();
return;
}
if (org.googleSsoAuthEnforced) {
await logout.mutateAsync();
window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`);
window.close();
return;
}
handleOrgChange(org?.id);
};
return (
<div className="z-10 flex min-h-12 items-center bg-mineshaft-900 px-4 pt-1">
<div className="mr-auto flex items-center overflow-hidden">
@@ -235,38 +285,45 @@ export const Navbar = () => {
) : (
<>
<div className="flex items-center overflow-hidden">
<DropdownMenu modal={false}>
<Link className="overflow-hidden" to="/organization/projects">
<div className="group flex cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100 hover:text-primary">
<Badge
variant="org"
className={twMerge(
"max-w-full min-w-0 cursor-pointer text-sm",
!isOrgScope &&
"bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline"
)}
>
<FontAwesomeIcon icon={faGlobe} />
<p className="truncate">{currentOrg?.name}</p>
</Badge>
<div className="mr-1 rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline!">
{getPlan(subscription)}
</div>
{subscription.cardDeclined && (
<Tooltip
content={`Your payment could not be processed${subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. Please update your payment method to continue enjoying premium features.`}
className="max-w-xs"
>
<div className="flex items-center">
<FontAwesomeIcon
icon={faExclamationTriangle}
className="animate-pulse cursor-help text-xs text-primary-400"
/>
</div>
</Tooltip>
<DropdownMenu modal={false} open={isOrgSelectOpen} onOpenChange={setIsOrgSelectOpen}>
<div className="group flex cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100 hover:text-primary">
<Badge
onClick={async () => {
navigate({
to: "/organization/projects",
search: (search) => ({ ...search, subOrganization: undefined })
});
if (isSubOrganization) {
await router.invalidate({ sync: true }).catch(() => null);
}
}}
variant="org"
className={twMerge(
"max-w-full min-w-0 cursor-pointer text-sm",
(!isOrgScope || isSubOrganization) &&
"bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline"
)}
>
<FontAwesomeIcon icon={faGlobe} />
<p className="truncate">{currentOrg?.name}</p>
</Badge>
<div className="mr-1 rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline!">
{getPlan(subscription)}
</div>
</Link>
{subscription.cardDeclined && (
<Tooltip
content={`Your payment could not be processed${subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. Please update your payment method to continue enjoying premium features.`}
className="max-w-xs"
>
<div className="flex items-center">
<FontAwesomeIcon
icon={faExclamationTriangle}
className="animate-pulse cursor-help text-xs text-primary-400"
/>
</div>
</Tooltip>
)}
</div>
<DropdownMenuTrigger asChild>
<div>
<IconButton
@@ -280,58 +337,95 @@ export const Navbar = () => {
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
align="center"
side="bottom"
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "220px" }}
>
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">
organizations
Organizations
</div>
{orgs?.map((org) => {
if (
subscription.subOrganization &&
(org.id === currentOrg?.id || org.id === currentOrg?.parentOrgId)
) {
return (
<DropdownSubMenu key={`${org.id}-sub-orgs`}>
<DropdownSubMenuTrigger
onClick={() => {
setIsOrgSelectOpen(false);
handleOrgNav(org);
}}
className="cursor-pointer font-normal"
>
<div className="flex w-full max-w-48 cursor-pointer items-center gap-x-2">
{currentOrg?.id === org.id && (
<FontAwesomeIcon icon={faCheck} className="shrink-0 text-primary" />
)}
<p className="truncate">{org.name}</p>
<FontAwesomeIcon className="ml-auto shrink-0" icon={faChevronRight} />
</div>
</DropdownSubMenuTrigger>
<DropdownSubMenuContent
sideOffset={8}
alignOffset={-24}
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "220px" }}
>
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">
Sub-Organizations
</div>
{subOrganizations.map((subOrg) => (
<DropdownMenuItem
onClick={async () => {
navigate({
to: "/organization/projects",
search: (prev) => ({ ...prev, subOrganization: subOrg.name })
});
await router.invalidate({ sync: true }).catch(() => null);
}}
className="cursor-pointer font-normal"
key={subOrg.id}
>
<div className="flex w-full max-w-48 cursor-pointer items-center gap-x-2">
{currentOrg?.id === subOrg.id && (
<FontAwesomeIcon
icon={faCheck}
className="shrink-0 text-primary"
/>
)}
<p className="truncate">{subOrg.name}</p>
</div>
</DropdownMenuItem>
))}
{Boolean(subOrganizations.length) && (
<div className="mt-1 h-1 border-t border-mineshaft-600" />
)}
<DropdownMenuItem
className="cursor-pointer"
icon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => setShowSubOrgForm(true)}
>
New Sub-Organization
</DropdownMenuItem>
</DropdownSubMenuContent>
</DropdownSubMenu>
);
}
return (
<DropdownMenuItem key={org.id}>
<Button
onClick={async () => {
if (currentOrg?.id === org.id) return;
if (org.authEnforced) {
// org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO
await logout.mutateAsync();
if (org.orgAuthMethod === AuthMethod.OIDC) {
window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`);
} else {
window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`);
}
window.close();
return;
}
if (org.googleSsoAuthEnforced) {
await logout.mutateAsync();
window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`);
window.close();
return;
}
handleOrgChange(org?.id);
}}
variant="plain"
colorSchema="secondary"
size="xs"
className="flex w-full items-center justify-start p-0 font-normal"
leftIcon={
currentOrg?.id === org.id && (
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
)
}
>
<div className="flex w-full max-w-[150px] items-center justify-between truncate">
{org.name}
</div>
</Button>
<DropdownMenuItem
onClick={() => handleOrgNav(org)}
className="cursor-pointer font-normal"
key={org.id}
>
<div className="flex w-full max-w-48 cursor-pointer items-center gap-x-2">
{currentOrg?.id === org.id && (
<FontAwesomeIcon icon={faCheck} className="shrink-0 text-primary" />
)}
<p className="truncate">{org.name}</p>
</div>
</DropdownMenuItem>
);
})}
@@ -345,6 +439,78 @@ export const Navbar = () => {
</DropdownMenuContent>
</DropdownMenu>
</div>
{currentOrg.subOrganization && (
<>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
<DropdownMenu modal={false}>
<Link className="overflow-hidden" to="/organization/projects">
<Badge
variant="namespace"
className={twMerge(
"flex max-w-full min-w-0 cursor-pointer text-sm",
!isOrgScope &&
"bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline"
)}
>
<FontAwesomeIcon icon={faCubes} />
<p className="truncate">{currentOrg.subOrganization.name}</p>
</Badge>
</Link>
<DropdownMenuTrigger asChild>
<div>
<IconButton
variant="plain"
colorSchema="secondary"
ariaLabel="switch-org"
className="px-2 py-1"
>
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
</IconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="center"
side="bottom"
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "220px" }}
>
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">
Sub-Organizations
</div>
{subOrganizations.map((subOrg) => (
<DropdownMenuItem
onClick={async () => {
navigate({
to: "/organization/projects",
search: (prev) => ({ ...prev, subOrganization: subOrg.name })
});
await router.invalidate({ sync: true }).catch(() => null);
}}
className="cursor-pointer font-normal"
key={subOrg.id}
>
<div className="flex w-full max-w-48 cursor-pointer items-center gap-x-2">
{currentOrg?.id === subOrg.id && (
<FontAwesomeIcon icon={faCheck} className="shrink-0 text-primary" />
)}
<p className="truncate">{subOrg.name}</p>
</div>
</DropdownMenuItem>
))}
{Boolean(subOrganizations.length) && (
<div className="mt-1 h-1 border-t border-mineshaft-600" />
)}
<DropdownMenuItem
className="cursor-pointer"
icon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => setShowSubOrgForm(true)}
>
New Sub-Organization
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
)}
{!isOrgScope && (
<>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
@@ -517,6 +683,7 @@ export const Navbar = () => {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Modal isOpen={showCardDeclinedModal} onOpenChange={setShowCardDeclinedModal}>
<ModalContent
title={
@@ -560,6 +727,20 @@ export const Navbar = () => {
</div>
</ModalContent>
</Modal>
<Modal isOpen={showSubOrgForm} onOpenChange={setShowSubOrgForm}>
<ModalContent
title="Create Sub-Organizations"
subTitle="Define a new sub-organization under your current organization."
>
<div className="mb-2">
<NewSubOrganizationForm
onClose={() => {
setShowSubOrgForm(false);
}}
/>
</div>
</ModalContent>
</Modal>
<Modal isOpen={showAdminsModal} onOpenChange={setShowAdminsModal}>
<ModalContent title="Server Administrators" subTitle="View all server administrators">
<div className="mb-2">
@@ -0,0 +1,94 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate, useRouter } from "@tanstack/react-router";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input } from "@app/components/v2";
import { useCreateSubOrganization } from "@app/hooks/api";
import { slugSchema } from "@app/lib/schemas";
type ContentProps = {
onClose: () => void;
};
const AddOrgSchema = z.object({
name: slugSchema()
});
type FormData = z.infer<typeof AddOrgSchema>;
export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
const createSubOrg = useCreateSubOrganization();
const {
handleSubmit,
control,
formState: { isSubmitting }
} = useForm({
defaultValues: {
name: ""
},
resolver: zodResolver(AddOrgSchema)
});
const navigate = useNavigate();
const router = useRouter();
const onSubmit = async ({ name }: FormData) => {
try {
const { organization } = await createSubOrg.mutateAsync({
name
});
createNotification({
type: "success",
text: "Successfully created sub organization"
});
onClose();
navigate({
to: "/organization/projects",
search: (prev) => ({ ...prev, subOrganization: organization.name })
});
await router.invalidate({ sync: true }).catch(() => null);
} catch {
createNotification({
text: "Failed to create sub organization",
type: "error"
});
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
helperText="Must be slug-friendly"
errorText={error?.message}
label="Name"
>
<Input autoFocus value={value} onChange={onChange} placeholder="example-team" />
</FormControl>
)}
control={control}
name="name"
/>
<div className="flex w-full gap-4 pt-4">
<Button
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
colorSchema="secondary"
>
Add Sub-Organization
</Button>
<Button onClick={() => onClose()} variant="plain" colorSchema="secondary">
Cancel
</Button>
</div>
</form>
);
};
@@ -3,6 +3,7 @@ import { motion } from "framer-motion";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import { Tab, TabList, Tabs } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { usePopUp } from "@app/hooks";
type Props = {
@@ -10,10 +11,13 @@ type Props = {
};
export const OrgNavBar = ({ isHidden }: Props) => {
const { isRootOrganization } = useOrganization();
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
const { pathname } = useLocation();
const variant = isRootOrganization ? "org" : "namespace";
return (
<>
{!isHidden && (
@@ -30,28 +34,28 @@ export const OrgNavBar = ({ isHidden }: Props) => {
<TabList className="border-b-0">
<Link to="/organization/projects">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
<Tab variant={variant} value={isActive ? "selected" : ""}>
Overview
</Tab>
)}
</Link>
<Link to="/organization/app-connections">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
<Tab variant={variant} value={isActive ? "selected" : ""}>
App Connections
</Tab>
)}
</Link>
<Link to="/organization/networking">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
<Tab variant={variant} value={isActive ? "selected" : ""}>
Networking
</Tab>
)}
</Link>
<Link to="/organization/secret-sharing">
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""} variant="org">
<Tab value={isActive ? "selected" : ""} variant={variant}>
Secret Sharing
</Tab>
)}
@@ -59,7 +63,7 @@ export const OrgNavBar = ({ isHidden }: Props) => {
<Link to="/organization/access-management">
{({ isActive }) => (
<Tab
variant="org"
variant={variant}
value={
isActive ||
pathname.match(
@@ -75,21 +79,23 @@ export const OrgNavBar = ({ isHidden }: Props) => {
</Link>
<Link to="/organization/audit-logs">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
<Tab variant={variant} value={isActive ? "selected" : ""}>
Audit Logs
</Tab>
)}
</Link>
<Link to="/organization/billing">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
Usage & Billing
</Tab>
)}
</Link>
{isRootOrganization && (
<Link to="/organization/billing">
{({ isActive }) => (
<Tab variant={variant} value={isActive ? "selected" : ""}>
Usage & Billing
</Tab>
)}
</Link>
)}
<Link to="/organization/settings">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
<Tab variant={variant} value={isActive ? "selected" : ""}>
Settings
</Tab>
)}
@@ -171,9 +171,21 @@ export const ProjectSelect = () => {
to: getProjectHomePage(workspace.type, workspace.environments),
params: {
projectId: workspace.id
},
search: {
subOrganization: currentOrg?.subOrganization?.name
}
});
window.location.assign(url.to.replaceAll("$projectId", workspace.id));
const urlInstance = new URL(
`${window.location.origin}/${url.to.replaceAll("$projectId", workspace.id)}`
);
if (currentOrg?.subOrganization) {
urlInstance.searchParams.set(
"subOrganization",
currentOrg.subOrganization.name
);
}
window.location.assign(urlInstance);
}}
icon={
currentWorkspace?.id === workspace.id && (
@@ -24,7 +24,7 @@ import { OrgGroupsTab, OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from "
export const AccessManagementPage = () => {
const { t } = useTranslation();
const { permission } = useOrgPermission();
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
const navigate = useNavigate({
from: ROUTE_PATHS.Organization.AccessControlPage.path
@@ -82,7 +82,7 @@ export const AccessManagementPage = () => {
</Helmet>
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="org"
scope={isSubOrganization ? "namespace" : "org"}
title="Access Control"
description="Manage fine-grained access for users, groups, roles, and identities within your organization resources."
/>
@@ -116,7 +116,11 @@ export const AccessManagementPage = () => {
{tabSections
.filter((el) => !el.isHidden)
.map((el) => (
<Tab variant="org" value={el.key} key={`org-access-tab-${el.key}`}>
<Tab
variant={isSubOrganization ? "namespace" : "org"}
value={el.key}
key={`org-access-tab-${el.key}`}
>
{el.label}
</Tab>
))}
@@ -0,0 +1,129 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate } from "@tanstack/react-router";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FilterableSelect, FormControl } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useGetAvailableOrgIdentities, useGetOrgRoles } from "@app/hooks/api";
import { useCreateOrgIdentityMembership } from "@app/hooks/api/orgIdentityMembership";
const schema = z
.object({
identity: z.object({ name: z.string(), id: z.string() }),
role: z.object({ name: z.string(), slug: z.string() })
})
.required();
export type FormData = z.infer<typeof schema>;
type Props = {
onClose: () => void;
};
export const IdentityLinkForm = ({ onClose }: Props) => {
const navigate = useNavigate();
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { data: roles } = useGetOrgRoles(orgId);
const { mutateAsync: createMutateAsync } = useCreateOrgIdentityMembership();
const { data: rootOrgIdentities, isPending: isRootOrgLoading } = useGetAvailableOrgIdentities();
const {
control,
handleSubmit,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {}
});
const onFormSubmit = async ({ identity, role }: FormData) => {
try {
await createMutateAsync({
identityId: identity.id,
roles: [{ role: role.slug, isTemporary: false }]
});
createNotification({
text: "Successfully linked identity",
type: "success"
});
navigate({
to: "/organization/identities/$identityId",
params: {
identityId: identity.id
}
});
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to link identity";
createNotification({
text,
type: "error"
});
}
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="identity"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl label="Identity" errorText={error?.message} isError={Boolean(error)}>
<FilterableSelect
value={value}
onChange={onChange}
placeholder="Select identity..."
options={rootOrgIdentities}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
isLoading={isRootOrgLoading}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="role"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
label="Role"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<FilterableSelect
value={value}
onChange={onChange}
options={roles}
placeholder="Select role..."
getOptionValue={(option) => option.slug}
getOptionLabel={(option) => option.name}
// menuPortalTarget={document.body}
/>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Link
</Button>
<Button colorSchema="secondary" variant="plain" onClick={() => onClose()}>
Cancel
</Button>
</div>
</form>
);
};
@@ -53,6 +53,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
const orgId = currentOrg?.id || "";
const { data: roles } = useGetOrgRoles(orgId);
const isOrgIdentity = popUp?.identity?.data ? orgId === popUp?.identity?.data?.orgId : true;
const { mutateAsync: createMutateAsync } = useCreateIdentity();
const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
@@ -113,6 +114,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
name: string;
role: string;
hasDeleteProtection: boolean;
orgId: string;
};
if (identity) {
@@ -196,16 +198,23 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
title={`${popUp?.identity?.data ? "Update" : "Create"} Identity`}
>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="Machine 1" />
</FormControl>
)}
/>
{isOrgIdentity && (
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
className="mb-4"
label="Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Machine 1" />
</FormControl>
)}
/>
)}
<Controller
control={control}
name="role"
@@ -214,7 +223,6 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
label={`${popUp?.identity?.data ? "Update" : ""} Role`}
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<FilterableSelect
placeholder="Select role..."
@@ -227,24 +235,26 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
</FormControl>
)}
/>
<Controller
control={control}
name="hasDeleteProtection"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error)}>
<Switch
className="mr-2 ml-0 bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
containerClassName="flex-row-reverse w-fit"
id="delete-protection-enabled"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
<p>Delete Protection {value ? "Enabled" : "Disabled"}</p>
</Switch>
</FormControl>
)}
/>
{isOrgIdentity && (
<Controller
control={control}
name="hasDeleteProtection"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error)}>
<Switch
className="mr-2 ml-0 bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
containerClassName="flex-row-reverse w-fit"
id="delete-protection-enabled"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
<p>Delete Protection {value ? "Enabled" : "Disabled"}</p>
</Switch>
</FormControl>
)}
/>
)}
<div>
<FormLabel label="Metadata" />
</div>
@@ -1,10 +1,15 @@
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
import {
faArrowUpRightFromSquare,
faBookOpen,
faLink,
faPlus
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { Button, DeleteActionModal, Modal, ModalContent } from "@app/components/v2";
import {
OrgPermissionIdentityActions,
OrgPermissionSubjects,
@@ -19,6 +24,7 @@ import { usePopUp } from "@app/hooks/usePopUp";
import { IdentityAuthTemplateModal } from "./IdentityAuthTemplateModal";
import { IdentityAuthTemplatesTable } from "./IdentityAuthTemplatesTable";
import { IdentityLinkForm } from "./IdentityLinkForm";
import { IdentityModal } from "./IdentityModal";
import { IdentityTable } from "./IdentityTable";
import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal";
@@ -27,7 +33,7 @@ import { MachineAuthTemplateUsagesModal } from "./MachineAuthTemplateUsagesModal
export const IdentitySection = withPermission(
() => {
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
const orgId = currentOrg?.id || "";
const { mutateAsync: deleteMutateAsync } = useDeleteIdentity();
@@ -43,7 +49,8 @@ export const IdentitySection = withPermission(
"createTemplate",
"editTemplate",
"deleteTemplate",
"viewUsages"
"viewUsages",
"linkIdentity"
] as const);
const isMoreIdentitiesAllowed = subscription?.identityLimit
@@ -105,8 +112,8 @@ export const IdentitySection = withPermission(
return (
<div>
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-1">
<div className="mb-4 flex w-full items-center gap-4">
<div className="flex flex-1 items-center gap-1">
<p className="text-xl font-medium text-mineshaft-100">Identities</p>
<a
href="https://infisical.com/docs/documentation/platform/identities/overview"
@@ -123,6 +130,26 @@ export const IdentitySection = withPermission(
</div>
</a>
</div>
{isSubOrganization && (
<OrgPermissionCan
I={OrgPermissionIdentityActions.Create}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<Button
variant="plain"
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faLink} />}
onClick={() => {
handlePopUpOpen("linkIdentity");
}}
isDisabled={!isAllowed}
>
Link Identity
</Button>
)}
</OrgPermissionCan>
)}
<OrgPermissionCan
I={OrgPermissionIdentityActions.Create}
a={OrgPermissionSubjects.Identity}
@@ -210,16 +237,18 @@ export const IdentitySection = withPermission(
?.name || ""
}
/>
{/* <IdentityAuthMethodModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/> */}
{/* <IdentityUniversalAuthClientSecretModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/> */}
<Modal
isOpen={popUp.linkIdentity.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("linkIdentity", isOpen)}
>
<ModalContent
title="Assign Existing Identity"
subTitle="Assign an existing identity from your root organization to the sub organization. The identity will continue to be managed at its original scope."
bodyClassName="overflow-visible"
>
<IdentityLinkForm onClose={() => handlePopUpClose("linkIdentity")} />
</ModalContent>
</Modal>
<IdentityTokenAuthTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen}
@@ -2,6 +2,7 @@ import { useCallback, useState } from "react";
import {
faArrowDown,
faArrowUp,
faBuilding,
faCheckCircle,
faChevronRight,
faEdit,
@@ -78,7 +79,7 @@ type Filter = {
export const IdentityTable = ({ handlePopUpOpen }: Props) => {
const navigate = useNavigate();
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
const {
offset,
@@ -286,15 +287,18 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
</IconButton>
</div>
</Th>
{isSubOrganization && <Th>Managed By</Th>}
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={3} innerKey="org-identities" />}
{isPending && (
<TableSkeleton columns={isSubOrganization ? 4 : 3} innerKey="org-identities" />
)}
{!isPending &&
data?.identities?.map(
({
identity: { id, name },
identity: { id, name, orgId },
role,
customRole,
lastLoginAuthMethod,
@@ -362,6 +366,14 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
}}
</OrgPermissionCan>
</Td>
{isSubOrganization && (
<Td>
<p className="truncate">
<FontAwesomeIcon size="sm" className="mr-1.5" icon={faBuilding} />
{currentOrg.id === orgId ? "Sub Organization" : "Root Organization"}
</p>
</Td>
)}
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -0,0 +1,280 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { RoleOption } from "@app/components/roles";
import { Button, FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { findOrgMembershipRole } from "@app/helpers/roles";
import {
useAddUsersToOrg,
useAddUserToWsNonE2EE,
useGetOrgRoles,
useGetUserProjects
} from "@app/hooks/api";
import { useGetAvailableOrgUsers } from "@app/hooks/api/organization/queries";
import { ProjectType, ProjectVersion } from "@app/hooks/api/projects/types";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
const DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG = "member";
const addMemberFormSchema = z.object({
users: z
.array(
z.object({
username: z.string().trim(),
email: z.string().trim()
})
)
.min(1),
projects: z
.array(
z.object({
name: z.string(),
id: z.string(),
slug: z.string(),
version: z.nativeEnum(ProjectVersion)
})
)
.default([]),
projectRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG),
organizationRole: z.object({
name: z.string(),
slug: z.string(),
description: z.string().optional()
})
});
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
type Props = {
onClose: () => void;
};
export const AddSubOrgMemberModal = ({ onClose }: Props) => {
const { currentOrg } = useOrganization();
const { data: organizationRoles } = useGetOrgRoles(currentOrg?.id ?? "");
const { data: members = [], isPending: isMembersPending } = useGetAvailableOrgUsers();
const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg();
const { mutateAsync: addUserToProject } = useAddUserToWsNonE2EE();
const { data: projects, isPending: isProjectsLoading } = useGetUserProjects({
includeRoles: true
});
const {
control,
handleSubmit,
watch,
reset,
formState: { isSubmitting }
} = useForm<TAddMemberForm>({
resolver: zodResolver(addMemberFormSchema)
});
// set initial form role based off org default role
useEffect(() => {
if (organizationRoles) {
reset({
organizationRole: findOrgMembershipRole(organizationRoles, currentOrg.defaultMembershipRole)
});
}
}, [organizationRoles]);
const onAddMembers = async ({
users,
organizationRole,
projects: selectedProjects,
projectRoleSlug
}: TAddMemberForm) => {
if (!currentOrg?.id) return;
if (selectedProjects?.length) {
// eslint-disable-next-line no-restricted-syntax
for (const project of selectedProjects) {
if (project.version !== ProjectVersion.V3) {
createNotification({
type: "error",
text: `Cannot add users to project "${project.name}" because it's incompatible. Please upgrade the project.`
});
return;
}
}
}
try {
const usernames = users.map((el) => el.username);
await addUsersMutateAsync({
organizationId: currentOrg?.id,
inviteeEmails: usernames,
organizationRoleSlug: organizationRole.slug
});
await Promise.allSettled(
selectedProjects.map((el) =>
addUserToProject({
orgId: currentOrg.id,
projectId: el.id,
roleSlugs: [projectRoleSlug],
usernames
})
)
);
onClose();
} catch (error) {
console.error(error);
createNotification({
text: "Failed to add user to suborganization",
type: "error"
});
}
};
const getGroupHeaderLabel = (type: ProjectType) => {
switch (type) {
case ProjectType.SecretManager:
return "Secrets";
case ProjectType.CertificateManager:
return "PKI";
case ProjectType.KMS:
return "KMS";
case ProjectType.SSH:
return "SSH";
default:
return "Other";
}
};
return (
<form onSubmit={handleSubmit(onAddMembers)}>
<Controller
control={control}
name="users"
render={({ field, fieldState: { error } }) => (
<FormControl label="Emails" isError={Boolean(error)} errorText={error?.message}>
<FilterableSelect
className="w-full"
placeholder="Add one or more users..."
isMulti
name="members"
isLoading={isMembersPending}
options={members}
value={field.value}
onChange={field.onChange}
getOptionValue={(option) => option.username}
getOptionLabel={(option) => option.username}
/* eslint-disable-next-line react/no-unstable-nested-components */
noOptionsMessage={() => (
<p>All root organization users are already assigned to this project</p>
)}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="organizationRole"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText="Select which organization role you want to assign to the user."
label="Assign organization role"
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
placeholder="Select role..."
options={organizationRoles}
getOptionValue={(option) => option.slug}
getOptionLabel={(option) => option.name}
value={value}
onChange={onChange}
components={{ Option: RoleOption }}
/>
</FormControl>
)}
/>
<div className="flex items-start justify-between gap-2">
<div className="w-full">
<Controller
control={control}
name="projects"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Assign users to projects"
isOptional
isError={Boolean(error?.message)}
errorText={error?.message}
>
<FilterableSelect
isMulti
value={value}
onChange={onChange}
isLoading={isProjectsLoading}
getOptionLabel={(project) => project.name}
getOptionValue={(project) => project.id}
options={projects}
groupBy="type"
getGroupHeaderLabel={getGroupHeaderLabel}
placeholder="Select projects..."
/>
</FormControl>
)}
/>
</div>
<div className="mt-[0.15rem] flex min-w-fit justify-end">
<Controller
control={control}
name="projectRoleSlug"
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipText="Select which role to assign to the users in the selected projects."
label="Role"
isError={Boolean(error)}
errorText={error?.message}
>
<div>
<Select
isDisabled={watch("projects", []).length === 0}
defaultValue={DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG}
{...field}
onValueChange={(val) => field.onChange(val)}
>
{Object.entries(ProjectMembershipRole).map(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
([_, slug]) =>
slug !== "custom" && (
<SelectItem key={slug} value={slug}>
<span className="capitalize">{slug.replace("-", " ")}</span>
</SelectItem>
)
)}
</Select>
</div>
</FormControl>
)}
/>
</div>
</div>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Add Member
</Button>
<Button colorSchema="secondary" variant="plain" onClick={onClose}>
Cancel
</Button>
</div>
</form>
);
};
@@ -11,6 +11,8 @@ import {
Button,
DeleteActionModal,
EmailServiceSetupModal,
Modal,
ModalContent,
Tooltip
} from "@app/components/v2";
import {
@@ -26,11 +28,12 @@ import { OrgUser } from "@app/hooks/api/users/types";
import { usePopUp } from "@app/hooks/usePopUp";
import { AddOrgMemberModal } from "./AddOrgMemberModal";
import { AddSubOrgMemberModal } from "./AddSubOrgMemberModal";
import { OrgMembersTable } from "./OrgMembersTable";
export const OrgMembersSection = () => {
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
const orgId = currentOrg?.id ?? "";
const { user } = useUser();
const userId = user?.id || "";
@@ -41,6 +44,7 @@ export const OrgMembersSection = () => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addMember",
"addMemberToSubOrg",
"removeMember",
"deactivateMember",
"upgradePlan",
@@ -210,7 +214,9 @@ export const OrgMembersSection = () => {
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handleAddMemberModal()}
onClick={() =>
isSubOrganization ? handlePopUpOpen("addMemberToSubOrg") : handleAddMemberModal()
}
isDisabled={!isAllowed}
>
Add Member
@@ -230,6 +236,14 @@ export const OrgMembersSection = () => {
completeInviteLinks={completeInviteLinks}
setCompleteInviteLinks={setCompleteInviteLinks}
/>
<Modal
isOpen={popUp.addMemberToSubOrg.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addMemberToSubOrg", isOpen)}
>
<ModalContent title="Add member from your organization">
<AddSubOrgMemberModal onClose={() => handlePopUpClose("addMemberToSubOrg")} />
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.removeMember.isOpen}
title={`Are you sure you want to remove member with username ${
@@ -109,7 +109,7 @@ export const OrgMembersTable = ({
}: Props) => {
const navigate = useNavigate();
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
const { user } = useUser();
const userId = user?.id || "";
const orgId = currentOrg?.id || "";
@@ -586,6 +586,7 @@ export const OrgMembersTable = ({
{isActive &&
(status === "invited" || status === "verified") &&
email &&
!isSubOrganization &&
serverDetails?.emailConfigured && (
<OrgPermissionCan
I={OrgPermissionActions.Edit}
@@ -68,7 +68,7 @@ enum RolesOrderBy {
export const OrgRoleTable = () => {
const navigate = useNavigate();
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
const orgId = currentOrg?.id || "";
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -200,7 +200,9 @@ export const OrgRoleTable = () => {
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<p className="text-xl font-medium text-mineshaft-100">Organization Roles</p>
<p className="text-xl font-medium text-mineshaft-100">
{isSubOrganization ? "Sub-" : ""}Organization Roles
</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
{(isAllowed) => (
<Button
@@ -3,6 +3,7 @@ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
OrgPermissionAppConnectionActions,
OrgPermissionSubjects
@@ -12,6 +13,8 @@ import { AppConnectionsTable } from "@app/pages/organization/AppConnections/AppC
export const AppConnectionsPage = withPermission(
() => {
const { isSubOrganization } = useOrganization();
return (
<div className="bg-bunker-800">
<Helmet>
@@ -22,7 +25,7 @@ export const AppConnectionsPage = withPermission(
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-8xl">
<PageHeader
scope="org"
scope={isSubOrganization ? "namespace" : "org"}
className="w-full"
title="App Connections"
description="Manage organization App Connections"
@@ -1,4 +1,5 @@
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";
import { AppConnectionsPage } from "./AppConnectionsPage";
@@ -6,6 +7,11 @@ export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/"
)({
component: AppConnectionsPage,
validateSearch: z.object({
error: z.string().optional(),
success: z.string().optional(),
connectionId: z.string().optional()
}),
context: () => ({
breadcrumbs: [
{
@@ -1,10 +1,13 @@
import { Helmet } from "react-helmet";
import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { LogsSection } from "./components";
export const AuditLogsPage = () => {
const { isSubOrganization } = useOrganization();
return (
<div className="h-full bg-bunker-800">
<Helmet>
@@ -15,7 +18,7 @@ export const AuditLogsPage = () => {
<div className="flex w-full justify-center bg-bunker-800 pb-6 text-white">
<div className="w-full max-w-8xl">
<PageHeader
scope="org"
scope={isSubOrganization ? "namespace" : "org"}
title="Audit Logs"
description="Audit logs for security and compliance teams to monitor information access."
/>
@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { BillingPage } from "./BillingPage";
@@ -6,7 +6,14 @@ export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/billing"
)({
component: BillingPage,
beforeLoad: () => {
beforeLoad: ({ search }) => {
if (search.subOrganization) {
throw redirect({
to: "/organization/projects",
search
});
}
return {
breadcrumbs: [
{
@@ -20,7 +20,7 @@ import {
Tooltip
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/context";
import { OrgPermissionGroupActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDeleteGroup } from "@app/hooks/api";
import { useGetGroupById } from "@app/hooks/api/groups/queries";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -45,6 +45,8 @@ const Page = () => {
const { data, isPending } = useGetGroupById(groupId);
const { isSubOrganization } = useOrganization();
const { mutateAsync: deleteMutateAsync } = useDeleteGroup();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -95,7 +97,11 @@ const Page = () => {
<FontAwesomeIcon icon={faChevronLeft} />
Groups
</Link>
<PageHeader scope="org" description="Organization Group" title={data.group.name}>
<PageHeader
scope={isSubOrganization ? "namespace" : "org"}
description={`${isSubOrganization ? "Sub-" : ""}Organization Group`}
title={data.group.name}
>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -29,10 +29,11 @@ const Page = () => {
from: ROUTE_PATHS.Organization.IdentityDetailsByIDPage.id
});
const identityId = params.identityId as string;
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
const orgId = currentOrg?.id || "";
const { data } = useGetIdentityById(identityId);
const { mutateAsync: deleteIdentity } = useDeleteIdentity();
const isAuthHidden = orgId !== data?.identity?.orgId;
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"identity",
@@ -87,14 +88,24 @@ const Page = () => {
<FontAwesomeIcon icon={faChevronLeft} />
Identities
</Link>
<PageHeader scope="org" description="Organization Identity" title={data.identity.name} />
<PageHeader
scope={isSubOrganization ? "namespace" : "org"}
description={`${isSubOrganization ? "Sub-" : ""}Organization Identity`}
title={data.identity.name}
/>
<div className="flex">
<div className="mr-4 w-96">
<IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
<IdentityAuthenticationSection
<IdentityDetailsSection
isOrgIdentity={data.identity.orgId === currentOrg.id}
identityId={identityId}
handlePopUpOpen={handlePopUpOpen}
/>
{!isAuthHidden && (
<IdentityAuthenticationSection
identityId={identityId}
handlePopUpOpen={handlePopUpOpen}
/>
)}
</div>
<IdentityProjectsSection identityId={identityId} />
</div>
@@ -21,23 +21,25 @@ import {
Tag,
Tooltip
} from "@app/components/v2";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context";
import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { identityAuthToNameMap, useGetIdentityById } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
identityId: string;
isOrgIdentity?: boolean;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["identity", "identityAuthMethod", "deleteIdentity"]>,
data?: object
) => void;
};
export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) => {
export const IdentityDetailsSection = ({ identityId, handlePopUpOpen, isOrgIdentity }: Props) => {
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard"
});
const { isSubOrganization } = useOrganization();
const { data } = useGetIdentityById(identityId);
return data ? (
@@ -75,6 +77,7 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
handlePopUpOpen("identity", {
identityId,
name: data.identity.name,
orgId: data.identity.orgId,
hasDeleteProtection: data.identity.hasDeleteProtection,
role: data.role,
customRole: data.customRole,
@@ -140,24 +143,38 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
<p className="text-sm font-medium text-mineshaft-300">Name</p>
<p className="text-sm text-mineshaft-300">{data.identity.name}</p>
</div>
<div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">Last Login Auth Method</p>
<p className="text-sm text-mineshaft-300">
{data.lastLoginAuthMethod ? identityAuthToNameMap[data.lastLoginAuthMethod] : "-"}
</p>
</div>
<div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">Last Login Time</p>
<p className="text-sm text-mineshaft-300">
{data.lastLoginTime ? format(data.lastLoginTime, "PPpp") : "-"}
</p>
</div>
<div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">Delete Protection</p>
<p className="text-sm text-mineshaft-300">
{data.identity.hasDeleteProtection ? "On" : "Off"}
</p>
</div>
{isSubOrganization && (
<div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">Managed By</p>
<p className="text-sm text-mineshaft-300">
{isOrgIdentity ? "Organization" : "Root Organization"}
</p>
</div>
)}
{isOrgIdentity && (
<div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">Last Login Auth Method</p>
<p className="text-sm text-mineshaft-300">
{data.lastLoginAuthMethod ? identityAuthToNameMap[data.lastLoginAuthMethod] : "-"}
</p>
</div>
)}
{isOrgIdentity && (
<div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">Last Login Time</p>
<p className="text-sm text-mineshaft-300">
{data.lastLoginTime ? format(data.lastLoginTime, "PPpp") : "-"}
</p>
</div>
)}
{isOrgIdentity && (
<div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">Delete Protection</p>
<p className="text-sm text-mineshaft-300">
{data.identity.hasDeleteProtection ? "On" : "Off"}
</p>
</div>
)}
<div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">Organization Role</p>
<p className="text-sm text-mineshaft-300">{data.role}</p>
@@ -1,10 +1,13 @@
import { Helmet } from "react-helmet";
import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { NetworkingTabGroup } from "./components/NetworkingTabGroup/NetworkingTabGroup";
export const NetworkingPage = () => {
const { isSubOrganization } = useOrganization();
return (
<>
<Helmet>
@@ -14,7 +17,7 @@ export const NetworkingPage = () => {
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-8xl">
<PageHeader
scope="org"
scope={isSubOrganization ? "namespace" : "org"}
title="Networking"
description="Manage gateways and relays to securely access private network resources from Infisical"
/>
@@ -2,6 +2,7 @@ import { useState } from "react";
import { useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { GatewayTab } from "../GatewayTab/GatewayTab";
import { RelayTab } from "../RelayTab/RelayTab";
@@ -18,11 +19,13 @@ export const NetworkingTabGroup = () => {
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
const { isSubOrganization } = useOrganization();
return (
<Tabs orientation="vertical" value={selectedTab} onValueChange={setSelectedTab}>
<TabList>
{tabs.map((tab) => (
<Tab variant="org" value={tab.key} key={tab.key}>
<Tab variant={isSubOrganization ? "namespace" : "org"} value={tab.key} key={tab.key}>
{tab.name}
</Tab>
))}
@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { NewProjectModal } from "@app/components/projects";
import { PageHeader } from "@app/components/v2";
import { useSubscription } from "@app/context";
import { useOrganization, useSubscription } from "@app/context";
import { usePopUp } from "@app/hooks/usePopUp";
import { AllProjectView } from "./components/AllProjectView";
@@ -52,7 +52,7 @@ export const ProjectsPage = () => {
] as const);
const { subscription } = useSubscription();
const { isSubOrganization } = useOrganization();
const isAddingProjectsAllowed = subscription?.workspaceLimit
? subscription.workspacesUsed < subscription.workspaceLimit
: true;
@@ -64,7 +64,7 @@ export const ProjectsPage = () => {
<link rel="icon" href="/infisical.ico" />
</Helmet>
<PageHeader
scope="org"
scope={isSubOrganization ? "namespace" : "org"}
title="Overview"
description="Your team's complete security toolkit - organized and ready when you need them."
/>
@@ -378,7 +378,7 @@ export const MyProjectView = ({
<div className="flex w-full flex-row">
<ProjectListToggle value={projectListView} onChange={onProjectListViewChange} />
<Input
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50/60 duration-200 focus:bg-mineshaft-700/80"
containerClassName="w-full ml-2"
placeholder="Search by project name..."
value={searchFilter}
@@ -30,7 +30,7 @@ export const Page = () => {
from: ROUTE_PATHS.Organization.RoleByIDPage.id
});
const roleId = params.roleId as string;
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
const orgId = currentOrg?.id || "";
const { data } = useGetOrgRole(orgId, roleId);
const { mutateAsync: deleteOrgRole } = useDeleteOrgRole();
@@ -91,7 +91,7 @@ export const Page = () => {
Roles
</Link>
<PageHeader
scope="org"
scope={isSubOrganization ? "namespace" : "org"}
title={data.name}
description={
<>
@@ -12,6 +12,7 @@ import {
OrgPermissionKmipActions,
OrgPermissionMachineIdentityAuthTemplateActions,
OrgPermissionSecretShareAction,
OrgPermissionSubOrgActions,
OrgRelayPermissionActions
} from "@app/context/OrgPermissionContext/types";
import { TPermission } from "@app/hooks/api/roles/types";
@@ -123,6 +124,13 @@ const secretSharingPermissionSchema = z
})
.optional();
const subOrganizationPermissionSchema = z
.object({
[OrgPermissionSubOrgActions.Create]: z.boolean().optional(),
[OrgPermissionSubOrgActions.DirectAccess]: z.boolean().optional()
})
.optional();
export const formSchema = z.object({
name: z.string().trim(),
description: z.string().trim().optional(),
@@ -159,7 +167,8 @@ export const formSchema = z.object({
gateway: orgGatewayPermissionSchema,
relay: orgRelayPermissionSchema,
"machine-identity-auth-template": machineIdentityAuthTemplatePermissionSchema,
"secret-share": secretSharingPermissionSchema
"secret-share": secretSharingPermissionSchema,
"sub-organization": subOrganizationPermissionSchema
})
.optional()
});
@@ -0,0 +1,159 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
import { OrgPermissionSubOrgActions } from "@app/context/OrgPermissionContext/types";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "../OrgRoleModifySection.utils";
const PERMISSION_ACTIONS = [
{ action: OrgPermissionSubOrgActions.Create, label: "Create" },
{ action: OrgPermissionSubOrgActions.DirectAccess, label: "Direct Access" }
] as const;
type Props = {
isEditable: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
FullAccess = "full-access",
Custom = "custom"
}
export const OrgPermissionSubOrgRow = ({ isEditable, control, setValue }: Props) => {
const [isRowExpanded, setIsRowExpanded] = useToggle();
const [isCustom, setIsCustom] = useToggle();
const rule = useWatch({
control,
name: "permissions.sub-organization"
});
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSION_ACTIONS.length;
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) {
setIsRowExpanded.on();
setIsCustom.on();
return;
}
setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue(
"permissions.sub-organization",
{
[OrgPermissionSubOrgActions.Create]: false,
[OrgPermissionSubOrgActions.DirectAccess]: false
},
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setValue(
"permissions.sub-organization",
{
[OrgPermissionSubOrgActions.Create]: true,
[OrgPermissionSubOrgActions.DirectAccess]: true
},
{ shouldDirty: true }
);
break;
default:
setValue(
"permissions.sub-organization",
{
[OrgPermissionSubOrgActions.Create]: true,
[OrgPermissionSubOrgActions.DirectAccess]: true
},
{ shouldDirty: true }
);
break;
}
};
return (
<>
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td className="w-full select-none">Sub-Organizations</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</Td>
</Tr>
{isRowExpanded && (
<Tr>
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
name={`permissions.sub-organization.${action}`}
key={`permissions.sub-organization.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={(e) => {
if (!isEditable) {
createNotification({
type: "error",
text: "Failed to update default role"
});
return;
}
field.onChange(e);
}}
id={`permissions.sub-organization.${action}`}
>
{label}
</Checkbox>
)}
/>
);
})}
</div>
</Td>
</Tr>
)}
</>
);
};
@@ -74,6 +74,7 @@ type Props = {
| "billing"
| "audit-logs"
| "machine-identity-auth-template"
| "sub-organization"
>;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
@@ -25,6 +25,7 @@ import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow";
import { OrgPermissionMachineIdentityAuthTemplateRow } from "./OrgPermissionMachineIdentityAuthTemplateRow";
import { OrgRelayPermissionRow } from "./OrgPermissionRelayRow";
import { OrgPermissionSecretShareRow } from "./OrgPermissionSecretShareRow";
import { OrgPermissionSubOrgRow } from "./OrgPermissionSubOrgRow";
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
import { RolePermissionRow } from "./RolePermissionRow";
@@ -76,8 +77,18 @@ type Props = {
roleId: string;
};
const INVALID_SUBORG_PERMISSIONS = [
OrgPermissionSubjects.Sso,
OrgPermissionSubjects.Ldap,
OrgPermissionSubjects.Scim,
OrgPermissionSubjects.GithubOrgSync,
OrgPermissionSubjects.GithubOrgSyncManual,
OrgPermissionSubjects.Billing,
OrgPermissionSubjects.SubOrganization
];
export const RolePermissionsSection = ({ roleId }: Props) => {
const { currentOrg } = useOrganization();
const { currentOrg, isRootOrganization } = useOrganization();
const orgId = currentOrg?.id || "";
const { data: role } = useGetOrgRole(orgId, roleId);
@@ -152,7 +163,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
<TableContainer>
<Table>
<TBody>
{SIMPLE_PERMISSION_OPTIONS.map((permission) => {
{SIMPLE_PERMISSION_OPTIONS.filter((el) =>
isRootOrganization
? true
: !INVALID_SUBORG_PERMISSIONS.includes(el.formName as OrgPermissionSubjects)
).map((permission) => {
return (
<RolePermissionRow
title={permission.title}
@@ -194,11 +209,13 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
setValue={setValue}
isEditable={isCustomRole}
/>
<OrgPermissionBillingRow
control={control}
setValue={setValue}
isEditable={isCustomRole}
/>
{isRootOrganization && (
<OrgPermissionBillingRow
control={control}
setValue={setValue}
isEditable={isCustomRole}
/>
)}
<OrgPermissionSecretShareRow
control={control}
setValue={setValue}
@@ -224,6 +241,13 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
setValue={setValue}
isEditable={isCustomRole}
/>
{isRootOrganization && (
<OrgPermissionSubOrgRow
control={control}
setValue={setValue}
isEditable={isCustomRole}
/>
)}
</TBody>
</Table>
</TableContainer>
@@ -4,12 +4,13 @@ import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { ShareSecretSection } from "./ShareSecretSection";
export const SecretSharingPage = () => {
const { t } = useTranslation();
const { isSubOrganization } = useOrganization();
return (
<>
<Helmet>
@@ -22,7 +23,7 @@ export const SecretSharingPage = () => {
<div className="h-full">
<div className="mx-auto h-full w-full max-w-8xl bg-bunker-800 text-white">
<PageHeader
scope="org"
scope={isSubOrganization ? "namespace" : "org"}
title="Secret Sharing"
description="Share secrets securely using a shareable link"
>
@@ -3,17 +3,21 @@ import { useNavigate, useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { useOrganization } from "@app/context";
import { RequestSecretTab } from "./components/RequestSecret/RequestSecretTab";
import { SecretSharingSettingsTab } from "./components/SecretSharingSettings/SecretSharingSettingsTab";
import { ShareSecretTab } from "./components/ShareSecret/ShareSecretTab";
enum SecretSharingPageTabs {
ShareSecret = "share-secret",
RequestSecret = "request-secret"
RequestSecret = "request-secret",
Settings = "settings"
}
export const ShareSecretSection = () => {
const navigate = useNavigate();
const { isSubOrganization } = useOrganization();
const { selectedTab } = useSearch({
from: ROUTE_PATHS.Organization.SecretSharing.id
@@ -26,6 +30,8 @@ export const ShareSecretSection = () => {
});
};
const tabVariant = isSubOrganization ? "namespace" : "org";
return (
<div>
<Helmet>
@@ -36,12 +42,17 @@ export const ShareSecretSection = () => {
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
<TabList>
<Tab variant="org" value={SecretSharingPageTabs.ShareSecret}>
<Tab variant={tabVariant} value={SecretSharingPageTabs.ShareSecret}>
Share Secrets
</Tab>
<Tab variant="org" value={SecretSharingPageTabs.RequestSecret}>
<Tab variant={tabVariant} value={SecretSharingPageTabs.RequestSecret}>
Request Secrets
</Tab>
{!isSubOrganization && (
<Tab variant={tabVariant} value={SecretSharingPageTabs.Settings}>
Settings
</Tab>
)}
</TabList>
<TabPanel value={SecretSharingPageTabs.ShareSecret}>
<ShareSecretTab />
@@ -49,6 +60,9 @@ export const ShareSecretSection = () => {
<TabPanel value={SecretSharingPageTabs.RequestSecret}>
<RequestSecretTab />
</TabPanel>
<TabPanel value={SecretSharingPageTabs.Settings}>
<SecretSharingSettingsTab />
</TabPanel>
</Tabs>
</div>
);
@@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy, faRedo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { useSearch } from "@tanstack/react-router";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
@@ -36,6 +37,10 @@ export const RequestSecretForm = () => {
const [, isCopyingSecret, setCopyTextSecret] = useTimedReset<string>({
initialState: "Copy to clipboard"
});
const subOrganization = useSearch({
strict: false,
select: (el) => el?.subOrganization
});
const { mutateAsync: createSecretRequest } = useCreateSecretRequest();
@@ -58,12 +63,15 @@ export const RequestSecretForm = () => {
expiresAt
});
const link = `${window.location.origin}/secret-request/secret/${id}`;
const link = new URL(`${window.location.origin}/secret-request/secret/${id}`);
if (subOrganization) {
link.searchParams.set("subOrganization", subOrganization);
}
setSecretLink(link);
setSecretLink(link.toString());
reset();
navigator.clipboard.writeText(link);
navigator.clipboard.writeText(link.toString());
setCopyTextSecret("secret");
createNotification({
@@ -0,0 +1,21 @@
import { OrgPermissionSubjects } from "@app/context";
import { OrgPermissionSecretShareAction } from "@app/context/OrgPermissionContext/types";
import { withPermission } from "@app/hoc";
import { OrgSecretShareLimitSection } from "./OrgSecretShareLimitSection";
import { SecretSharingAllowShareToAnyone } from "./SecretSharingAllowShareToAnyone";
export const SecretSharingSettingsTab = withPermission(
() => {
return (
<div className="w-full">
<SecretSharingAllowShareToAnyone />
<OrgSecretShareLimitSection />
</div>
);
},
{
action: OrgPermissionSecretShareAction.ManageSettings,
subject: OrgPermissionSubjects.SecretShare
}
);
@@ -1,35 +0,0 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import {
OrgPermissionSecretShareAction,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { withPermission } from "@app/hoc";
import { SecretSharingSettingsTabGroup } from "./components";
export const SecretSharingSettingsPage = withPermission(
() => {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>{t("common.head-title", { title: "Secret Share Settings" })}</title>
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-8xl">
<PageHeader scope="org" title="Secret Share Settings" />
<SecretSharingSettingsTabGroup />
</div>
</div>
</>
);
},
{
action: OrgPermissionSecretShareAction.ManageSettings,
subject: OrgPermissionSubjects.SecretShare
}
);
@@ -1 +0,0 @@
export { OrgSecretShareLimitSection } from "./OrgSecretShareLimitSection";
@@ -1 +0,0 @@
export { SecretSharingAllowShareToAnyone } from "./SecretSharingAllowShareToAnyone";
@@ -1,11 +0,0 @@
import { OrgSecretShareLimitSection } from "../OrgSecretShareLimitSection";
import { SecretSharingAllowShareToAnyone } from "../SecretSharingAllowShareToAnyone";
export const SecretSharingSettingsGeneralTab = () => {
return (
<div className="w-full">
<SecretSharingAllowShareToAnyone />
<OrgSecretShareLimitSection />
</div>
);
};
@@ -1 +0,0 @@
export { SecretSharingSettingsGeneralTab } from "./SecretSharingSettingsGeneralTab";
@@ -1,39 +0,0 @@
import { useState } from "react";
import { useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { SecretSharingSettingsGeneralTab } from "../SecretSharingSettingsGeneralTab";
export const SecretSharingSettingsTabGroup = () => {
const search = useSearch({
from: ROUTE_PATHS.Organization.SecretSharingSettings.id
});
const tabs = [
{
name: "General",
key: "tab-secret-sharing-general",
component: SecretSharingSettingsGeneralTab
}
];
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
return (
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
<TabList>
{tabs.map((tab) => (
<Tab value={tab.key} key={tab.key}>
{tab.name}
</Tab>
))}
</TabList>
{tabs.map(({ key, component: Component }) => (
<TabPanel value={key} key={`tab-panel-${key}`}>
<Component />
</TabPanel>
))}
</Tabs>
);
};
@@ -1 +0,0 @@
export { SecretSharingSettingsTabGroup } from "./SecretSharingSettingsTabGroup";
@@ -1 +0,0 @@
export { SecretSharingSettingsTabGroup } from "./SecretSharingSettingsTabGroup";
@@ -1,30 +0,0 @@
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { SecretSharingSettingsPage } from "./SecretSharingSettingsPage";
const SettingsPageQueryParams = z.object({
selectedTab: z.string().catch("")
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings"
)({
component: SecretSharingSettingsPage,
validateSearch: zodValidator(SettingsPageQueryParams),
search: {
middlewares: [stripSearchParams({ selectedTab: "" })]
},
context: () => ({
breadcrumbs: [
{
label: "Secret Sharing",
link: linkOptions({ to: "/organization/secret-sharing" })
},
{
label: "Settings"
}
]
})
});
@@ -2,11 +2,13 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { OrgTabGroup } from "./components";
export const SettingsPage = () => {
const { t } = useTranslation();
const { isSubOrganization } = useOrganization();
return (
<>
@@ -16,7 +18,7 @@ export const SettingsPage = () => {
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-8xl">
<PageHeader
scope="org"
scope={isSubOrganization ? "namespace" : "org"}
description="Configure organization-wide settings"
title={t("settings.org.title")}
/>
@@ -55,7 +55,7 @@ export const OrgDeleteSection = () => {
onClick={() => handlePopUpOpen("deleteOrg")}
isDisabled={Boolean(!hasOrgRole(OrgMembershipRole.Admin))}
>
{`Delete ${currentOrg?.name}`}
{`Delete ${currentOrg.subOrganization?.name ?? currentOrg?.name}`}
</Button>
</div>
<DeleteActionModal
@@ -1,16 +1,17 @@
import { useOrgPermission } from "@app/context";
import { useOrganization, useOrgPermission } from "@app/context";
import { OrgMembershipRole } from "@app/helpers/roles";
import { OrgDeleteSection } from "../OrgDeleteSection";
import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection";
import { OrgNameChangeSection } from "../OrgNameChangeSection";
import { OrgNameChangeSection, SubOrgNameChangeSection } from "../OrgNameChangeSection";
export const OrgGeneralTab = () => {
const { hasOrgRole } = useOrgPermission();
const { isSubOrganization } = useOrganization();
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<OrgNameChangeSection />
<OrgIncidentContactsSection />
{isSubOrganization ? <SubOrgNameChangeSection /> : <OrgNameChangeSection />}
{!isSubOrganization && <OrgIncidentContactsSection />}
{hasOrgRole(OrgMembershipRole.Admin) && <OrgDeleteSection />}
</div>
);
@@ -41,21 +41,19 @@ export const OrgNameChangeSection = (): JSX.Element => {
const [isFormInitialized, setIsFormInitialized] = useState(false);
useEffect(() => {
if (currentOrg) {
reset({
name: currentOrg.name,
slug: currentOrg.slug,
...(canReadOrgRoles &&
roles?.length && {
// will always be present, can't remove role if default
defaultMembershipRole: isCustomOrgRole(currentOrg.defaultMembershipRole)
? roles?.find((role) => currentOrg.defaultMembershipRole === role.id)?.slug || ""
: currentOrg.defaultMembershipRole
})
});
setIsFormInitialized(true);
}
}, [currentOrg, roles]);
reset({
name: currentOrg.name,
slug: currentOrg.slug,
...(canReadOrgRoles &&
roles?.length && {
// will always be present, can't remove role if default
defaultMembershipRole: isCustomOrgRole(currentOrg.defaultMembershipRole)
? roles?.find((role) => currentOrg.defaultMembershipRole === role.id)?.slug || ""
: currentOrg.defaultMembershipRole
})
});
setIsFormInitialized(true);
}, [roles]);
const onFormSubmit = async ({ name, slug, defaultMembershipRole }: FormData) => {
try {
@@ -0,0 +1,101 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQueryClient } from "@tanstack/react-query";
import { useNavigate, useRouter } from "@tanstack/react-router";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useOrgPermission
} from "@app/context";
import { useUpdateSubOrganization } from "@app/hooks/api";
const formSchema = z.object({
name: z
.string()
.regex(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens")
});
type FormData = z.infer<typeof formSchema>;
export const SubOrgNameChangeSection = (): JSX.Element => {
const { currentOrg } = useOrganization();
const { permission } = useOrgPermission();
const navigate = useNavigate();
const router = useRouter();
const queryClient = useQueryClient();
const { handleSubmit, control } = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
name: currentOrg?.subOrganization?.name || ""
}
});
const { mutateAsync, isPending } = useUpdateSubOrganization();
const onFormSubmit = async ({ name }: FormData) => {
try {
await mutateAsync({
name,
subOrgId: currentOrg.id
});
navigate({ to: "/organization/settings", search: { subOrganization: name } });
queryClient.invalidateQueries();
await router.invalidate({ sync: true });
createNotification({
text: "Successfully updated sub-organization details",
type: "success"
});
} catch (error) {
console.error(error);
createNotification({
text: "Failed to update sub-organization details",
type: "error"
});
}
};
return (
<form onSubmit={handleSubmit(onFormSubmit)} className="py-4">
<div>
<h2 className="text-md mb-2 text-mineshaft-100">Organization Name</h2>
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} className="max-w-md">
<Input
isDisabled={permission.cannot(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Settings
)}
placeholder="Acme Corp"
{...field}
/>
</FormControl>
)}
control={control}
name="name"
/>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<Button
isLoading={isPending}
isDisabled={!isAllowed}
colorSchema="primary"
variant="outline_bg"
type="submit"
>
Save
</Button>
)}
</OrgPermissionCan>
</form>
);
};
@@ -1 +1,2 @@
export { OrgNameChangeSection } from "./OrgNameChangeSection";
export { SubOrgNameChangeSection } from "./SubOrgNameChangeSection";
@@ -50,7 +50,7 @@ export const OrgProductSelectSection = () => {
}));
}
});
}, [currentOrg]);
}, [currentOrg?.id]);
const onProductToggle = async (value: boolean, key: string) => {
setIsLoading(true);
@@ -3,6 +3,7 @@ import { useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { useOrganization } from "@app/context";
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
import { ExternalMigrationsTab } from "../ExternalMigrationsTab";
@@ -19,20 +20,33 @@ export const OrgTabGroup = () => {
const search = useSearch({
from: ROUTE_PATHS.Organization.SettingsPage.id
});
const { isSubOrganization } = useOrganization();
const tabs = [
{ name: "General", key: "tab-org-general", component: OrgGeneralTab },
{
name: "SSO",
key: "sso-settings",
component: OrgSsoTab
component: OrgSsoTab,
isHidden: isSubOrganization
},
{
name: "Provisioning",
key: "provisioning-settings",
component: OrgProvisioningTab
component: OrgProvisioningTab,
isHidden: isSubOrganization
},
{
name: "Security",
key: "tab-org-security",
component: OrgSecurityTab,
isHidden: isSubOrganization
},
{
name: "Encryption",
key: "tab-org-encryption",
component: OrgEncryptionTab
},
{ name: "Security", key: "tab-org-security", component: OrgSecurityTab },
{ name: "Encryption", key: "tab-org-encryption", component: OrgEncryptionTab },
{
name: "Workflow Integrations",
key: "workflow-integrations",
@@ -57,17 +71,21 @@ export const OrgTabGroup = () => {
return (
<Tabs orientation="vertical" value={selectedTab} onValueChange={setSelectedTab}>
<TabList>
{tabs.map((tab) => (
<Tab variant="org" value={tab.key} key={tab.key}>
{tab.name}
</Tab>
))}
{tabs
.filter((el) => !el.isHidden)
.map((tab) => (
<Tab variant={isSubOrganization ? "namespace" : "org"} value={tab.key} key={tab.key}>
{tab.name}
</Tab>
))}
</TabList>
{tabs.map(({ key, component: Component }) => (
<TabPanel value={key} key={`tab-panel-${key}`}>
<Component />
</TabPanel>
))}
{tabs
.filter((el) => !el.isHidden)
.map(({ key, component: Component }) => (
<TabPanel value={key} key={`tab-panel-${key}`}>
<Component />
</TabPanel>
))}
</Tabs>
);
};
@@ -1,6 +1,5 @@
import { faPlus, faTrash, faUnlock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
@@ -70,154 +69,129 @@ export const ProjectTemplateRolesSection = ({ projectTemplate, isInfisicalTempla
return (
<div className="relative">
<AnimatePresence>
{popUp?.editRole.isOpen ? (
<motion.div
key="edit-role"
transition={{ duration: 0.3 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
className="absolute min-h-40 w-full"
>
<ProjectTemplateEditRoleForm
onGoBack={() => handlePopUpClose("editRole")}
projectTemplate={projectTemplate}
role={editRole}
isDisabled={
permission.cannot(
OrgPermissionActions.Edit,
OrgPermissionSubjects.ProjectTemplates
) ||
(editRole && !isCustomProjectRole(editRole.slug))
}
/>
<div className="h-4 w-full" />
</motion.div>
) : (
<motion.div
key="role-list"
transition={{ duration: 0.3 }}
initial={{ opacity: 0, translateX: 0 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
className="absolute w-full"
>
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<div>
<h2 className="text-lg font-medium">Project Roles</h2>
<p className="text-sm text-mineshaft-400">
{isInfisicalTemplate
? "Click a role to view the associated permissions"
: "Add, edit and remove roles for this project template"}
</p>
</div>
{!isInfisicalTemplate && (
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ProjectTemplates}
{popUp?.editRole.isOpen ? (
<ProjectTemplateEditRoleForm
onGoBack={() => handlePopUpClose("editRole")}
projectTemplate={projectTemplate}
role={editRole}
isDisabled={
permission.cannot(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates) ||
(editRole && !isCustomProjectRole(editRole.slug))
}
/>
) : (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<div>
<h2 className="text-lg font-medium">Project Roles</h2>
<p className="text-sm text-mineshaft-400">
{isInfisicalTemplate
? "Click a role to view the associated permissions"
: "Add, edit and remove roles for this project template"}
</p>
</div>
{!isInfisicalTemplate && (
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ProjectTemplates}
>
{(isAllowed) => (
<Button
onClick={() => {
handlePopUpOpen("editRole");
}}
colorSchema="primary"
className="ml-auto"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={!isAllowed}
>
{(isAllowed) => (
<Button
onClick={() => {
handlePopUpOpen("editRole");
}}
colorSchema="primary"
className="ml-auto"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={!isAllowed}
>
Add Role
</Button>
)}
</OrgPermissionCan>
Add Role
</Button>
)}
</div>
<div className="py-4">
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{roles.length ? (
roles.map((role) => {
return (
<Tr
key={role.slug}
className="group w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
role="button"
tabIndex={0}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
handlePopUpOpen("editRole", role);
}
}}
onClick={() => handlePopUpOpen("editRole", role)}
>
<Td>{role.name}</Td>
<Td>{role.slug}</Td>
<Td>
{isCustomProjectRole(role.slug) && (
<div className="flex space-x-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ProjectTemplates}
renderTooltip
allowedLabel="Remove Role"
</OrgPermissionCan>
)}
</div>
<div className="py-4">
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{roles.length ? (
roles.map((role) => {
return (
<Tr
key={role.slug}
className="group w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
role="button"
tabIndex={0}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
handlePopUpOpen("editRole", role);
}
}}
onClick={() => handlePopUpOpen("editRole", role)}
>
<Td>{role.name}</Td>
<Td>{role.slug}</Td>
<Td>
{isCustomProjectRole(role.slug) && (
<div className="flex space-x-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ProjectTemplates}
renderTooltip
allowedLabel="Remove Role"
>
{(isAllowed) => (
<IconButton
colorSchema="danger"
ariaLabel="delete-icon"
variant="plain"
className="group relative"
isDisabled={!isAllowed}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
handlePopUpOpen("removeRole", role);
}}
>
{(isAllowed) => (
<IconButton
colorSchema="danger"
ariaLabel="delete-icon"
variant="plain"
className="group relative"
isDisabled={!isAllowed}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
handlePopUpOpen("removeRole", role);
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</OrgPermissionCan>
</div>
)}
</Td>
</Tr>
);
})
) : (
<Tr>
<Td colSpan={2}>
<EmptyState title="No roles assigned to template" icon={faUnlock} />
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</OrgPermissionCan>
</div>
)}
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
</div>
<DeleteActionModal
isOpen={popUp.removeRole.isOpen}
deleteKey="remove"
title={`Are you sure you want to remove the role ${roleToDelete?.slug}?`}
onChange={(isOpen) => handlePopUpToggle("removeRole", isOpen)}
onDeleteApproved={() => handleRemoveRole(roleToDelete?.slug)}
/>
</div>
<div className="h-4 w-full" />
</motion.div>
)}
</AnimatePresence>
);
})
) : (
<Tr>
<Td colSpan={2}>
<EmptyState title="No roles assigned to template" icon={faUnlock} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
</div>
<DeleteActionModal
isOpen={popUp.removeRole.isOpen}
deleteKey="remove"
title={`Are you sure you want to remove the role ${roleToDelete?.slug}?`}
onChange={(isOpen) => handlePopUpToggle("removeRole", isOpen)}
onDeleteApproved={() => handleRemoveRole(roleToDelete?.slug)}
/>
</div>
)}
</div>
);
};
@@ -1,7 +1,6 @@
import { useState } from "react";
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { OrgPermissionCan } from "@app/components/permissions";
@@ -25,93 +24,70 @@ export const ProjectTemplatesSection = () => {
return (
<div className="relative">
<AnimatePresence>
{editTemplate ? (
<motion.div
key="edit-project-template"
transition={{ duration: 0.3 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
className="absolute min-h-40 w-full"
>
<EditProjectTemplateSection
template={editTemplate}
onBack={() => setEditTemplate(null)}
/>
</motion.div>
) : (
<motion.div
key="project-templates-list"
transition={{ duration: 0.3 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
className="absolute min-h-40 w-full"
>
<div>
<p className="mb-6 font-inter text-bunker-300">
Create and configure templates with predefined roles and environments to streamline
project setup
</p>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-start">
<p className="text-xl font-medium text-mineshaft-100">Project Templates</p>
<a
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/project-templates"
>
<div className="mt-[0.32rem] ml-2 inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
<OrgPermissionCan
I={OrgPermissionActions.Create}
a={OrgPermissionSubjects.ProjectTemplates}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (!subscription?.projectTemplates) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("addTemplate");
}}
isDisabled={!isAllowed}
className="ml-auto"
>
Add Template
</Button>
)}
</OrgPermissionCan>
{editTemplate ? (
<EditProjectTemplateSection template={editTemplate} onBack={() => setEditTemplate(null)} />
) : (
<div>
<p className="mb-6 font-inter text-bunker-300">
Create and configure templates with predefined roles and environments to streamline
project setup
</p>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-start">
<p className="text-xl font-medium text-mineshaft-100">Project Templates</p>
<a
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/project-templates"
>
<div className="mt-[0.32rem] ml-2 inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
<ProjectTemplatesTable onEdit={setEditTemplate} />
<ProjectTemplateDetailsModal
onComplete={(template) => setEditTemplate(template)}
isOpen={popUp.addTemplate.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addTemplate", isOpen)}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can create project templates if you switch to Infisical's Enterprise plan."
/>
</div>
</a>
<OrgPermissionCan
I={OrgPermissionActions.Create}
a={OrgPermissionSubjects.ProjectTemplates}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (!subscription?.projectTemplates) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("addTemplate");
}}
isDisabled={!isAllowed}
className="ml-auto"
>
Add Template
</Button>
)}
</OrgPermissionCan>
</div>
</motion.div>
)}
</AnimatePresence>
<ProjectTemplatesTable onEdit={setEditTemplate} />
<ProjectTemplateDetailsModal
onComplete={(template) => setEditTemplate(template)}
isOpen={popUp.addTemplate.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addTemplate", isOpen)}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can create project templates if you switch to Infisical's Enterprise plan."
/>
</div>
</div>
)}
</div>
);
};
@@ -46,7 +46,7 @@ const Page = withPermission(
});
const membershipId = search.membershipId as string;
const { user } = useUser();
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
const userId = user?.id || "";
const orgId = currentOrg?.id || "";
@@ -131,7 +131,7 @@ const Page = withPermission(
Users
</Link>
<PageHeader
scope="org"
scope={isSubOrganization ? "namespace" : "org"}
title={
membership.user.firstName || membership.user.lastName
? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim()
@@ -140,7 +140,7 @@ const Page = withPermission(
membership.inviteEmail ??
"Unknown User")
}
description="Organization User Membership"
description={`${isSubOrganization ? "Sub-" : ""}Organization User Membership`}
>
<div>
{userId !== membership.user.id && (
@@ -35,7 +35,7 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
});
const { user } = useUser();
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
const userId = user?.id || "";
const orgId = currentOrg?.id || "";
@@ -214,7 +214,8 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
<p className="text-sm text-mineshaft-300">-</p>
)}
</div>
{membership.isActive &&
{!isSubOrganization &&
membership.isActive &&
(membership.status === "invited" || membership.status === "verified") &&
membership.user.email &&
serverDetails?.emailConfigured && (
+9 -2
View File
@@ -1,7 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, retainSearchParams } from "@tanstack/react-router";
import { z } from "zod";
import { OrganizationLayout } from "@app/layouts/OrganizationLayout";
export const Route = createFileRoute("/_authenticate/_inject-org-details/_org-layout")({
component: OrganizationLayout
component: OrganizationLayout,
validateSearch: z.object({
subOrganization: z.string().optional()
}),
search: {
middlewares: [retainSearchParams(["subOrganization"])]
}
});
@@ -1,11 +1,16 @@
import { useMemo } from "react";
import { Controller, useForm } from "react-hook-form";
import { components, OptionProps } from "react-select";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { faGlobe } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { Link } from "@tanstack/react-router";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Badge,
Button,
FilterableSelect,
FormControl,
@@ -24,7 +29,7 @@ import {
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({
identity: z.object({ name: z.string(), id: z.string() }),
identity: z.object({ name: z.string(), id: z.string(), isManagedByRootOrg: z.boolean() }),
role: z.object({ name: z.string(), slug: z.string() })
});
@@ -35,6 +40,29 @@ type Props = {
handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void;
};
const Option = ({
isSelected,
children,
...props
}: OptionProps<{ name: string; id: string; isManagedByRootOrg: boolean }>) => {
return (
<components.Option isSelected={isSelected} {...props}>
<div className="flex flex-row items-center justify-between">
<p className="truncate">{children}</p>
{props.data.isManagedByRootOrg && (
<Badge variant="org" className="ml-auto cursor-pointer">
<FontAwesomeIcon icon={faGlobe} />
Managed by Root Org
</Badge>
)}
{isSelected && (
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
)}
</div>
</components.Option>
);
};
const Content = ({ popUp, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization();
const { projectId } = useProject();
@@ -131,9 +159,15 @@ const Content = ({ popUp, handlePopUpToggle }: Props) => {
value={value}
onChange={onChange}
placeholder="Select identity..."
options={filteredIdentityMembershipOrgs.map((membership) => membership.identity)}
options={filteredIdentityMembershipOrgs.map((membership) => ({
...membership.identity,
isManagedByRootOrg: membership.identity.orgId !== currentOrg.id
}))}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
components={{
Option
}}
/>
</FormControl>
)}
@@ -17,7 +17,7 @@ export const ErrorPage = ({ error }: ErrorComponentProps) => {
}
return (
<div className="flex h-full items-center justify-center">
<div className="flex h-screen items-center justify-center bg-bunker-800">
<div className="flex max-w-3xl flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-8 text-center text-mineshaft-200">
<FontAwesomeIcon icon={faBugs} className="my-2 inline text-6xl" />
<p>
@@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy, faRedo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { useSearch } from "@tanstack/react-router";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
@@ -87,6 +88,10 @@ export const ShareSecretForm = ({
const [, isCopyingSecret, setCopyTextSecret] = useTimedReset<string>({
initialState: "Copy to clipboard"
});
const subOrganization = useSearch({
strict: false,
select: (el) => el?.subOrganization
});
const publicSharedSecretCreator = useCreatePublicSharedSecret();
const privateSharedSecretCreator = useCreateSharedSecret();
@@ -148,11 +153,14 @@ export const ShareSecretForm = ({
type: "success"
});
} else {
const link = `${window.location.origin}/shared/secret/${id}`;
const link = new URL(`${window.location.origin}/shared/secret/${id}`);
if (subOrganization) {
link.searchParams.set("subOrganization", subOrganization);
}
setSecretLink(link);
setSecretLink(link.toString());
navigator.clipboard.writeText(link);
navigator.clipboard.writeText(link.toString());
setCopyTextSecret("secret");
createNotification({
+11 -1
View File
@@ -1,5 +1,6 @@
import { useEffect } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
import { createRootRouteWithContext, Outlet, useSearch } from "@tanstack/react-router";
import { NotificationContainer } from "@app/components/notifications";
import { TooltipProvider } from "@app/components/v2";
@@ -13,6 +14,15 @@ type TRouterContext = {
};
const RootPage = () => {
const subOrganization = useSearch({
strict: false,
select: (el) => el?.subOrganization
});
useEffect(() => {
queryClient.invalidateQueries();
}, [subOrganization]);
return (
<QueryClientProvider client={queryClient}>
<TooltipProvider>
+1 -31
View File
@@ -56,7 +56,6 @@ import { Route as organizationAccessManagementPageRouteImport } from './pages/or
import { Route as adminGeneralPageRouteImport } from './pages/admin/GeneralPage/route'
import { Route as secretManagerRedirectsRedirectApprovalPageImport } from './pages/secret-manager/redirects/redirect-approval-page'
import { Route as adminResourceOverviewPageRouteImport } from './pages/admin/ResourceOverviewPage/route'
import { Route as organizationSecretSharingSettingsPageRouteImport } from './pages/organization/SecretSharingSettingsPage/route'
import { Route as organizationRoleByIDPageRouteImport } from './pages/organization/RoleByIDPage/route'
import { Route as organizationUserDetailsByIDPageRouteImport } from './pages/organization/UserDetailsByIDPage/route'
import { Route as organizationIdentityDetailsByIDPageRouteImport } from './pages/organization/IdentityDetailsByIDPage/route'
@@ -752,14 +751,6 @@ const adminResourceOverviewPageRouteRoute =
getParentRoute: () => adminLayoutRoute,
} as any)
const organizationSecretSharingSettingsPageRouteRoute =
organizationSecretSharingSettingsPageRouteImport.update({
id: '/settings',
path: '/settings',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute,
} as any)
const organizationRoleByIDPageRouteRoute =
organizationRoleByIDPageRouteImport.update({
id: '/roles/$roleId',
@@ -2625,13 +2616,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationRoleByIDPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings'
path: '/settings'
fullPath: '/organization/secret-sharing/settings'
preLoaderRoute: typeof organizationSecretSharingSettingsPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport
}
'/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview': {
id: '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview'
path: '/resources/overview'
@@ -4033,15 +4017,12 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildr
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren {
organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute
organizationSecretSharingSettingsPageRouteRoute: typeof organizationSecretSharingSettingsPageRouteRoute
}
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren =
{
organizationSecretSharingPageRouteRoute:
organizationSecretSharingPageRouteRoute,
organizationSecretSharingSettingsPageRouteRoute:
organizationSecretSharingSettingsPageRouteRoute,
}
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren =
@@ -5094,7 +5075,6 @@ export interface FileRoutesByFullPath {
'/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
'/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute
'/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute
'/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute
'/admin/resources/overview': typeof adminResourceOverviewPageRouteRoute
'/projects/cert-management/$projectId': typeof certManagerLayoutRouteWithChildren
'/projects/kms/$projectId': typeof kmsLayoutRouteWithChildren
@@ -5328,7 +5308,6 @@ export interface FileRoutesByTo {
'/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
'/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute
'/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute
'/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute
'/admin/resources/overview': typeof adminResourceOverviewPageRouteRoute
'/projects/cert-management/$projectId': typeof certManagerLayoutRouteWithChildren
'/projects/kms/$projectId': typeof kmsLayoutRouteWithChildren
@@ -5568,7 +5547,6 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute
'/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview': typeof adminResourceOverviewPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRouteWithChildren
@@ -5818,7 +5796,6 @@ export interface FileRouteTypes {
| '/organization/identities/$identityId'
| '/organization/members/$membershipId'
| '/organization/roles/$roleId'
| '/organization/secret-sharing/settings'
| '/admin/resources/overview'
| '/projects/cert-management/$projectId'
| '/projects/kms/$projectId'
@@ -6051,7 +6028,6 @@ export interface FileRouteTypes {
| '/organization/identities/$identityId'
| '/organization/members/$membershipId'
| '/organization/roles/$roleId'
| '/organization/secret-sharing/settings'
| '/admin/resources/overview'
| '/projects/cert-management/$projectId'
| '/projects/kms/$projectId'
@@ -6289,7 +6265,6 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId'
| '/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId'
| '/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId'
| '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings'
| '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId'
@@ -6814,8 +6789,7 @@ export const routeTree = rootRoute
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization",
"children": [
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings"
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/"
]
},
"/_authenticate/_inject-org-details/_org-layout/organization/settings": {
@@ -6865,10 +6839,6 @@ export const routeTree = rootRoute
"filePath": "organization/RoleByIDPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
},
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings": {
"filePath": "organization/SecretSharingSettingsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
},
"/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview": {
"filePath": "admin/ResourceOverviewPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/admin/_admin-layout"
+1 -4
View File
@@ -21,10 +21,7 @@ const organizationRoutes = route("/organization", [
route("/access-management", "organization/AccessManagementPage/route.tsx"),
route("/audit-logs", "organization/AuditLogsPage/route.tsx"),
route("/billing", "organization/BillingPage/route.tsx"),
route("/secret-sharing", [
index("organization/SecretSharingPage/route.tsx"),
route("/settings", "organization/SecretSharingSettingsPage/route.tsx")
]),
route("/secret-sharing", [index("organization/SecretSharingPage/route.tsx")]),
route("/settings", [
index("organization/SettingsPage/route.tsx"),
route("/oauth/callback", "organization/SettingsPage/OauthCallbackPage/route.tsx")