mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 02:26:26 +00:00
feat: completed base setup
This commit is contained in:
@@ -50,10 +50,12 @@ import { registerSshHostGroupRouter } from "./ssh-host-group-router";
|
|||||||
import { registerSshHostRouter } from "./ssh-host-router";
|
import { registerSshHostRouter } from "./ssh-host-router";
|
||||||
import { registerTrustedIpRouter } from "./trusted-ip-router";
|
import { registerTrustedIpRouter } from "./trusted-ip-router";
|
||||||
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
|
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
|
||||||
|
import { registerSubOrgRouter } from "./sub-org-router";
|
||||||
|
|
||||||
export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||||
// org role starts with organization
|
// org role starts with organization
|
||||||
await server.register(registerOrgRoleRouter, { prefix: "/organization" });
|
await server.register(registerOrgRoleRouter, { prefix: "/organization" });
|
||||||
|
await server.register(registerSubOrgRouter, { prefix: "/sub-organizations" });
|
||||||
await server.register(registerLicenseRouter, { prefix: "/organizations" });
|
await server.register(registerLicenseRouter, { prefix: "/organizations" });
|
||||||
|
|
||||||
// depreciated in favour of infisical workspace
|
// depreciated in favour of infisical workspace
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ 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.orgId,
|
actorOrgId: req.permission.parentOrgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
orgId: req.params.organizationId,
|
orgId: req.params.organizationId,
|
||||||
refreshCache: req.query.refreshCache
|
refreshCache: req.query.refreshCache
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
|||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
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) => {
|
export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -28,7 +36,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
organization: OrganizationsSchema
|
organization: sanitiziedSubOrganizationSchema
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -77,7 +85,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
querystring: z.object({
|
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),
|
offset: z.coerce.number().min(0).default(0).describe(SUB_ORGANIZATIONS.LIST.offset),
|
||||||
isAccessible: z
|
isAccessible: z
|
||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
@@ -87,7 +95,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
organizations: OrganizationsSchema.array()
|
organizations: sanitiziedSubOrganizationSchema.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
rbac: false,
|
rbac: false,
|
||||||
githubOrgSync: false,
|
githubOrgSync: false,
|
||||||
customRateLimits: false,
|
customRateLimits: false,
|
||||||
childOrganization: true,
|
subOrganization: true,
|
||||||
customAlerts: false,
|
customAlerts: false,
|
||||||
secretAccessInsights: false,
|
secretAccessInsights: false,
|
||||||
auditLogs: false,
|
auditLogs: false,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export type TFeatureSet = {
|
|||||||
membersUsed: number;
|
membersUsed: number;
|
||||||
identityLimit: null;
|
identityLimit: null;
|
||||||
identitiesUsed: number;
|
identitiesUsed: number;
|
||||||
childOrganization: true;
|
subOrganization: true;
|
||||||
environmentLimit: null;
|
environmentLimit: null;
|
||||||
environmentsUsed: 0;
|
environmentsUsed: 0;
|
||||||
secretVersioning: true;
|
secretVersioning: true;
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
|||||||
MembershipsSchema.extend({
|
MembershipsSchema.extend({
|
||||||
orgAuthEnforced: z.boolean().optional().nullable(),
|
orgAuthEnforced: z.boolean().optional().nullable(),
|
||||||
shouldUseNewPrivilegeSystem: z.boolean().optional().nullable(),
|
shouldUseNewPrivilegeSystem: z.boolean().optional().nullable(),
|
||||||
parentOrgId: z.boolean().optional().nullable(),
|
parentOrgId: z.string().optional().nullable(),
|
||||||
orgGoogleSsoAuthEnforced: z.boolean(),
|
orgGoogleSsoAuthEnforced: z.boolean(),
|
||||||
bypassOrgAuthEnabled: z.boolean()
|
bypassOrgAuthEnabled: z.boolean()
|
||||||
}).parse(el),
|
}).parse(el),
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const subOrgServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const orgLicensePlan = await licenseService.getPlan(permissionActor.parentOrgId);
|
const orgLicensePlan = await licenseService.getPlan(permissionActor.parentOrgId);
|
||||||
if (!orgLicensePlan.gateway) {
|
if (!orgLicensePlan.subOrganization) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Child organization creation failed. Please upgrade your instance to Infisical's Enterprise plan."
|
message: "Child organization creation failed. Please upgrade your instance to Infisical's Enterprise plan."
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
|||||||
decodedToken.userId,
|
decodedToken.userId,
|
||||||
decodedToken.organizationId,
|
decodedToken.organizationId,
|
||||||
decodedToken.authMethod,
|
decodedToken.authMethod,
|
||||||
|
decodedToken.organizationId,
|
||||||
decodedToken.organizationId
|
decodedToken.organizationId
|
||||||
);
|
);
|
||||||
if (org && org.userTokenExpiration) {
|
if (org && org.userTokenExpiration) {
|
||||||
|
|||||||
@@ -59,7 +59,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
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.permission.id,
|
||||||
req.params.organizationId,
|
req.params.organizationId,
|
||||||
req.permission.authMethod,
|
req.permission.authMethod,
|
||||||
|
req.permission.parentOrgId,
|
||||||
req.permission.orgId
|
req.permission.orgId
|
||||||
);
|
);
|
||||||
return { organization };
|
return { organization };
|
||||||
|
|||||||
@@ -157,23 +157,31 @@ export const orgServiceFactory = ({
|
|||||||
userId: string,
|
userId: string,
|
||||||
orgId: string,
|
orgId: string,
|
||||||
actorAuthMethod: ActorAuthMethod,
|
actorAuthMethod: ActorAuthMethod,
|
||||||
actorOrgId: string | undefined
|
parentOrgId: string,
|
||||||
|
actorOrgId: string
|
||||||
) => {
|
) => {
|
||||||
await permissionService.getOrgPermission({
|
await permissionService.getOrgPermission({
|
||||||
actor: ActorType.USER,
|
actor: ActorType.USER,
|
||||||
actorId: userId,
|
actorId: userId,
|
||||||
orgId,
|
orgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId: parentOrgId,
|
||||||
scope: OrganizationActionScope.Any
|
scope: OrganizationActionScope.Any
|
||||||
});
|
});
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const org = await orgDAL.findOrgById(orgId);
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
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
|
* Get all organization a user part of
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ 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
|
||||||
@@ -38,6 +40,10 @@ 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 subOrganization = params.get("subOrganization");
|
||||||
|
if (subOrganization) {
|
||||||
|
config.headers.set("x-infisical-org", subOrganization);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
|
|||||||
@@ -15,5 +15,12 @@ export const useOrganization = () => {
|
|||||||
staleTime: Infinity
|
staleTime: Infinity
|
||||||
});
|
});
|
||||||
|
|
||||||
return { currentOrg };
|
return {
|
||||||
|
currentOrg: {
|
||||||
|
...currentOrg,
|
||||||
|
id: currentOrg?.subOrganization?.id || currentOrg?.id,
|
||||||
|
parentOrgId: currentOrg.id
|
||||||
|
},
|
||||||
|
isSubOrganization: Boolean(currentOrg.subOrganization)
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export * from "./sshCertificateTemplates";
|
|||||||
export * from "./sshHost";
|
export * from "./sshHost";
|
||||||
export * from "./sshHostGroup";
|
export * from "./sshHostGroup";
|
||||||
export * from "./ssoConfig";
|
export * from "./ssoConfig";
|
||||||
|
export * from "./subOrganizations";
|
||||||
export * from "./subscriptions";
|
export * from "./subscriptions";
|
||||||
export * from "./tags";
|
export * from "./tags";
|
||||||
export * from "./trustedIps";
|
export * from "./trustedIps";
|
||||||
|
|||||||
@@ -64,7 +64,9 @@ export const useGetOrganizations = () => {
|
|||||||
export const fetchOrganizationById = async (id: string) => {
|
export const fetchOrganizationById = async (id: string) => {
|
||||||
const {
|
const {
|
||||||
data: { organization }
|
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;
|
return organization;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export { useCreateSubOrganization } from "./mutations";
|
||||||
|
export { subOrganizationsQuery } from "./queries";
|
||||||
|
export type {
|
||||||
|
TCreateSubOrganizationDTO,
|
||||||
|
TListSubOrganizationsDTO,
|
||||||
|
TSubOrganization
|
||||||
|
} from "./types";
|
||||||
@@ -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() });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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,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;
|
||||||
|
};
|
||||||
@@ -13,6 +13,7 @@ export type SubscriptionPlan = {
|
|||||||
customRateLimits: boolean;
|
customRateLimits: boolean;
|
||||||
pitRecovery: boolean;
|
pitRecovery: boolean;
|
||||||
githubOrgSync: boolean;
|
githubOrgSync: boolean;
|
||||||
|
subOrganization?: boolean;
|
||||||
ipAllowlisting: boolean;
|
ipAllowlisting: boolean;
|
||||||
rbac: boolean;
|
rbac: boolean;
|
||||||
secretVersioning: boolean;
|
secretVersioning: boolean;
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import {
|
|||||||
faBook,
|
faBook,
|
||||||
faCaretDown,
|
faCaretDown,
|
||||||
faCheck,
|
faCheck,
|
||||||
|
faCubes,
|
||||||
faEnvelope,
|
faEnvelope,
|
||||||
faExclamationTriangle,
|
faExclamationTriangle,
|
||||||
faGlobe,
|
faGlobe,
|
||||||
faInfinity,
|
faInfinity,
|
||||||
faInfo,
|
faInfo,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
|
faPlus,
|
||||||
faServer,
|
faServer,
|
||||||
faSignOut,
|
faSignOut,
|
||||||
faToolbox,
|
faToolbox,
|
||||||
@@ -19,7 +21,7 @@ import {
|
|||||||
faUsers
|
faUsers
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
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 { Link, useLocation, useNavigate, useRouter, useRouterState } from "@tanstack/react-router";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
@@ -34,6 +36,9 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
|
DropdownSubMenu,
|
||||||
|
DropdownSubMenuContent,
|
||||||
|
DropdownSubMenuTrigger,
|
||||||
IconButton,
|
IconButton,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
@@ -44,7 +49,7 @@ import { envConfig } from "@app/config/env";
|
|||||||
import { useOrganization, useSubscription, useUser } from "@app/context";
|
import { useOrganization, useSubscription, useUser } from "@app/context";
|
||||||
import { isInfisicalCloud } from "@app/helpers/platform";
|
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||||
import { useToggle } from "@app/hooks";
|
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 { authKeys, selectOrganization } 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";
|
||||||
@@ -54,6 +59,7 @@ import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
|
|||||||
|
|
||||||
import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel";
|
import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel";
|
||||||
import { NotificationDropdown } from "./NotificationDropdown";
|
import { NotificationDropdown } from "./NotificationDropdown";
|
||||||
|
import { NewSubOrganizationForm } from "./NewSubOrganizationForm";
|
||||||
|
|
||||||
const getPlan = (subscription: SubscriptionPlan) => {
|
const getPlan = (subscription: SubscriptionPlan) => {
|
||||||
if (subscription.groups) return "Enterprise";
|
if (subscription.groups) return "Enterprise";
|
||||||
@@ -119,9 +125,15 @@ export const INFISICAL_SUPPORT_OPTIONS = [
|
|||||||
export const Navbar = () => {
|
export const Navbar = () => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg, isSubOrganization } = useOrganization();
|
||||||
|
|
||||||
const [showAdminsModal, setShowAdminsModal] = useState(false);
|
const [showAdminsModal, setShowAdminsModal] = useState(false);
|
||||||
|
const [showSubOrgForm, setShowSubOrgForm] = useState(false);
|
||||||
const [showCardDeclinedModal, setShowCardDeclinedModal] = useState(false);
|
const [showCardDeclinedModal, setShowCardDeclinedModal] = useState(false);
|
||||||
|
const { data: subOrganizations = [] } = useQuery({
|
||||||
|
...subOrganizationsQuery.list({ limit: 500 }),
|
||||||
|
enabled: Boolean(subscription.subOrganization) && !isSubOrganization
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (subscription?.cardDeclined && !sessionStorage.getItem("paymentFailed")) {
|
if (subscription?.cardDeclined && !sessionStorage.getItem("paymentFailed")) {
|
||||||
@@ -517,6 +529,7 @@ export const Navbar = () => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<Modal isOpen={showCardDeclinedModal} onOpenChange={setShowCardDeclinedModal}>
|
<Modal isOpen={showCardDeclinedModal} onOpenChange={setShowCardDeclinedModal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={
|
title={
|
||||||
@@ -560,6 +573,16 @@ export const Navbar = () => {
|
|||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</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}>
|
<Modal isOpen={showAdminsModal} onOpenChange={setShowAdminsModal}>
|
||||||
<ModalContent title="Server Administrators" subTitle="View all server administrators">
|
<ModalContent title="Server Administrators" subTitle="View all server administrators">
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute, retainSearchParams } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { OrganizationLayout } from "@app/layouts/OrganizationLayout";
|
import { OrganizationLayout } from "@app/layouts/OrganizationLayout";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authenticate/_inject-org-details/_org-layout")({
|
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"])]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user