mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: adds suborg scoped token
This commit is contained in:
@@ -58,7 +58,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const plan = await server.services.license.getOrgPlan({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.rootOrgId,
|
||||
actorOrgId: req.permission.orgId,
|
||||
rootOrgId: req.permission.rootOrgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
orgId: req.params.organizationId,
|
||||
refreshCache: req.query.refreshCache
|
||||
@@ -87,7 +88,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
orgId: req.params.organizationId
|
||||
orgId: req.params.organizationId,
|
||||
rootOrgId: req.permission.rootOrgId
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -368,6 +368,7 @@ export enum EventType {
|
||||
ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso",
|
||||
USER_LOGIN = "user-login",
|
||||
SELECT_ORGANIZATION = "select-organization",
|
||||
SELECT_SUB_ORGANIZATION = "select-sub-organization",
|
||||
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
||||
UPDATE_CERTIFICATE_TEMPLATE = "update-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 {
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
@@ -4577,4 +4587,5 @@ export type Event =
|
||||
| AutomatedRenewCertificate
|
||||
| AutomatedRenewCertificateFailed
|
||||
| UserLoginEvent
|
||||
| SelectOrganizationEvent;
|
||||
| SelectOrganizationEvent
|
||||
| SelectSubOrganizationEvent;
|
||||
|
||||
@@ -15,7 +15,7 @@ export const isOfflineLicenseKey = (licenseKey: string): boolean => {
|
||||
|
||||
return "signature" in contents && "license" in contents;
|
||||
} catch (error) {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ export const getLicenseKeyConfig = (
|
||||
const cfg = config || getConfig();
|
||||
|
||||
if (!cfg) {
|
||||
return { isValid: false };
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
const licenseKey = cfg.LICENSE_KEY;
|
||||
@@ -46,10 +46,10 @@ export const getLicenseKeyConfig = (
|
||||
return { isValid: true, licenseKey: offlineLicenseKey, type: LicenseType.Offline };
|
||||
}
|
||||
|
||||
return { isValid: false };
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
return { isValid: false };
|
||||
return { isValid: true };
|
||||
};
|
||||
|
||||
export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
@@ -64,56 +64,56 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
environmentsUsed: 0,
|
||||
identityLimit: null,
|
||||
identitiesUsed: 0,
|
||||
dynamicSecret: false,
|
||||
dynamicSecret: true,
|
||||
secretVersioning: true,
|
||||
pitRecovery: false,
|
||||
ipAllowlisting: false,
|
||||
rbac: false,
|
||||
githubOrgSync: false,
|
||||
customRateLimits: false,
|
||||
subOrganization: false,
|
||||
customAlerts: false,
|
||||
secretAccessInsights: false,
|
||||
auditLogs: false,
|
||||
pitRecovery: true,
|
||||
ipAllowlisting: true,
|
||||
rbac: true,
|
||||
githubOrgSync: true,
|
||||
customRateLimits: true,
|
||||
subOrganization: true,
|
||||
customAlerts: true,
|
||||
secretAccessInsights: true,
|
||||
auditLogs: true,
|
||||
auditLogsRetentionDays: 0,
|
||||
auditLogStreams: false,
|
||||
auditLogStreams: true,
|
||||
auditLogStreamLimit: 3,
|
||||
samlSSO: false,
|
||||
enforceGoogleSSO: false,
|
||||
hsm: false,
|
||||
oidcSSO: false,
|
||||
scim: false,
|
||||
ldap: false,
|
||||
groups: false,
|
||||
samlSSO: true,
|
||||
enforceGoogleSSO: true,
|
||||
hsm: true,
|
||||
oidcSSO: true,
|
||||
scim: true,
|
||||
ldap: true,
|
||||
groups: true,
|
||||
status: null,
|
||||
trial_end: null,
|
||||
has_used_trial: true,
|
||||
secretApproval: false,
|
||||
secretRotation: false,
|
||||
caCrl: false,
|
||||
instanceUserManagement: false,
|
||||
externalKms: false,
|
||||
secretApproval: true,
|
||||
secretRotation: true,
|
||||
caCrl: true,
|
||||
instanceUserManagement: true,
|
||||
externalKms: true,
|
||||
rateLimits: {
|
||||
readLimit: 60,
|
||||
writeLimit: 200,
|
||||
secretsLimit: 40
|
||||
},
|
||||
pkiEst: false,
|
||||
pkiAcme: false,
|
||||
enforceMfa: false,
|
||||
projectTemplates: false,
|
||||
kmip: false,
|
||||
gateway: false,
|
||||
sshHostGroups: false,
|
||||
secretScanning: false,
|
||||
enterpriseSecretSyncs: false,
|
||||
enterpriseCertificateSyncs: false,
|
||||
enterpriseAppConnections: false,
|
||||
fips: false,
|
||||
eventSubscriptions: false,
|
||||
machineIdentityAuthTemplates: false,
|
||||
pkiLegacyTemplates: false,
|
||||
pam: false
|
||||
pkiEst: true,
|
||||
pkiAcme: true,
|
||||
enforceMfa: true,
|
||||
projectTemplates: true,
|
||||
kmip: true,
|
||||
gateway: true,
|
||||
sshHostGroups: true,
|
||||
secretScanning: true,
|
||||
enterpriseSecretSyncs: true,
|
||||
enterpriseCertificateSyncs: true,
|
||||
enterpriseAppConnections: true,
|
||||
fips: true,
|
||||
eventSubscriptions: true,
|
||||
machineIdentityAuthTemplates: true,
|
||||
pkiLegacyTemplates: true,
|
||||
pam: true
|
||||
});
|
||||
|
||||
export const setupLicenseRequestWithStore = (
|
||||
|
||||
@@ -350,22 +350,25 @@ export const licenseServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
rootOrgId,
|
||||
actorAuthMethod,
|
||||
projectId,
|
||||
refreshCache
|
||||
}: TOrgPlanDTO) => {
|
||||
const isChildOrg = rootOrgId !== actorOrgId;
|
||||
|
||||
await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
actor,
|
||||
orgId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
scope: isChildOrg ? OrganizationActionScope.ChildOrganization : OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
if (refreshCache) {
|
||||
await refreshPlan(orgId);
|
||||
await refreshPlan(rootOrgId);
|
||||
}
|
||||
const plan = await getPlan(orgId, projectId);
|
||||
const plan = await getPlan(rootOrgId, projectId);
|
||||
return plan;
|
||||
};
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ export type TOrgPlansTableDTO = {
|
||||
export type TOrgPlanDTO = {
|
||||
projectId?: string;
|
||||
refreshCache?: boolean;
|
||||
rootOrgId: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TStartOrgTrialDTO = {
|
||||
|
||||
@@ -89,16 +89,30 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
||||
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
|
||||
const appCfg = getConfig();
|
||||
let expiresIn: string | number = appCfg.JWT_AUTH_LIFETIME;
|
||||
|
||||
if (decodedToken.organizationId) {
|
||||
const org = await server.services.org.findOrganizationById(
|
||||
decodedToken.userId,
|
||||
decodedToken.organizationId,
|
||||
decodedToken.authMethod,
|
||||
decodedToken.organizationId,
|
||||
decodedToken.organizationId
|
||||
);
|
||||
if (org && org.userTokenExpiration) {
|
||||
expiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
|
||||
if (decodedToken.subOrganizationId) {
|
||||
const subOrg = await server.services.org.findOrganizationById({
|
||||
userId: decodedToken.userId,
|
||||
orgId: decodedToken.subOrganizationId,
|
||||
actorAuthMethod: decodedToken.authMethod,
|
||||
actorOrgId: decodedToken.subOrganizationId,
|
||||
rootOrgId: decodedToken.organizationId
|
||||
});
|
||||
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,
|
||||
accessVersion: tokenVersion.accessVersion,
|
||||
organizationId: decodedToken.organizationId,
|
||||
...(decodedToken.subOrganizationId && { subOrganizationId: decodedToken.subOrganizationId }),
|
||||
isMfaVerified: decodedToken.isMfaVerified,
|
||||
mfaMethod: decodedToken.mfaMethod
|
||||
},
|
||||
@@ -117,7 +132,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
||||
{ expiresIn }
|
||||
);
|
||||
|
||||
return { token, organizationId: decodedToken.organizationId };
|
||||
return { token, organizationId: decodedToken.organizationId, subOrganizationId: decodedToken.subOrganizationId };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -60,26 +60,19 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
organization: sanitizedOrganizationSchema.extend({
|
||||
subOrganization: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
organization: sanitizedOrganizationSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const organization = await server.services.org.findOrganizationById(
|
||||
req.permission.id,
|
||||
req.params.organizationId,
|
||||
req.permission.authMethod,
|
||||
req.permission.rootOrgId,
|
||||
req.permission.orgId
|
||||
);
|
||||
const organization = await server.services.org.findOrganizationById({
|
||||
userId: req.permission.id,
|
||||
orgId: req.params.organizationId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
rootOrgId: req.permission.rootOrgId,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
return { organization };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -196,7 +196,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD
|
||||
};
|
||||
|
||||
// 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({
|
||||
id: token.tokenVersionId,
|
||||
userId: token.userId
|
||||
@@ -214,13 +214,17 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD
|
||||
let rootOrgId = "";
|
||||
let parentOrgId = "";
|
||||
if (token.organizationId) {
|
||||
if (subOrganizationSelector) {
|
||||
// Check if token has sub-organization scope
|
||||
if (token.subOrganizationId) {
|
||||
const subOrganization = await orgDAL.findOne({
|
||||
rootOrgId: token.organizationId,
|
||||
slug: subOrganizationSelector
|
||||
id: token.subOrganizationId
|
||||
});
|
||||
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({
|
||||
actorUserId: user.id,
|
||||
|
||||
@@ -258,13 +258,13 @@ export const authSignupServiceFactory = ({
|
||||
let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME;
|
||||
|
||||
if (organizationId) {
|
||||
const org = await orgService.findOrganizationById(
|
||||
user.id,
|
||||
organizationId,
|
||||
authMethod,
|
||||
organizationId,
|
||||
organizationId
|
||||
);
|
||||
const org = await orgService.findOrganizationById({
|
||||
userId: user.id,
|
||||
orgId: organizationId,
|
||||
actorAuthMethod: authMethod,
|
||||
actorOrgId: organizationId,
|
||||
rootOrgId: organizationId
|
||||
});
|
||||
if (org && org.userTokenExpiration) {
|
||||
tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
|
||||
refreshTokenExpiresIn = org.userTokenExpiration;
|
||||
|
||||
@@ -55,6 +55,7 @@ export type AuthModeJwtTokenPayload = {
|
||||
tokenVersionId: string;
|
||||
accessVersion: number;
|
||||
organizationId?: string;
|
||||
subOrganizationId?: string;
|
||||
isMfaVerified?: boolean;
|
||||
mfaMethod?: MfaMethod;
|
||||
};
|
||||
@@ -74,6 +75,7 @@ export type AuthModeRefreshJwtTokenPayload = {
|
||||
tokenVersionId: string;
|
||||
refreshVersion: number;
|
||||
organizationId?: string;
|
||||
subOrganizationId?: string;
|
||||
isMfaVerified?: boolean;
|
||||
mfaMethod?: MfaMethod;
|
||||
};
|
||||
|
||||
@@ -28,5 +28,7 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
||||
shareSecretsProductEnabled: true,
|
||||
maxSharedSecretLifetime: true,
|
||||
maxSharedSecretViewLimit: true,
|
||||
blockDuplicateSecretSyncDestinations: true
|
||||
blockDuplicateSecretSyncDestinations: true,
|
||||
rootOrgId: true,
|
||||
parentOrgId: true
|
||||
});
|
||||
|
||||
@@ -150,36 +150,47 @@ export const orgServiceFactory = ({
|
||||
/*
|
||||
* Get organization details by the organization id
|
||||
* */
|
||||
const findOrganizationById = async (
|
||||
userId: string,
|
||||
orgId: string,
|
||||
actorAuthMethod: ActorAuthMethod,
|
||||
rootOrgId: string,
|
||||
actorOrgId: string
|
||||
) => {
|
||||
const findOrganizationById = async ({
|
||||
userId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
rootOrgId,
|
||||
actorOrgId
|
||||
}: {
|
||||
userId: string;
|
||||
orgId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
rootOrgId: string;
|
||||
actorOrgId: string;
|
||||
}) => {
|
||||
await permissionService.getOrgPermission({
|
||||
actor: ActorType.USER,
|
||||
actorId: userId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId: rootOrgId,
|
||||
actorOrgId,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
const appCfg = getConfig();
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||
const hasSubOrg = rootOrgId !== actorOrgId;
|
||||
|
||||
const org = await orgDAL.findOrgById(rootOrgId);
|
||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${rootOrgId}' not found` });
|
||||
|
||||
const hasSubOrg = actorOrgId !== rootOrgId;
|
||||
let subOrg;
|
||||
if (hasSubOrg) {
|
||||
subOrg = await orgDAL.findOne({ rootOrgId, id: actorOrgId });
|
||||
|
||||
if (!subOrg) throw new NotFoundError({ message: `Sub-organization with ID '${actorOrgId}' not found` });
|
||||
}
|
||||
|
||||
if (!org.userTokenExpiration) {
|
||||
return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME, subOrganization: subOrg };
|
||||
const data = hasSubOrg && subOrg ? subOrg : org;
|
||||
if (!data.userTokenExpiration) {
|
||||
return { ...data, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME };
|
||||
}
|
||||
return { ...org, subOrganization: subOrg };
|
||||
return data;
|
||||
};
|
||||
|
||||
/*
|
||||
* Get all organization a user part of
|
||||
* */
|
||||
|
||||
@@ -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 = "support@infisical.com";
|
||||
|
||||
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,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>
|
||||
|
||||
Reference in New Issue
Block a user