diff --git a/server/middlewares/verifyClientsEnabled.ts b/server/middlewares/verifyClientsEnabled.ts index 2b573dba..6e8070da 100644 --- a/server/middlewares/verifyClientsEnabled.ts +++ b/server/middlewares/verifyClientsEnabled.ts @@ -9,7 +9,7 @@ export async function verifyClientsEnabled( next: NextFunction ) { try { - if (!config.getRawConfig().flags?.enable_redis) { + if (!config.getRawConfig().flags?.enable_clients) { return next( createHttpError( HttpCode.NOT_IMPLEMENTED, diff --git a/server/routers/org/pickOrgDefaults.ts b/server/routers/org/pickOrgDefaults.ts index 9a35279b..771b0d99 100644 --- a/server/routers/org/pickOrgDefaults.ts +++ b/server/routers/org/pickOrgDefaults.ts @@ -4,6 +4,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { getNextAvailableOrgSubnet } from "@server/lib/ip"; +import config from "@server/lib/config"; export type PickOrgDefaultsResponse = { subnet: string; @@ -15,7 +16,10 @@ export async function pickOrgDefaults( next: NextFunction ): Promise { try { - const subnet = await getNextAvailableOrgSubnet(); + // TODO: Why would each org have to have its own subnet? + // const subnet = await getNextAvailableOrgSubnet(); + // Just hard code the subnet for now for everyone + const subnet = config.getRawConfig().orgs.subnet_group; return response(res, { data: { diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index 7e86b524..9f1a6eb5 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -38,7 +38,7 @@ const createSiteSchema = z subnet: z.string().optional(), newtId: z.string().optional(), secret: z.string().optional(), - // address: z.string().optional(), + address: z.string().optional(), type: z.enum(["newt", "wireguard", "local"]) }) .strict() @@ -97,7 +97,7 @@ export async function createSite( subnet, newtId, secret, - // address + address } = parsedBody.data; const parsedParams = createSiteParamsSchema.safeParse(req.params); @@ -129,58 +129,58 @@ export async function createSite( ); } - // let updatedAddress = null; - // if (address) { - // if (!isValidIP(address)) { - // return next( - // createHttpError( - // HttpCode.BAD_REQUEST, - // "Invalid subnet format. Please provide a valid CIDR notation." - // ) - // ); - // } - // - // if (!isIpInCidr(address, org.subnet)) { - // return next( - // createHttpError( - // HttpCode.BAD_REQUEST, - // "IP is not in the CIDR range of the subnet." - // ) - // ); - // } - // - // updatedAddress = `${address}/${org.subnet.split("/")[1]}`; // we want the block size of the whole org - // - // // make sure the subnet is unique - // const addressExistsSites = await db - // .select() - // .from(sites) - // .where(eq(sites.address, updatedAddress)) - // .limit(1); - // - // if (addressExistsSites.length > 0) { - // return next( - // createHttpError( - // HttpCode.CONFLICT, - // `Subnet ${subnet} already exists` - // ) - // ); - // } - // - // const addressExistsClients = await db - // .select() - // .from(sites) - // .where(eq(sites.subnet, updatedAddress)) - // .limit(1); - // if (addressExistsClients.length > 0) { - // return next( - // createHttpError( - // HttpCode.CONFLICT, - // `Subnet ${subnet} already exists` - // ) - // ); - // } - // } + let updatedAddress = null; + if (address) { + if (!isValidIP(address)) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid subnet format. Please provide a valid CIDR notation." + ) + ); + } + + if (!isIpInCidr(address, org.subnet)) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "IP is not in the CIDR range of the subnet." + ) + ); + } + + updatedAddress = `${address}/${org.subnet.split("/")[1]}`; // we want the block size of the whole org + + // make sure the subnet is unique + const addressExistsSites = await db + .select() + .from(sites) + .where(eq(sites.address, updatedAddress)) + .limit(1); + + if (addressExistsSites.length > 0) { + return next( + createHttpError( + HttpCode.CONFLICT, + `Subnet ${subnet} already exists` + ) + ); + } + + const addressExistsClients = await db + .select() + .from(sites) + .where(eq(sites.subnet, updatedAddress)) + .limit(1); + if (addressExistsClients.length > 0) { + return next( + createHttpError( + HttpCode.CONFLICT, + `Subnet ${subnet} already exists` + ) + ); + } + } const niceId = await getUniqueSiteName(orgId); diff --git a/server/routers/site/pickSiteDefaults.ts b/server/routers/site/pickSiteDefaults.ts index 430fb95a..2ae25c11 100644 --- a/server/routers/site/pickSiteDefaults.ts +++ b/server/routers/site/pickSiteDefaults.ts @@ -108,17 +108,17 @@ export async function pickSiteDefaults( ); } - // const newClientAddress = await getNextAvailableClientSubnet(orgId); - // if (!newClientAddress) { - // return next( - // createHttpError( - // HttpCode.INTERNAL_SERVER_ERROR, - // "No available subnet found" - // ) - // ); - // } + const newClientAddress = await getNextAvailableClientSubnet(orgId); + if (!newClientAddress) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "No available subnet found" + ) + ); + } - // const clientAddress = newClientAddress.split("/")[0]; + const clientAddress = newClientAddress.split("/")[0]; const newtId = generateId(15); const secret = generateId(48); @@ -133,7 +133,7 @@ export async function pickSiteDefaults( endpoint: exitNode.endpoint, // subnet: `${newSubnet.split("/")[0]}/${config.getRawConfig().gerbil.block_size}`, // we want the block size of the whole subnet subnet: newSubnet, - // clientAddress: clientAddress, + clientAddress: clientAddress, newtId, newtSecret: secret }, diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index eb08ed80..4aa15713 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -35,7 +35,7 @@ import { SettingsSectionFooter } from "@app/components/Settings"; import { useUserContext } from "@app/hooks/useUserContext"; -import { useTranslations } from 'next-intl'; +import { useTranslations } from "next-intl"; // Updated schema to include subnet field const GeneralFormSchema = z.object({ @@ -53,6 +53,7 @@ export default function GeneralPage() { const api = createApiClient(useEnvContext()); const { user } = useUserContext(); const t = useTranslations(); + const { env } = useEnvContext(); const [loadingDelete, setLoadingDelete] = useState(false); const [loadingSave, setLoadingSave] = useState(false); @@ -73,8 +74,8 @@ export default function GeneralPage() { `/org/${org?.org.orgId}` ); toast({ - title: t('orgDeleted'), - description: t('orgDeletedMessage') + title: t("orgDeleted"), + description: t("orgDeletedMessage") }); if (res.status === 200) { pickNewOrgAndNavigate(); @@ -83,8 +84,8 @@ export default function GeneralPage() { console.error(err); toast({ variant: "destructive", - title: t('orgErrorDelete'), - description: formatAxiosError(err, t('orgErrorDeleteMessage')) + title: t("orgErrorDelete"), + description: formatAxiosError(err, t("orgErrorDeleteMessage")) }); } finally { setLoadingDelete(false); @@ -111,8 +112,8 @@ export default function GeneralPage() { console.error(err); toast({ variant: "destructive", - title: t('orgErrorFetch'), - description: formatAxiosError(err, t('orgErrorFetchMessage')) + title: t("orgErrorFetch"), + description: formatAxiosError(err, t("orgErrorFetchMessage")) }); } } @@ -126,16 +127,16 @@ export default function GeneralPage() { }) .then(() => { toast({ - title: t('orgUpdated'), - description: t('orgUpdatedDescription') + title: t("orgUpdated"), + description: t("orgUpdatedDescription") }); router.refresh(); }) .catch((e) => { toast({ variant: "destructive", - title: t('orgErrorUpdate'), - description: formatAxiosError(e, t('orgErrorUpdateMessage')) + title: t("orgErrorUpdate"), + description: formatAxiosError(e, t("orgErrorUpdateMessage")) }); }) .finally(() => { @@ -153,28 +154,26 @@ export default function GeneralPage() { dialog={

- {t('orgQuestionRemove', {selectedOrg: org?.org.name})} -

-

- {t('orgMessageRemove')} -

-

- {t('orgMessageConfirm')} + {t("orgQuestionRemove", { + selectedOrg: org?.org.name + })}

+

{t("orgMessageRemove")}

+

{t("orgMessageConfirm")}

} - buttonText={t('orgDeleteConfirm')} + buttonText={t("orgDeleteConfirm")} onConfirm={deleteOrg} string={org?.org.name || ""} - title={t('orgDelete')} + title={t("orgDelete")} /> - {t('orgGeneralSettings')} + {t("orgGeneralSettings")} - {t('orgGeneralSettingsDescription')} + {t("orgGeneralSettingsDescription")} @@ -190,37 +189,40 @@ export default function GeneralPage() { name="name" render={({ field }) => ( - {t('name')} + {t("name")} - {t('orgDisplayName')} + {t("orgDisplayName")} )} /> - - {/* ( */} - {/* */} - {/* Subnet */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* The subnet for this organization's network configuration. */} - {/* */} - {/* */} - {/* )} */} - {/* /> */} + {env.flags.enableClients && ( + ( + + Subnet + + + + + + The subnet for this + organization's network + configuration. + + + )} + /> + )} @@ -232,15 +234,17 @@ export default function GeneralPage() { loading={loadingSave} disabled={loadingSave} > - {t('saveGeneralSettings')} + {t("saveGeneralSettings")} - {t('orgDangerZone')} + + {t("orgDangerZone")} + - {t('orgDangerZoneDescription')} + {t("orgDangerZoneDescription")} @@ -251,7 +255,7 @@ export default function GeneralPage() { loading={loadingDelete} disabled={loadingDelete} > - {t('orgDelete')} + {t("orgDelete")} diff --git a/src/app/[orgId]/settings/layout.tsx b/src/app/[orgId]/settings/layout.tsx index b28beb4f..7db530dd 100644 --- a/src/app/[orgId]/settings/layout.tsx +++ b/src/app/[orgId]/settings/layout.tsx @@ -86,7 +86,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { return ( - + {children} diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index 16b65916..3546e871 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -98,9 +98,9 @@ export default function Page() { .object({ name: z .string() - .min(2, { message: t('nameMin', {len: 2}) }) + .min(2, { message: t("nameMin", { len: 2 }) }) .max(30, { - message: t('nameMax', {len: 30}) + message: t("nameMax", { len: 30 }) }), method: z.enum(["newt", "wireguard", "local"]), copied: z.boolean(), @@ -115,7 +115,7 @@ export default function Page() { return true; }, { - message: t('sitesConfirmCopy'), + message: t("sitesConfirmCopy"), path: ["copied"] } ); @@ -127,21 +127,29 @@ export default function Page() { >([ { id: "newt", - title: t('siteNewtTunnel'), - description: t('siteNewtTunnelDescription'), + title: t("siteNewtTunnel"), + description: t("siteNewtTunnelDescription"), disabled: true }, - ...(env.flags.disableBasicWireguardSites ? [] : [{ - id: "wireguard" as SiteType, - title: t('siteWg'), - description: t('siteWgDescription'), - disabled: true - }]), - ...(env.flags.disableLocalSites ? [] : [{ - id: "local" as SiteType, - title: t('local'), - description: t('siteLocalDescription') - }]) + ...(env.flags.disableBasicWireguardSites + ? [] + : [ + { + id: "wireguard" as SiteType, + title: t("siteWg"), + description: t("siteWgDescription"), + disabled: true + } + ]), + ...(env.flags.disableLocalSites + ? [] + : [ + { + id: "local" as SiteType, + title: t("local"), + description: t("siteLocalDescription") + } + ]) ]); const [loadingPage, setLoadingPage] = useState(true); @@ -319,7 +327,7 @@ WantedBy=default.target` }; const getCommand = () => { - const placeholder = [t('unknownCommand')]; + const placeholder = [t("unknownCommand")]; if (!commands) { return placeholder; } @@ -384,8 +392,8 @@ WantedBy=default.target` if (!siteDefaults || !wgConfig) { toast({ variant: "destructive", - title: t('siteErrorCreate'), - description: t('siteErrorCreateKeyPair') + title: t("siteErrorCreate"), + description: t("siteErrorCreateKeyPair") }); setCreateLoading(false); return; @@ -402,8 +410,8 @@ WantedBy=default.target` if (!siteDefaults) { toast({ variant: "destructive", - title: t('siteErrorCreate'), - description: t('siteErrorCreateDefaults') + title: t("siteErrorCreate"), + description: t("siteErrorCreateDefaults") }); setCreateLoading(false); return; @@ -415,7 +423,7 @@ WantedBy=default.target` exitNodeId: siteDefaults.exitNodeId, secret: siteDefaults.newtSecret, newtId: siteDefaults.newtId, - // address: clientAddress + address: clientAddress }; } @@ -426,7 +434,7 @@ WantedBy=default.target` .catch((e) => { toast({ variant: "destructive", - title: t('siteErrorCreate'), + title: t("siteErrorCreate"), description: formatAxiosError(e) }); }); @@ -452,14 +460,23 @@ WantedBy=default.target` ); if (!response.ok) { throw new Error( - t('newtErrorFetchReleases', {err: response.statusText}) + t("newtErrorFetchReleases", { + err: response.statusText + }) ); } const data = await response.json(); const latestVersion = data.tag_name; newtVersion = latestVersion; } catch (error) { - console.error(t('newtErrorFetchLatest', {err: error instanceof Error ? error.message : String(error)})); + console.error( + t("newtErrorFetchLatest", { + err: + error instanceof Error + ? error.message + : String(error) + }) + ); } const generatedKeypair = generateKeypair(); @@ -526,8 +543,8 @@ WantedBy=default.target` <>
@@ -545,7 +562,7 @@ WantedBy=default.target` - {t('siteInfo')} + {t("siteInfo")} @@ -561,7 +578,7 @@ WantedBy=default.target` render={({ field }) => ( - {t('name')} + {t("name")} - {t('siteNameDescription')} + {t( + "siteNameDescription" + )} )} /> - {/* ( */} - {/* */} - {/* */} - {/* Site Address */} - {/* */} - {/* */} - {/* { */} - {/* setClientAddress( */} - {/* e.target */} - {/* .value */} - {/* ); */} - {/* field.onChange( */} - {/* e.target */} - {/* .value */} - {/* ); */} - {/* }} */} - {/* /> */} - {/* */} - {/* */} - {/* */} - {/* Specify the IP */} - {/* address of the host. */} - {/* */} - {/* */} - {/* )} */} - {/* /> */} + {env.flags.enableClients && ( + ( + + + Site Address + + + { + setClientAddress( + e + .target + .value + ); + field.onChange( + e + .target + .value + ); + }} + /> + + + + Specify the IP + address of the + host. + + + )} + /> + )} @@ -622,10 +646,10 @@ WantedBy=default.target` - {t('tunnelType')} + {t("tunnelType")} - {t('siteTunnelDescription')} + {t("siteTunnelDescription")} @@ -646,17 +670,19 @@ WantedBy=default.target` - {t('siteNewtCredentials')} + {t("siteNewtCredentials")} - {t('siteNewtCredentialsDescription')} + {t( + "siteNewtCredentialsDescription" + )} - {t('newtEndpoint')} + {t("newtEndpoint")} - {t('newtId')} + {t("newtId")} - {t('newtSecretKey')} + {t("newtSecretKey")} - {t('siteCredentialsSave')} + {t("siteCredentialsSave")} - {t('siteCredentialsSaveDescription')} + {t( + "siteCredentialsSaveDescription" + )} @@ -743,16 +771,16 @@ WantedBy=default.target` - {t('siteInstallNewt')} + {t("siteInstallNewt")} - {t('siteInstallNewtDescription')} + {t("siteInstallNewtDescription")}

- {t('operatingSystem')} + {t("operatingSystem")}

{platforms.map((os) => ( @@ -780,8 +808,8 @@ WantedBy=default.target` {["docker", "podman"].includes( platform ) - ? t('method') - : t('architecture')} + ? t("method") + : t("architecture")}

{getArchitectures().map( @@ -808,7 +836,7 @@ WantedBy=default.target`

- {t('commands')} + {t("commands")}

- {t('WgConfiguration')} + {t("WgConfiguration")} - {t('WgConfigurationDescription')} + {t("WgConfigurationDescription")} @@ -853,10 +881,12 @@ WantedBy=default.target` - {t('siteCredentialsSave')} + {t("siteCredentialsSave")} - {t('siteCredentialsSaveDescription')} + {t( + "siteCredentialsSaveDescription" + )} @@ -891,7 +921,9 @@ WantedBy=default.target` htmlFor="terms" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" > - {t('siteConfirmCopy')} + {t( + "siteConfirmCopy" + )}
@@ -913,7 +945,7 @@ WantedBy=default.target` router.push(`/${orgId}/settings/sites`); }} > - {t('cancel')} + {t("cancel")}
diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index 546dba26..021cfdf8 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -11,7 +11,8 @@ import { KeyRound, TicketCheck, User, - Globe + Globe, + MonitorUp } from "lucide-react"; export type SidebarNavSection = { @@ -19,7 +20,7 @@ export type SidebarNavSection = { items: SidebarNavItem[]; }; -export const orgNavSections: SidebarNavSection[] = [ +export const orgNavSections = (enableClients: boolean = true): SidebarNavSection[] => [ { heading: "General", items: [ @@ -33,11 +34,16 @@ export const orgNavSections: SidebarNavSection[] = [ href: "/{orgId}/settings/resources", icon: }, + ...(enableClients ? [{ + title: "sidebarClients", + href: "/{orgId}/settings/clients", + icon: + }] : []), { title: "sidebarDomains", href: "/{orgId}/settings/domains", icon: - } + }, ] }, { diff --git a/src/app/setup/page.tsx b/src/app/setup/page.tsx index d6f45bbb..995893d8 100644 --- a/src/app/setup/page.tsx +++ b/src/app/setup/page.tsx @@ -11,7 +11,7 @@ import { CardHeader, CardTitle } from "@app/components/ui/card"; -import { formatAxiosError } from "@app/lib/api";; +import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { Separator } from "@/components/ui/separator"; @@ -38,6 +38,7 @@ export default function StepperForm() { const [currentStep, setCurrentStep] = useState("org"); const [orgIdTaken, setOrgIdTaken] = useState(false); const t = useTranslations(); + const { env } = useEnvContext(); const [loading, setLoading] = useState(false); const [isChecked, setIsChecked] = useState(false); @@ -45,9 +46,9 @@ export default function StepperForm() { const [orgCreated, setOrgCreated] = useState(false); const orgSchema = z.object({ - orgName: z.string().min(1, { message: t('orgNameRequired') }), - orgId: z.string().min(1, { message: t('orgIdRequired') }), - subnet: z.string().min(1, { message: t('subnetRequired') }) + orgName: z.string().min(1, { message: t("orgNameRequired") }), + orgId: z.string().min(1, { message: t("orgIdRequired") }), + subnet: z.string().min(1, { message: t("subnetRequired") }) }); const orgForm = useForm>({ @@ -83,21 +84,24 @@ export default function StepperForm() { } }; - const checkOrgIdAvailability = useCallback(async (value: string) => { - if (loading || orgCreated) { - return; - } - try { - const res = await api.get(`/org/checkId`, { - params: { - orgId: value - } - }); - setOrgIdTaken(res.status !== 404); - } catch (error) { - setOrgIdTaken(false); - } - }, [loading, orgCreated, api]); + const checkOrgIdAvailability = useCallback( + async (value: string) => { + if (loading || orgCreated) { + return; + } + try { + const res = await api.get(`/org/checkId`, { + params: { + orgId: value + } + }); + setOrgIdTaken(res.status !== 404); + } catch (error) { + setOrgIdTaken(false); + } + }, + [loading, orgCreated, api] + ); const debouncedCheckOrgIdAvailability = useCallback( debounce(checkOrgIdAvailability, 300), @@ -135,9 +139,7 @@ export default function StepperForm() { } } catch (e) { console.error(e); - setError( - formatAxiosError(e, t('orgErrorCreate')) - ); + setError(formatAxiosError(e, t("orgErrorCreate"))); } setLoading(false); @@ -147,10 +149,8 @@ export default function StepperForm() { <> - {t('setupNewOrg')} - - {t('setupCreate')} - + {t("setupNewOrg")} + {t("setupCreate")}
@@ -172,7 +172,7 @@ export default function StepperForm() { : "text-muted-foreground" }`} > - {t('setupCreateOrg')} + {t("setupCreateOrg")}
@@ -192,7 +192,7 @@ export default function StepperForm() { : "text-muted-foreground" }`} > - {t('siteCreate')} + {t("siteCreate")}
@@ -212,7 +212,7 @@ export default function StepperForm() { : "text-muted-foreground" }`} > - {t('setupCreateResources')} + {t("setupCreateResources")}
@@ -231,7 +231,7 @@ export default function StepperForm() { render={({ field }) => ( - {t('setupOrgName')} + {t("setupOrgName")} { // Prevent "/" in orgName input - const sanitizedValue = e.target.value.replace(/\//g, "-"); - const orgId = generateId(sanitizedValue); + const sanitizedValue = + e.target.value.replace( + /\//g, + "-" + ); + const orgId = + generateId( + sanitizedValue + ); orgForm.setValue( "orgId", orgId @@ -253,12 +260,15 @@ export default function StepperForm() { orgId ); }} - value={field.value.replace(/\//g, "-")} + value={field.value.replace( + /\//g, + "-" + )} /> - {t('orgDisplayName')} + {t("orgDisplayName")} )} @@ -269,7 +279,7 @@ export default function StepperForm() { render={({ field }) => ( - {t('orgId')} + {t("orgId")} - {t('setupIdentifierMessage')} - - - )} - /> - - ( - - - Subnet - - - - - - - Network subnet for this organization. - A default value has been provided. + {t( + "setupIdentifierMessage" + )} )} /> + {env.flags.enableClients && ( + ( + + + Subnet + + + + + + + Network subnet for this + organization. A default + value has been provided. + + + )} + /> + )} + {orgIdTaken && !orgCreated ? ( - {t('setupErrorIdentifier')} + {t("setupErrorIdentifier")} ) : null} @@ -334,7 +349,7 @@ export default function StepperForm() { orgIdTaken } > - {t('setupCreateOrg')} + {t("setupCreateOrg")} @@ -360,4 +375,4 @@ function debounce any>( func(...args); }, wait); }; -} \ No newline at end of file +}