diff --git a/server/lib/ip.ts b/server/lib/ip.ts index 16a926f1..805301bc 100644 --- a/server/lib/ip.ts +++ b/server/lib/ip.ts @@ -274,4 +274,13 @@ export async function getNextAvailableOrgSubnet(): Promise { } return subnet; +} + +export function isValidCidr(cidr: string): boolean { + try { + cidrToRange(cidr); + return true; + } catch (e) { + return false; + } } \ No newline at end of file diff --git a/server/routers/client/createClient.ts b/server/routers/client/createClient.ts index fc092c10..6448fb61 100644 --- a/server/routers/client/createClient.ts +++ b/server/routers/client/createClient.ts @@ -19,8 +19,7 @@ import { eq, and } from "drizzle-orm"; import { fromError } from "zod-validation-error"; import moment from "moment"; import { hashPassword } from "@server/auth/password"; -import { getNextAvailableClientSubnet } from "@server/lib/ip"; -import config from "@server/lib/config"; +import { isValidCIDR } from "@server/lib/validators"; const createClientParamsSchema = z .object({ @@ -34,6 +33,7 @@ const createClientSchema = z siteIds: z.array(z.number().int().positive()), olmId: z.string(), secret: z.string(), + subnet: z.string(), type: z.enum(["olm"]) }) .strict(); @@ -58,7 +58,7 @@ export async function createClient( ); } - const { name, type, siteIds, olmId, secret } = parsedBody.data; + const { name, type, siteIds, olmId, secret, subnet } = parsedBody.data; const parsedParams = createClientParamsSchema.safeParse(req.params); if (!parsedParams.success) { @@ -78,9 +78,14 @@ export async function createClient( ); } - const newSubnet = await getNextAvailableClientSubnet(orgId); - - const subnet = `${newSubnet.split("/")[0]}/${config.getRawConfig().orgs.block_size}`; // we want the block size of the whole org + if (subnet && !isValidCIDR(subnet)) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid subnet format. Please provide a valid CIDR notation." + ) + ); + } await db.transaction(async (trx) => { // TODO: more intelligent way to pick the exit node diff --git a/server/routers/client/pickClientDefaults.ts b/server/routers/client/pickClientDefaults.ts index 231bc409..32bcb45c 100644 --- a/server/routers/client/pickClientDefaults.ts +++ b/server/routers/client/pickClientDefaults.ts @@ -4,25 +4,53 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { generateId } from "@server/auth/sessions/app"; +import { getNextAvailableClientSubnet } from "@server/lib/ip"; +import config from "@server/lib/config"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; export type PickClientDefaultsResponse = { olmId: string; olmSecret: string; + subnet: string; }; +const pickClientDefaultsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + export async function pickClientDefaults( req: Request, res: Response, next: NextFunction ): Promise { try { + const parsedParams = pickClientDefaultsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + const olmId = generateId(15); const secret = generateId(48); + const newSubnet = await getNextAvailableClientSubnet(orgId); + + const subnet = `${newSubnet.split("/")[0]}/${config.getRawConfig().orgs.block_size}`; // we want the block size of the whole org + return response(res, { data: { olmId: olmId, - olmSecret: secret + olmSecret: secret, + subnet: subnet }, success: true, error: false, diff --git a/server/routers/external.ts b/server/routers/external.ts index 5601b63a..ba4c1716 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -47,8 +47,13 @@ unauthenticated.get("/", (_, res) => { export const authenticated = Router(); authenticated.use(verifySessionUserMiddleware); +authenticated.get( + "/pick-org-defaults", + org.pickOrgDefaults +); authenticated.get("/org/checkId", org.checkId); authenticated.put("/org", getUserOrgs, org.createOrg); + authenticated.get("/orgs", getUserOrgs, org.listOrgs); // TODO we need to check the orgs here authenticated.get( "/org/:orgId", diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index fef5e2ac..457c7fe4 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -19,12 +19,13 @@ import { createAdminRole } from "@server/setup/ensureActions"; import config from "@server/lib/config"; import { fromError } from "zod-validation-error"; import { defaultRoleAllowedActions } from "../role"; -import { getNextAvailableOrgSubnet } from "@server/lib/ip"; +import { isValidCIDR } from "@server/lib/validators"; const createOrgSchema = z .object({ orgId: z.string(), - name: z.string().min(1).max(255) + name: z.string().min(1).max(255), + subnet: z.string() }) .strict(); @@ -68,7 +69,16 @@ export async function createOrg( ); } - const { orgId, name } = parsedBody.data; + const { orgId, name, subnet } = parsedBody.data; + + if (subnet && !isValidCIDR(subnet)) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid subnet format. Please provide a valid CIDR notation." + ) + ); + } // make sure the orgId is unique const orgExists = await db @@ -89,8 +99,6 @@ export async function createOrg( let error = ""; let org: Org | null = null; - const subnet = await getNextAvailableOrgSubnet(); - await db.transaction(async (trx) => { const allDomains = await trx .select() diff --git a/server/routers/org/index.ts b/server/routers/org/index.ts index 04ff1362..24b15e40 100644 --- a/server/routers/org/index.ts +++ b/server/routers/org/index.ts @@ -5,3 +5,4 @@ export * from "./updateOrg"; export * from "./listOrgs"; export * from "./checkId"; export * from "./getOrgOverview"; +export* from "./pickOrgDefaults"; \ No newline at end of file diff --git a/server/routers/org/pickOrgDefaults.ts b/server/routers/org/pickOrgDefaults.ts new file mode 100644 index 00000000..9a35279b --- /dev/null +++ b/server/routers/org/pickOrgDefaults.ts @@ -0,0 +1,35 @@ +import { Request, Response, NextFunction } from "express"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { getNextAvailableOrgSubnet } from "@server/lib/ip"; + +export type PickOrgDefaultsResponse = { + subnet: string; +}; + +export async function pickOrgDefaults( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const subnet = await getNextAvailableOrgSubnet(); + + return response(res, { + data: { + subnet: subnet + }, + success: true, + error: false, + message: "Organization defaults created successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/src/app/[orgId]/settings/clients/CreateClientsForm.tsx b/src/app/[orgId]/settings/clients/CreateClientsForm.tsx index e3183c49..76ac6d93 100644 --- a/src/app/[orgId]/settings/clients/CreateClientsForm.tsx +++ b/src/app/[orgId]/settings/clients/CreateClientsForm.tsx @@ -59,6 +59,9 @@ const createClientFormSchema = z.object({ }), siteIds: z.array(z.number()).min(1, { message: "Select at least one site." + }), + subnet: z.string().min(1, { + message: "Subnet is required." }) }); @@ -66,7 +69,8 @@ type CreateClientFormValues = z.infer; const defaultValues: Partial = { name: "", - siteIds: [] + siteIds: [], + subnet: "" }; type CreateClientFormProps = { @@ -151,6 +155,11 @@ export default function CreateClientForm({ setClientDefaults(data); const olmConfig = `olm --id ${data?.olmId} --secret ${data?.olmSecret} --endpoint ${env.app.dashboardUrl}`; setOlmCommand(olmConfig); + + // Set the subnet value from client defaults + if (data?.subnet) { + form.setValue("subnet", data.subnet); + } } }); }; @@ -191,6 +200,7 @@ export default function CreateClientForm({ siteIds: data.siteIds, olmId: clientDefaults.olmId, secret: clientDefaults.olmSecret, + subnet: data.subnet, type: "olm" } as CreateClientBody; @@ -249,6 +259,27 @@ export default function CreateClientForm({ )} /> + ( + + Subnet + + + + + The subnet that this client will use for connectivity. + + + + )} + /> + ); -} +} \ No newline at end of file diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index 959a32a6..4bafa38c 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -1,5 +1,4 @@ "use client"; - import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { Button } from "@app/components/ui/button"; import { useOrgContext } from "@app/hooks/useOrgContext"; @@ -22,17 +21,9 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { formatAxiosError } from "@app/lib/api"; -import { AlertTriangle, Trash2 } from "lucide-react"; -import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle -} from "@/components/ui/card"; import { AxiosResponse } from "axios"; import { DeleteOrgResponse, ListOrgsResponse } from "@server/routers/org"; -import { redirect, useRouter } from "next/navigation"; +import { useRouter } from "next/navigation"; import { SettingsContainer, SettingsSection, @@ -44,27 +35,28 @@ import { SettingsSectionFooter } from "@app/components/Settings"; +// Updated schema to include subnet field const GeneralFormSchema = z.object({ - name: z.string() + name: z.string(), + subnet: z.string().optional() }); type GeneralFormValues = z.infer; export default function GeneralPage() { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const { orgUser } = userOrgUserContext(); const router = useRouter(); const { org } = useOrgContext(); const api = createApiClient(useEnvContext()); - const [loadingDelete, setLoadingDelete] = useState(false); const [loadingSave, setLoadingSave] = useState(false); - + const form = useForm({ resolver: zodResolver(GeneralFormSchema), defaultValues: { - name: org?.org.name + name: org?.org.name, + subnet: org?.org.subnet || "" // Add default value for subnet }, mode: "onChange" }); @@ -75,12 +67,10 @@ export default function GeneralPage() { const res = await api.delete>( `/org/${org?.org.orgId}` ); - toast({ title: "Organization deleted", description: "The organization and its data has been deleted." }); - if (res.status === 200) { pickNewOrgAndNavigate(); } @@ -102,7 +92,6 @@ export default function GeneralPage() { async function pickNewOrgAndNavigate() { try { const res = await api.get>(`/orgs`); - if (res.status === 200) { if (res.data.data.orgs.length > 0) { const orgId = res.data.data.orgs[0].orgId; @@ -130,14 +119,14 @@ export default function GeneralPage() { setLoadingSave(true); await api .post(`/org/${org?.org.orgId}`, { - name: data.name + name: data.name, + subnet: data.subnet // Include subnet in the API request }) .then(() => { toast({ title: "Organization updated", description: "The organization has been updated." }); - router.refresh(); }) .catch((e) => { @@ -182,7 +171,6 @@ export default function GeneralPage() { string={org?.org.name || ""} title="Delete Organization" /> - @@ -192,7 +180,6 @@ export default function GeneralPage() { Manage your organization details and configuration -
@@ -218,11 +205,31 @@ export default function GeneralPage() { )} /> + + {/* New FormField for subnet input */} + ( + + Subnet + + + + + + The subnet for this organization's network configuration. + + + )} + />
-