feat: completed base setup

This commit is contained in:
=
2025-10-19 01:38:34 +05:30
parent 9a7849c12b
commit 6fca30f2cb
22 changed files with 249 additions and 21 deletions

View File

@@ -50,10 +50,12 @@ import { registerSshHostGroupRouter } from "./ssh-host-group-router";
import { registerSshHostRouter } from "./ssh-host-router";
import { registerTrustedIpRouter } from "./trusted-ip-router";
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
import { registerSubOrgRouter } from "./sub-org-router";
export const registerV1EERoutes = async (server: FastifyZodProvider) => {
// org role starts with organization
await server.register(registerOrgRoleRouter, { prefix: "/organization" });
await server.register(registerSubOrgRouter, { prefix: "/sub-organizations" });
await server.register(registerLicenseRouter, { prefix: "/organizations" });
// depreciated in favour of infisical workspace

View File

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

View File

@@ -7,6 +7,14 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const sanitiziedSubOrganizationSchema = OrganizationsSchema.pick({
id: true,
name: true,
slug: true,
createdAt: true,
updatedAt: true
});
export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
@@ -28,7 +36,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
organization: OrganizationsSchema
organization: sanitiziedSubOrganizationSchema
})
}
},
@@ -77,7 +85,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
}
],
querystring: z.object({
limit: z.coerce.number().min(1).max(100).default(25).describe(SUB_ORGANIZATIONS.LIST.limit),
limit: z.coerce.number().min(1).max(1000).default(25).describe(SUB_ORGANIZATIONS.LIST.limit),
offset: z.coerce.number().min(0).default(0).describe(SUB_ORGANIZATIONS.LIST.offset),
isAccessible: z
.enum(["true", "false"])
@@ -87,7 +95,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
organizations: OrganizationsSchema.array()
organizations: sanitiziedSubOrganizationSchema.array()
})
}
},

View File

@@ -28,7 +28,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
rbac: false,
githubOrgSync: false,
customRateLimits: false,
childOrganization: true,
subOrganization: true,
customAlerts: false,
secretAccessInsights: false,
auditLogs: false,

View File

@@ -33,7 +33,7 @@ export type TFeatureSet = {
membersUsed: number;
identityLimit: null;
identitiesUsed: number;
childOrganization: true;
subOrganization: true;
environmentLimit: null;
environmentsUsed: 0;
secretVersioning: true;

View File

@@ -285,7 +285,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
MembershipsSchema.extend({
orgAuthEnforced: z.boolean().optional().nullable(),
shouldUseNewPrivilegeSystem: z.boolean().optional().nullable(),
parentOrgId: z.boolean().optional().nullable(),
parentOrgId: z.string().optional().nullable(),
orgGoogleSsoAuthEnforced: z.boolean(),
bypassOrgAuthEnabled: z.boolean()
}).parse(el),

View File

@@ -45,7 +45,7 @@ export const subOrgServiceFactory = ({
);
const orgLicensePlan = await licenseService.getPlan(permissionActor.parentOrgId);
if (!orgLicensePlan.gateway) {
if (!orgLicensePlan.subOrganization) {
throw new BadRequestError({
message: "Child organization creation failed. Please upgrade your instance to Infisical's Enterprise plan."
});

View File

@@ -94,6 +94,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
decodedToken.userId,
decodedToken.organizationId,
decodedToken.authMethod,
decodedToken.organizationId,
decodedToken.organizationId
);
if (org && org.userTokenExpiration) {

View File

@@ -59,7 +59,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
organization: sanitizedOrganizationSchema
organization: sanitizedOrganizationSchema.extend({
subOrganization: z
.object({
id: z.string(),
name: z.string()
})
.optional()
})
})
}
},
@@ -69,6 +76,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
req.permission.id,
req.params.organizationId,
req.permission.authMethod,
req.permission.parentOrgId,
req.permission.orgId
);
return { organization };

View File

@@ -157,23 +157,31 @@ export const orgServiceFactory = ({
userId: string,
orgId: string,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
parentOrgId: string,
actorOrgId: string
) => {
await permissionService.getOrgPermission({
actor: ActorType.USER,
actorId: userId,
orgId,
actorAuthMethod,
actorOrgId,
actorOrgId: parentOrgId,
scope: OrganizationActionScope.Any
});
const appCfg = getConfig();
const org = await orgDAL.findOrgById(orgId);
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
if (!org.userTokenExpiration) {
return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME };
const hasSubOrg = actorOrgId !== parentOrgId;
let subOrg;
if (hasSubOrg) {
subOrg = await orgDAL.findOne({ parentOrgId, id: actorOrgId });
}
return org;
if (!org.userTokenExpiration) {
return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME, subOrganization: subOrg };
}
return { ...org, subOrganization: subOrg };
};
/*
* Get all organization a user part of

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,10 @@ apiRequest.interceptors.request.use((config) => {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${providerAuthToken}`;
}
const subOrganization = params.get("subOrganization");
if (subOrganization) {
config.headers.set("x-infisical-org", subOrganization);
}
}
return config;

View File

@@ -15,5 +15,12 @@ export const useOrganization = () => {
staleTime: Infinity
});
return { currentOrg };
return {
currentOrg: {
...currentOrg,
id: currentOrg?.subOrganization?.id || currentOrg?.id,
parentOrgId: currentOrg.id
},
isSubOrganization: Boolean(currentOrg.subOrganization)
};
};

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";

View File

@@ -64,7 +64,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;
};

View File

@@ -0,0 +1,7 @@
export { useCreateSubOrganization } from "./mutations";
export { subOrganizationsQuery } from "./queries";
export type {
TCreateSubOrganizationDTO,
TListSubOrganizationsDTO,
TSubOrganization
} from "./types";

View File

@@ -0,0 +1,22 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { subOrganizationsQuery } from "./queries";
import { TCreateSubOrganizationDTO, TSubOrganization } 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
);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: subOrganizationsQuery.allKey() });
}
});
};

View File

@@ -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;
}
})
};

View File

@@ -0,0 +1,17 @@
export type TSubOrganization = {
id: string;
name: string;
slug: string;
createdAt: string;
updatedAt: string;
};
export type TCreateSubOrganizationDTO = {
name: string;
};
export type TListSubOrganizationsDTO = {
limit?: number;
offset?: number;
isAccessible?: boolean;
};

View File

@@ -13,6 +13,7 @@ export type SubscriptionPlan = {
customRateLimits: boolean;
pitRecovery: boolean;
githubOrgSync: boolean;
subOrganization?: boolean;
ipAllowlisting: boolean;
rbac: boolean;
secretVersioning: boolean;

View File

@@ -6,12 +6,14 @@ import {
faBook,
faCaretDown,
faCheck,
faCubes,
faEnvelope,
faExclamationTriangle,
faGlobe,
faInfinity,
faInfo,
faInfoCircle,
faPlus,
faServer,
faSignOut,
faToolbox,
@@ -19,7 +21,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 +36,9 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownSubMenu,
DropdownSubMenuContent,
DropdownSubMenuTrigger,
IconButton,
Modal,
ModalContent,
@@ -44,7 +49,7 @@ 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";
@@ -54,6 +59,7 @@ import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel";
import { NotificationDropdown } from "./NotificationDropdown";
import { NewSubOrganizationForm } from "./NewSubOrganizationForm";
const getPlan = (subscription: SubscriptionPlan) => {
if (subscription.groups) return "Enterprise";
@@ -119,9 +125,15 @@ 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 { data: subOrganizations = [] } = useQuery({
...subOrganizationsQuery.list({ limit: 500 }),
enabled: Boolean(subscription.subOrganization) && !isSubOrganization
});
useEffect(() => {
if (subscription?.cardDeclined && !sessionStorage.getItem("paymentFailed")) {
@@ -517,6 +529,7 @@ export const Navbar = () => {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Modal isOpen={showCardDeclinedModal} onOpenChange={setShowCardDeclinedModal}>
<ModalContent
title={
@@ -560,6 +573,16 @@ 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(true)} />
</div>
</ModalContent>
</Modal>
<Modal isOpen={showAdminsModal} onOpenChange={setShowAdminsModal}>
<ModalContent title="Server Administrators" subTitle="View all server administrators">
<div className="mb-2">

View File

@@ -0,0 +1,80 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input } from "@app/components/v2";
import { GenericResourceNameSchema } from "@app/lib/schemas";
import { useCreateSubOrganization } from "@app/hooks/api";
type ContentProps = {
onClose: () => void;
};
const AddOrgSchema = z.object({
name: GenericResourceNameSchema.nonempty("Suborganization name required")
});
type FormData = z.infer<typeof AddOrgSchema>;
export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
const createSubOrg = useCreateSubOrganization();
const {
handleSubmit,
control,
formState: { isSubmitting }
} = useForm({
defaultValues: {
name: "",
invitees: []
},
resolver: zodResolver(AddOrgSchema)
});
const onSubmit = async ({ name }: FormData) => {
try {
await createSubOrg.mutateAsync({
name
});
createNotification({
type: "success",
text: "Successfully created sub organization"
});
onClose();
} 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)} errorText={error?.message} label="Name">
<Input autoFocus value={value} onChange={onChange} placeholder="My Organization" />
</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>
);
};

View File

@@ -1,7 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, retainSearchParams } from "@tanstack/react-router";
import { OrganizationLayout } from "@app/layouts/OrganizationLayout";
import { z } from "zod";
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"])]
}
});