feat: adds suborg scoped token

This commit is contained in:
Piyush Gupta
2025-11-24 19:18:47 +05:30
parent 9dd7e8d675
commit 1f2a764d3e
19 changed files with 206 additions and 167 deletions
-13
View File
@@ -24,8 +24,6 @@ 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
@@ -40,17 +38,6 @@ 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;
@@ -1,6 +1,6 @@
import { useMemo } from "react";
import { useSuspenseQuery } from "@tanstack/react-query";
import { useRouteContext, useSearch } from "@tanstack/react-router";
import { useRouteContext } from "@tanstack/react-router";
import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries";
@@ -10,28 +10,23 @@ export const useOrganization = () => {
select: (el) => el.organizationId
});
const subOrganization = useSearch({
strict: false,
select: (el) => el?.subOrganization
});
const { data: currentOrg } = useSuspenseQuery({
queryKey: organizationKeys.getOrgById(organizationId, subOrganization || "root"),
queryKey: organizationKeys.getOrgById(organizationId),
queryFn: () => fetchOrganizationById(organizationId),
staleTime: Infinity
});
const isSubOrganization = currentOrg.id !== currentOrg.rootOrgId && Boolean(currentOrg.rootOrgId);
const org = useMemo(
() => ({
currentOrg: {
...currentOrg,
id: currentOrg?.subOrganization?.id || currentOrg?.id,
parentOrgId: currentOrg.id
parentOrgId: isSubOrganization ? currentOrg?.parentOrgId : null
},
isSubOrganization: Boolean(currentOrg.subOrganization),
isRootOrganization: !currentOrg.subOrganization
isSubOrganization,
isRootOrganization: !isSubOrganization
}),
[currentOrg, subOrganization]
[currentOrg]
);
return org;
@@ -42,7 +42,7 @@ 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, subOrg?: string) => ["organization", { orgId, subOrg }],
getOrgById: (orgId: string) => ["organization", { orgId }],
getAvailableIdentities: () => ["available-identities"],
getAvailableUsers: () => ["available-users"]
};
@@ -67,7 +67,7 @@ export const fetchOrganizationById = async (id: string) => {
const {
data: { organization }
} = await apiRequest.get<{
organization: Organization & { subOrganization?: { id: string; name: string } };
organization: Organization;
}>(`/api/v1/organization/${id}`);
return organization;
};
@@ -30,6 +30,8 @@ export type Organization = {
maxSharedSecretLifetime: number;
maxSharedSecretViewLimit: number | null;
blockDuplicateSecretSyncDestinations: boolean;
parentOrgId: string | null;
rootOrgId: string | null;
};
export type UpdateOrgDTO = {
@@ -64,7 +64,7 @@ import {
useGetOrgTrialUrl,
useLogoutUser
} from "@app/hooks/api";
import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries";
import { authKeys, selectOrganization, selectSubOrganization } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { getAuthToken } from "@app/hooks/api/reactQuery";
import { Organization, SubscriptionPlan } from "@app/hooks/api/types";
@@ -81,7 +81,11 @@ const getPlan = (subscription: SubscriptionPlan) => {
return "Free";
};
const getFormattedSupportEmailLink = (variables: { org_id: string; domain: string }) => {
const getFormattedSupportEmailLink = (variables: {
org_id: string;
domain: string;
root_org_id?: string;
}) => {
const email = "[email protected]";
const body = `Hello Infisical Support Team,
@@ -95,6 +99,7 @@ Issue Details:
Account Info:
- Organization ID: ${variables.org_id}
${variables.root_org_id ? `- Root Organization ID: ${variables.root_org_id}` : ""}
- Domain: ${variables.domain}
Thank you,
@@ -170,6 +175,10 @@ export const Navbar = () => {
const isModalIntrusive = Boolean(!isBillingPage && isCardDeclinedMoreThan30Days);
const rootOrg = isSubOrganization
? orgs?.find((org) => org.id === currentOrg.rootOrgId) || currentOrg
: currentOrg;
useEffect(() => {
if (isModalIntrusive) {
setShowCardDeclinedModal(true);
@@ -208,6 +217,31 @@ export const Navbar = () => {
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey });
};
const handleSubOrgChange = async (subOrgId: string) => {
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
const { token, isMfaEnabled, mfaMethod } = await selectSubOrganization({
subOrganizationId: subOrgId
});
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => () => handleSubOrgChange(subOrgId));
return;
}
await router.invalidate();
navigate({
to: "/organizations/$orgId/projects",
params: { orgId: subOrgId }
});
};
const { mutateAsync } = useGetOrgTrialUrl();
const logout = useLogoutUser();
@@ -321,17 +355,14 @@ export const Navbar = () => {
<button
type="button"
onClick={async () => {
navigate({
to: "/organizations/$orgId/projects",
params: { orgId: currentOrg.id }
});
handleOrgChange(rootOrg.id);
if (isSubOrganization) {
await router.invalidate({ sync: true }).catch(() => null);
}
}}
>
<OrgIcon className="size-[12px]" />
<span>{currentOrg?.name}</span>
<span>{rootOrg?.name}</span>
</button>
</Badge>
<div className="mr-1 hidden rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline! md:inline-block">
@@ -405,13 +436,7 @@ export const Navbar = () => {
</div>
{subOrganizations.map((subOrg) => (
<DropdownMenuItem
onClick={async () => {
navigate({
to: "/organizations/$orgId/projects",
params: { orgId: subOrg.id }
});
await router.invalidate({ sync: true }).catch(() => null);
}}
onClick={() => handleSubOrgChange(subOrg.id)}
className="cursor-pointer font-normal"
key={subOrg.id}
>
@@ -466,7 +491,7 @@ export const Navbar = () => {
</DropdownMenuContent>
</DropdownMenu>
</div>
{currentOrg.subOrganization && (
{isSubOrganization && (
<>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
<DropdownMenu modal={false}>
@@ -483,7 +508,7 @@ export const Navbar = () => {
>
<Link to="/organizations/$orgId/projects" params={{ orgId: currentOrg.id }}>
<SubOrgIcon className="size-[12px]" />
<span>{currentOrg.subOrganization.name}</span>
<span>{currentOrg.name}</span>
</Link>
</Badge>
<DropdownMenuTrigger asChild>
@@ -509,13 +534,7 @@ export const Navbar = () => {
</div>
{subOrganizations.map((subOrg) => (
<DropdownMenuItem
onClick={async () => {
navigate({
to: "/organizations/$orgId/projects",
params: { orgId: subOrg.id }
});
await router.invalidate({ sync: true }).catch(() => null);
}}
onClick={() => handleSubOrgChange(subOrg.id)}
className="cursor-pointer font-normal"
key={subOrg.id}
>
@@ -562,11 +581,11 @@ export const Navbar = () => {
className="mr-2 border-mineshaft-500 px-2.5 py-1.5 whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
leftIcon={<FontAwesomeIcon icon={faInfinity} />}
onClick={async () => {
if (!subscription || !currentOrg) return;
if (!subscription || !rootOrg) return;
// direct user to start pro trial
const url = await mutateAsync({
orgId: currentOrg.id,
orgId: rootOrg.id,
success_url: window.location.href
});
@@ -611,6 +630,7 @@ export const Navbar = () => {
<DropdownMenu modal={false}>
<DropdownMenuTrigger>
<div className="rounded-l-md border border-r-0 border-mineshaft-500 px-2.5 py-1 hover:bg-mineshaft-600">
aple
<FontAwesomeIcon icon={faCircleQuestion} className="text-mineshaft-200" />
</div>
</DropdownMenuTrigger>
@@ -620,7 +640,8 @@ export const Navbar = () => {
text === "Email Support"
? getUrl({
org_id: currentOrg.id,
domain: window.location.origin
domain: window.location.origin,
...(isSubOrganization && { root_org_id: rootOrg.id })
})
: getUrl();
@@ -782,7 +803,7 @@ export const Navbar = () => {
<div className="flex space-x-3">
<Link
to="/organizations/$orgId/billing"
params={{ orgId: currentOrg.id }}
params={{ orgId: rootOrg.id }}
className="inline-flex"
>
<Button
@@ -33,7 +33,7 @@ export const SubOrgNameChangeSection = (): JSX.Element => {
const { handleSubmit, control } = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
name: currentOrg?.subOrganization?.name || ""
name: currentOrg?.name || ""
}
});
const { mutateAsync, isPending } = useUpdateSubOrganization();
+1 -11
View File
@@ -1,6 +1,5 @@
import { useEffect } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRootRouteWithContext, Outlet, useSearch } from "@tanstack/react-router";
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
import { NotificationContainer } from "@app/components/notifications";
import { TooltipProvider } from "@app/components/v2";
@@ -14,15 +13,6 @@ type TRouterContext = {
};
const RootPage = () => {
const subOrganization = useSearch({
strict: false,
select: (el) => el?.subOrganization
});
useEffect(() => {
queryClient.invalidateQueries();
}, [subOrganization]);
return (
<QueryClientProvider client={queryClient}>
<TooltipProvider>