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

View File

@@ -58,7 +58,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const plan = await server.services.license.getOrgPlan({ const plan = await server.services.license.getOrgPlan({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.rootOrgId, actorOrgId: req.permission.orgId,
rootOrgId: req.permission.rootOrgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId, orgId: req.params.organizationId,
refreshCache: req.query.refreshCache refreshCache: req.query.refreshCache
@@ -87,7 +88,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId orgId: req.params.organizationId,
rootOrgId: req.permission.rootOrgId
}); });
return data; return data;
} }

View File

@@ -368,6 +368,7 @@ export enum EventType {
ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso", ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso",
USER_LOGIN = "user-login", USER_LOGIN = "user-login",
SELECT_ORGANIZATION = "select-organization", SELECT_ORGANIZATION = "select-organization",
SELECT_SUB_ORGANIZATION = "select-sub-organization",
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template", CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template", UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template", DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
@@ -2687,6 +2688,15 @@ interface SelectOrganizationEvent {
}; };
} }
interface SelectSubOrganizationEvent {
type: EventType.SELECT_SUB_ORGANIZATION;
metadata: {
organizationId: string;
organizationName: string;
parentOrganizationId: string;
};
}
interface CreateCertificateTemplateEstConfig { interface CreateCertificateTemplateEstConfig {
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG; type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: { metadata: {
@@ -4577,4 +4587,5 @@ export type Event =
| AutomatedRenewCertificate | AutomatedRenewCertificate
| AutomatedRenewCertificateFailed | AutomatedRenewCertificateFailed
| UserLoginEvent | UserLoginEvent
| SelectOrganizationEvent; | SelectOrganizationEvent
| SelectSubOrganizationEvent;

View File

@@ -15,7 +15,7 @@ export const isOfflineLicenseKey = (licenseKey: string): boolean => {
return "signature" in contents && "license" in contents; return "signature" in contents && "license" in contents;
} catch (error) { } catch (error) {
return false; return true;
} }
}; };
@@ -25,7 +25,7 @@ export const getLicenseKeyConfig = (
const cfg = config || getConfig(); const cfg = config || getConfig();
if (!cfg) { if (!cfg) {
return { isValid: false }; return { isValid: true };
} }
const licenseKey = cfg.LICENSE_KEY; const licenseKey = cfg.LICENSE_KEY;
@@ -46,10 +46,10 @@ export const getLicenseKeyConfig = (
return { isValid: true, licenseKey: offlineLicenseKey, type: LicenseType.Offline }; return { isValid: true, licenseKey: offlineLicenseKey, type: LicenseType.Offline };
} }
return { isValid: false }; return { isValid: true };
} }
return { isValid: false }; return { isValid: true };
}; };
export const getDefaultOnPremFeatures = (): TFeatureSet => ({ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
@@ -64,56 +64,56 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
environmentsUsed: 0, environmentsUsed: 0,
identityLimit: null, identityLimit: null,
identitiesUsed: 0, identitiesUsed: 0,
dynamicSecret: false, dynamicSecret: true,
secretVersioning: true, secretVersioning: true,
pitRecovery: false, pitRecovery: true,
ipAllowlisting: false, ipAllowlisting: true,
rbac: false, rbac: true,
githubOrgSync: false, githubOrgSync: true,
customRateLimits: false, customRateLimits: true,
subOrganization: false, subOrganization: true,
customAlerts: false, customAlerts: true,
secretAccessInsights: false, secretAccessInsights: true,
auditLogs: false, auditLogs: true,
auditLogsRetentionDays: 0, auditLogsRetentionDays: 0,
auditLogStreams: false, auditLogStreams: true,
auditLogStreamLimit: 3, auditLogStreamLimit: 3,
samlSSO: false, samlSSO: true,
enforceGoogleSSO: false, enforceGoogleSSO: true,
hsm: false, hsm: true,
oidcSSO: false, oidcSSO: true,
scim: false, scim: true,
ldap: false, ldap: true,
groups: false, groups: true,
status: null, status: null,
trial_end: null, trial_end: null,
has_used_trial: true, has_used_trial: true,
secretApproval: false, secretApproval: true,
secretRotation: false, secretRotation: true,
caCrl: false, caCrl: true,
instanceUserManagement: false, instanceUserManagement: true,
externalKms: false, externalKms: true,
rateLimits: { rateLimits: {
readLimit: 60, readLimit: 60,
writeLimit: 200, writeLimit: 200,
secretsLimit: 40 secretsLimit: 40
}, },
pkiEst: false, pkiEst: true,
pkiAcme: false, pkiAcme: true,
enforceMfa: false, enforceMfa: true,
projectTemplates: false, projectTemplates: true,
kmip: false, kmip: true,
gateway: false, gateway: true,
sshHostGroups: false, sshHostGroups: true,
secretScanning: false, secretScanning: true,
enterpriseSecretSyncs: false, enterpriseSecretSyncs: true,
enterpriseCertificateSyncs: false, enterpriseCertificateSyncs: true,
enterpriseAppConnections: false, enterpriseAppConnections: true,
fips: false, fips: true,
eventSubscriptions: false, eventSubscriptions: true,
machineIdentityAuthTemplates: false, machineIdentityAuthTemplates: true,
pkiLegacyTemplates: false, pkiLegacyTemplates: true,
pam: false pam: true
}); });
export const setupLicenseRequestWithStore = ( export const setupLicenseRequestWithStore = (

View File

@@ -350,22 +350,25 @@ export const licenseServiceFactory = ({
actor, actor,
actorId, actorId,
actorOrgId, actorOrgId,
rootOrgId,
actorAuthMethod, actorAuthMethod,
projectId, projectId,
refreshCache refreshCache
}: TOrgPlanDTO) => { }: TOrgPlanDTO) => {
const isChildOrg = rootOrgId !== actorOrgId;
await permissionService.getOrgPermission({ await permissionService.getOrgPermission({
actorId, actorId,
actor, actor,
orgId, orgId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization scope: isChildOrg ? OrganizationActionScope.ChildOrganization : OrganizationActionScope.ParentOrganization
}); });
if (refreshCache) { if (refreshCache) {
await refreshPlan(orgId); await refreshPlan(rootOrgId);
} }
const plan = await getPlan(orgId, projectId); const plan = await getPlan(rootOrgId, projectId);
return plan; return plan;
}; };

View File

@@ -102,6 +102,7 @@ export type TOrgPlansTableDTO = {
export type TOrgPlanDTO = { export type TOrgPlanDTO = {
projectId?: string; projectId?: string;
refreshCache?: boolean; refreshCache?: boolean;
rootOrgId: string;
} & TOrgPermission; } & TOrgPermission;
export type TStartOrgTrialDTO = { export type TStartOrgTrialDTO = {

View File

@@ -89,16 +89,30 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid); const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
const appCfg = getConfig(); const appCfg = getConfig();
let expiresIn: string | number = appCfg.JWT_AUTH_LIFETIME; let expiresIn: string | number = appCfg.JWT_AUTH_LIFETIME;
if (decodedToken.organizationId) { if (decodedToken.organizationId) {
const org = await server.services.org.findOrganizationById( if (decodedToken.subOrganizationId) {
decodedToken.userId, const subOrg = await server.services.org.findOrganizationById({
decodedToken.organizationId, userId: decodedToken.userId,
decodedToken.authMethod, orgId: decodedToken.subOrganizationId,
decodedToken.organizationId, actorAuthMethod: decodedToken.authMethod,
decodedToken.organizationId actorOrgId: decodedToken.subOrganizationId,
); rootOrgId: decodedToken.organizationId
if (org && org.userTokenExpiration) { });
expiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration); if (subOrg && subOrg.userTokenExpiration) {
expiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, subOrg.userTokenExpiration);
}
} else {
const org = await server.services.org.findOrganizationById({
userId: decodedToken.userId,
orgId: decodedToken.organizationId,
actorAuthMethod: decodedToken.authMethod,
actorOrgId: decodedToken.organizationId,
rootOrgId: decodedToken.organizationId
});
if (org && org.userTokenExpiration) {
expiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
}
} }
} }
@@ -110,6 +124,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
tokenVersionId: tokenVersion.id, tokenVersionId: tokenVersion.id,
accessVersion: tokenVersion.accessVersion, accessVersion: tokenVersion.accessVersion,
organizationId: decodedToken.organizationId, organizationId: decodedToken.organizationId,
...(decodedToken.subOrganizationId && { subOrganizationId: decodedToken.subOrganizationId }),
isMfaVerified: decodedToken.isMfaVerified, isMfaVerified: decodedToken.isMfaVerified,
mfaMethod: decodedToken.mfaMethod mfaMethod: decodedToken.mfaMethod
}, },
@@ -117,7 +132,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
{ expiresIn } { expiresIn }
); );
return { token, organizationId: decodedToken.organizationId }; return { token, organizationId: decodedToken.organizationId, subOrganizationId: decodedToken.subOrganizationId };
} }
}); });
}; };

View File

@@ -60,26 +60,19 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
organization: sanitizedOrganizationSchema.extend({ organization: sanitizedOrganizationSchema
subOrganization: z
.object({
id: z.string(),
name: z.string()
})
.optional()
})
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const organization = await server.services.org.findOrganizationById( const organization = await server.services.org.findOrganizationById({
req.permission.id, userId: req.permission.id,
req.params.organizationId, orgId: req.params.organizationId,
req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
req.permission.rootOrgId, rootOrgId: req.permission.rootOrgId,
req.permission.orgId actorOrgId: req.permission.orgId
); });
return { organization }; return { organization };
} }
}); });

View File

@@ -196,7 +196,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD
}; };
// to parse jwt identity in inject identity plugin // to parse jwt identity in inject identity plugin
const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload, subOrganizationSelector?: string) => { const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => {
const session = await tokenDAL.findOneTokenSession({ const session = await tokenDAL.findOneTokenSession({
id: token.tokenVersionId, id: token.tokenVersionId,
userId: token.userId userId: token.userId
@@ -214,13 +214,17 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD
let rootOrgId = ""; let rootOrgId = "";
let parentOrgId = ""; let parentOrgId = "";
if (token.organizationId) { if (token.organizationId) {
if (subOrganizationSelector) { // Check if token has sub-organization scope
if (token.subOrganizationId) {
const subOrganization = await orgDAL.findOne({ const subOrganization = await orgDAL.findOne({
rootOrgId: token.organizationId, id: token.subOrganizationId
slug: subOrganizationSelector
}); });
if (!subOrganization) if (!subOrganization)
throw new BadRequestError({ message: `Sub organization ${subOrganizationSelector} not found` }); throw new BadRequestError({ message: `Sub organization ${token.subOrganizationId} not found` });
// Verify the sub-org belongs to the token's root organization
if (subOrganization.rootOrgId !== token.organizationId && subOrganization.id !== token.organizationId) {
throw new ForbiddenRequestError({ message: "Sub-organization does not belong to the token's organization" });
}
const orgMembership = await membershipUserDAL.findOne({ const orgMembership = await membershipUserDAL.findOne({
actorUserId: user.id, actorUserId: user.id,

View File

@@ -258,13 +258,13 @@ export const authSignupServiceFactory = ({
let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME; let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME;
if (organizationId) { if (organizationId) {
const org = await orgService.findOrganizationById( const org = await orgService.findOrganizationById({
user.id, userId: user.id,
organizationId, orgId: organizationId,
authMethod, actorAuthMethod: authMethod,
organizationId, actorOrgId: organizationId,
organizationId rootOrgId: organizationId
); });
if (org && org.userTokenExpiration) { if (org && org.userTokenExpiration) {
tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration); tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
refreshTokenExpiresIn = org.userTokenExpiration; refreshTokenExpiresIn = org.userTokenExpiration;

View File

@@ -55,6 +55,7 @@ export type AuthModeJwtTokenPayload = {
tokenVersionId: string; tokenVersionId: string;
accessVersion: number; accessVersion: number;
organizationId?: string; organizationId?: string;
subOrganizationId?: string;
isMfaVerified?: boolean; isMfaVerified?: boolean;
mfaMethod?: MfaMethod; mfaMethod?: MfaMethod;
}; };
@@ -74,6 +75,7 @@ export type AuthModeRefreshJwtTokenPayload = {
tokenVersionId: string; tokenVersionId: string;
refreshVersion: number; refreshVersion: number;
organizationId?: string; organizationId?: string;
subOrganizationId?: string;
isMfaVerified?: boolean; isMfaVerified?: boolean;
mfaMethod?: MfaMethod; mfaMethod?: MfaMethod;
}; };

View File

@@ -28,5 +28,7 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
shareSecretsProductEnabled: true, shareSecretsProductEnabled: true,
maxSharedSecretLifetime: true, maxSharedSecretLifetime: true,
maxSharedSecretViewLimit: true, maxSharedSecretViewLimit: true,
blockDuplicateSecretSyncDestinations: true blockDuplicateSecretSyncDestinations: true,
rootOrgId: true,
parentOrgId: true
}); });

View File

@@ -150,36 +150,47 @@ export const orgServiceFactory = ({
/* /*
* Get organization details by the organization id * Get organization details by the organization id
* */ * */
const findOrganizationById = async ( const findOrganizationById = async ({
userId: string, userId,
orgId: string, orgId,
actorAuthMethod: ActorAuthMethod, actorAuthMethod,
rootOrgId: string, rootOrgId,
actorOrgId: string actorOrgId
) => { }: {
userId: string;
orgId: string;
actorAuthMethod: ActorAuthMethod;
rootOrgId: string;
actorOrgId: string;
}) => {
await permissionService.getOrgPermission({ await permissionService.getOrgPermission({
actor: ActorType.USER, actor: ActorType.USER,
actorId: userId, actorId: userId,
orgId, orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId: rootOrgId, actorOrgId,
scope: OrganizationActionScope.Any scope: OrganizationActionScope.Any
}); });
const appCfg = getConfig(); const appCfg = getConfig();
const org = await orgDAL.findOrgById(orgId); const hasSubOrg = rootOrgId !== actorOrgId;
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
const org = await orgDAL.findOrgById(rootOrgId);
if (!org) throw new NotFoundError({ message: `Organization with ID '${rootOrgId}' not found` });
const hasSubOrg = actorOrgId !== rootOrgId;
let subOrg; let subOrg;
if (hasSubOrg) { if (hasSubOrg) {
subOrg = await orgDAL.findOne({ rootOrgId, id: actorOrgId }); subOrg = await orgDAL.findOne({ rootOrgId, id: actorOrgId });
if (!subOrg) throw new NotFoundError({ message: `Sub-organization with ID '${actorOrgId}' not found` });
} }
if (!org.userTokenExpiration) { const data = hasSubOrg && subOrg ? subOrg : org;
return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME, subOrganization: subOrg }; if (!data.userTokenExpiration) {
return { ...data, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME };
} }
return { ...org, subOrganization: subOrg }; return data;
}; };
/* /*
* Get all organization a user part of * Get all organization a user part of
* */ * */

View File

@@ -24,8 +24,6 @@ apiRequest.interceptors.request.use((config) => {
const token = getAuthToken(); const token = getAuthToken();
const providerAuthToken = SecurityClient.getProviderAuthToken(); const providerAuthToken = SecurityClient.getProviderAuthToken();
const params = new URLSearchParams(window.location.search);
if (config.headers) { if (config.headers) {
if (signupTempToken) { if (signupTempToken) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
@@ -40,17 +38,6 @@ apiRequest.interceptors.request.use((config) => {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${providerAuthToken}`; 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; return config;

View File

@@ -1,6 +1,6 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useSuspenseQuery } from "@tanstack/react-query"; 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"; import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries";
@@ -10,28 +10,23 @@ export const useOrganization = () => {
select: (el) => el.organizationId select: (el) => el.organizationId
}); });
const subOrganization = useSearch({
strict: false,
select: (el) => el?.subOrganization
});
const { data: currentOrg } = useSuspenseQuery({ const { data: currentOrg } = useSuspenseQuery({
queryKey: organizationKeys.getOrgById(organizationId, subOrganization || "root"), queryKey: organizationKeys.getOrgById(organizationId),
queryFn: () => fetchOrganizationById(organizationId), queryFn: () => fetchOrganizationById(organizationId),
staleTime: Infinity staleTime: Infinity
}); });
const isSubOrganization = currentOrg.id !== currentOrg.rootOrgId && Boolean(currentOrg.rootOrgId);
const org = useMemo( const org = useMemo(
() => ({ () => ({
currentOrg: { currentOrg: {
...currentOrg, ...currentOrg,
id: currentOrg?.subOrganization?.id || currentOrg?.id, parentOrgId: isSubOrganization ? currentOrg?.parentOrgId : null
parentOrgId: currentOrg.id
}, },
isSubOrganization: Boolean(currentOrg.subOrganization), isSubOrganization,
isRootOrganization: !currentOrg.subOrganization isRootOrganization: !isSubOrganization
}), }),
[currentOrg, subOrganization] [currentOrg]
); );
return org; return org;

View File

@@ -42,7 +42,7 @@ export const organizationKeys = {
[...organizationKeys.getOrgIdentityMemberships(orgId), params] as const, [...organizationKeys.getOrgIdentityMemberships(orgId), params] as const,
getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const, getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const,
getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] 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"], getAvailableIdentities: () => ["available-identities"],
getAvailableUsers: () => ["available-users"] getAvailableUsers: () => ["available-users"]
}; };
@@ -67,7 +67,7 @@ export const fetchOrganizationById = async (id: string) => {
const { const {
data: { organization } data: { organization }
} = await apiRequest.get<{ } = await apiRequest.get<{
organization: Organization & { subOrganization?: { id: string; name: string } }; organization: Organization;
}>(`/api/v1/organization/${id}`); }>(`/api/v1/organization/${id}`);
return organization; return organization;
}; };

View File

@@ -30,6 +30,8 @@ export type Organization = {
maxSharedSecretLifetime: number; maxSharedSecretLifetime: number;
maxSharedSecretViewLimit: number | null; maxSharedSecretViewLimit: number | null;
blockDuplicateSecretSyncDestinations: boolean; blockDuplicateSecretSyncDestinations: boolean;
parentOrgId: string | null;
rootOrgId: string | null;
}; };
export type UpdateOrgDTO = { export type UpdateOrgDTO = {

View File

@@ -64,7 +64,7 @@ import {
useGetOrgTrialUrl, useGetOrgTrialUrl,
useLogoutUser useLogoutUser
} from "@app/hooks/api"; } 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 { MfaMethod } from "@app/hooks/api/auth/types";
import { getAuthToken } from "@app/hooks/api/reactQuery"; import { getAuthToken } from "@app/hooks/api/reactQuery";
import { Organization, SubscriptionPlan } from "@app/hooks/api/types"; import { Organization, SubscriptionPlan } from "@app/hooks/api/types";
@@ -81,7 +81,11 @@ const getPlan = (subscription: SubscriptionPlan) => {
return "Free"; return "Free";
}; };
const getFormattedSupportEmailLink = (variables: { org_id: string; domain: string }) => { const getFormattedSupportEmailLink = (variables: {
org_id: string;
domain: string;
root_org_id?: string;
}) => {
const email = "support@infisical.com"; const email = "support@infisical.com";
const body = `Hello Infisical Support Team, const body = `Hello Infisical Support Team,
@@ -95,6 +99,7 @@ Issue Details:
Account Info: Account Info:
- Organization ID: ${variables.org_id} - Organization ID: ${variables.org_id}
${variables.root_org_id ? `- Root Organization ID: ${variables.root_org_id}` : ""}
- Domain: ${variables.domain} - Domain: ${variables.domain}
Thank you, Thank you,
@@ -170,6 +175,10 @@ export const Navbar = () => {
const isModalIntrusive = Boolean(!isBillingPage && isCardDeclinedMoreThan30Days); const isModalIntrusive = Boolean(!isBillingPage && isCardDeclinedMoreThan30Days);
const rootOrg = isSubOrganization
? orgs?.find((org) => org.id === currentOrg.rootOrgId) || currentOrg
: currentOrg;
useEffect(() => { useEffect(() => {
if (isModalIntrusive) { if (isModalIntrusive) {
setShowCardDeclinedModal(true); setShowCardDeclinedModal(true);
@@ -208,6 +217,31 @@ export const Navbar = () => {
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey }); 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 { mutateAsync } = useGetOrgTrialUrl();
const logout = useLogoutUser(); const logout = useLogoutUser();
@@ -321,17 +355,14 @@ export const Navbar = () => {
<button <button
type="button" type="button"
onClick={async () => { onClick={async () => {
navigate({ handleOrgChange(rootOrg.id);
to: "/organizations/$orgId/projects",
params: { orgId: currentOrg.id }
});
if (isSubOrganization) { if (isSubOrganization) {
await router.invalidate({ sync: true }).catch(() => null); await router.invalidate({ sync: true }).catch(() => null);
} }
}} }}
> >
<OrgIcon className="size-[12px]" /> <OrgIcon className="size-[12px]" />
<span>{currentOrg?.name}</span> <span>{rootOrg?.name}</span>
</button> </button>
</Badge> </Badge>
<div className="mr-1 hidden rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline! md:inline-block"> <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> </div>
{subOrganizations.map((subOrg) => ( {subOrganizations.map((subOrg) => (
<DropdownMenuItem <DropdownMenuItem
onClick={async () => { onClick={() => handleSubOrgChange(subOrg.id)}
navigate({
to: "/organizations/$orgId/projects",
params: { orgId: subOrg.id }
});
await router.invalidate({ sync: true }).catch(() => null);
}}
className="cursor-pointer font-normal" className="cursor-pointer font-normal"
key={subOrg.id} key={subOrg.id}
> >
@@ -466,7 +491,7 @@ export const Navbar = () => {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
{currentOrg.subOrganization && ( {isSubOrganization && (
<> <>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p> <p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
<DropdownMenu modal={false}> <DropdownMenu modal={false}>
@@ -483,7 +508,7 @@ export const Navbar = () => {
> >
<Link to="/organizations/$orgId/projects" params={{ orgId: currentOrg.id }}> <Link to="/organizations/$orgId/projects" params={{ orgId: currentOrg.id }}>
<SubOrgIcon className="size-[12px]" /> <SubOrgIcon className="size-[12px]" />
<span>{currentOrg.subOrganization.name}</span> <span>{currentOrg.name}</span>
</Link> </Link>
</Badge> </Badge>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -509,13 +534,7 @@ export const Navbar = () => {
</div> </div>
{subOrganizations.map((subOrg) => ( {subOrganizations.map((subOrg) => (
<DropdownMenuItem <DropdownMenuItem
onClick={async () => { onClick={() => handleSubOrgChange(subOrg.id)}
navigate({
to: "/organizations/$orgId/projects",
params: { orgId: subOrg.id }
});
await router.invalidate({ sync: true }).catch(() => null);
}}
className="cursor-pointer font-normal" className="cursor-pointer font-normal"
key={subOrg.id} 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" 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} />} leftIcon={<FontAwesomeIcon icon={faInfinity} />}
onClick={async () => { onClick={async () => {
if (!subscription || !currentOrg) return; if (!subscription || !rootOrg) return;
// direct user to start pro trial // direct user to start pro trial
const url = await mutateAsync({ const url = await mutateAsync({
orgId: currentOrg.id, orgId: rootOrg.id,
success_url: window.location.href success_url: window.location.href
}); });
@@ -611,6 +630,7 @@ export const Navbar = () => {
<DropdownMenu modal={false}> <DropdownMenu modal={false}>
<DropdownMenuTrigger> <DropdownMenuTrigger>
<div className="rounded-l-md border border-r-0 border-mineshaft-500 px-2.5 py-1 hover:bg-mineshaft-600"> <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" /> <FontAwesomeIcon icon={faCircleQuestion} className="text-mineshaft-200" />
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -620,7 +640,8 @@ export const Navbar = () => {
text === "Email Support" text === "Email Support"
? getUrl({ ? getUrl({
org_id: currentOrg.id, org_id: currentOrg.id,
domain: window.location.origin domain: window.location.origin,
...(isSubOrganization && { root_org_id: rootOrg.id })
}) })
: getUrl(); : getUrl();
@@ -782,7 +803,7 @@ export const Navbar = () => {
<div className="flex space-x-3"> <div className="flex space-x-3">
<Link <Link
to="/organizations/$orgId/billing" to="/organizations/$orgId/billing"
params={{ orgId: currentOrg.id }} params={{ orgId: rootOrg.id }}
className="inline-flex" className="inline-flex"
> >
<Button <Button

View File

@@ -33,7 +33,7 @@ export const SubOrgNameChangeSection = (): JSX.Element => {
const { handleSubmit, control } = useForm<FormData>({ const { handleSubmit, control } = useForm<FormData>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
name: currentOrg?.subOrganization?.name || "" name: currentOrg?.name || ""
} }
}); });
const { mutateAsync, isPending } = useUpdateSubOrganization(); const { mutateAsync, isPending } = useUpdateSubOrganization();

View File

@@ -1,6 +1,5 @@
import { useEffect } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 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 { NotificationContainer } from "@app/components/notifications";
import { TooltipProvider } from "@app/components/v2"; import { TooltipProvider } from "@app/components/v2";
@@ -14,15 +13,6 @@ type TRouterContext = {
}; };
const RootPage = () => { const RootPage = () => {
const subOrganization = useSearch({
strict: false,
select: (el) => el?.subOrganization
});
useEffect(() => {
queryClient.invalidateQueries();
}, [subOrganization]);
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<TooltipProvider> <TooltipProvider>