Reintroduce clients conditionally

This commit is contained in:
Owen 2025-07-14 11:43:13 -07:00
parent a35add3fc6
commit b75800c583
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
9 changed files with 331 additions and 270 deletions

View file

@ -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,

View file

@ -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<any> {
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<PickOrgDefaultsResponse>(res, {
data: {

View file

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

View file

@ -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
},

View file

@ -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={
<div>
<p className="mb-2">
{t('orgQuestionRemove', {selectedOrg: org?.org.name})}
</p>
<p className="mb-2">
{t('orgMessageRemove')}
</p>
<p>
{t('orgMessageConfirm')}
{t("orgQuestionRemove", {
selectedOrg: org?.org.name
})}
</p>
<p className="mb-2">{t("orgMessageRemove")}</p>
<p>{t("orgMessageConfirm")}</p>
</div>
}
buttonText={t('orgDeleteConfirm')}
buttonText={t("orgDeleteConfirm")}
onConfirm={deleteOrg}
string={org?.org.name || ""}
title={t('orgDelete')}
title={t("orgDelete")}
/>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('orgGeneralSettings')}
{t("orgGeneralSettings")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('orgGeneralSettingsDescription')}
{t("orgGeneralSettingsDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@ -190,37 +189,40 @@ export default function GeneralPage() {
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t('name')}</FormLabel>
<FormLabel>{t("name")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
<FormDescription>
{t('orgDisplayName')}
{t("orgDisplayName")}
</FormDescription>
</FormItem>
)}
/>
{/* <FormField */}
{/* control={form.control} */}
{/* name="subnet" */}
{/* render={({ field }) => ( */}
{/* <FormItem> */}
{/* <FormLabel>Subnet</FormLabel> */}
{/* <FormControl> */}
{/* <Input */}
{/* {...field} */}
{/* disabled={true} */}
{/* /> */}
{/* </FormControl> */}
{/* <FormMessage /> */}
{/* <FormDescription> */}
{/* The subnet for this organization's network configuration. */}
{/* </FormDescription> */}
{/* </FormItem> */}
{/* )} */}
{/* /> */}
{env.flags.enableClients && (
<FormField
control={form.control}
name="subnet"
render={({ field }) => (
<FormItem>
<FormLabel>Subnet</FormLabel>
<FormControl>
<Input
{...field}
disabled={true}
/>
</FormControl>
<FormMessage />
<FormDescription>
The subnet for this
organization's network
configuration.
</FormDescription>
</FormItem>
)}
/>
)}
</form>
</Form>
</SettingsSectionForm>
@ -232,15 +234,17 @@ export default function GeneralPage() {
loading={loadingSave}
disabled={loadingSave}
>
{t('saveGeneralSettings')}
{t("saveGeneralSettings")}
</Button>
</SettingsSectionFooter>
</SettingsSection>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>{t('orgDangerZone')}</SettingsSectionTitle>
<SettingsSectionTitle>
{t("orgDangerZone")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('orgDangerZoneDescription')}
{t("orgDangerZoneDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionFooter>
@ -251,7 +255,7 @@ export default function GeneralPage() {
loading={loadingDelete}
disabled={loadingDelete}
>
{t('orgDelete')}
{t("orgDelete")}
</Button>
</SettingsSectionFooter>
</SettingsSection>

View file

@ -86,7 +86,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
return (
<UserProvider user={user}>
<Layout orgId={params.orgId} orgs={orgs} navItems={orgNavSections}>
<Layout orgId={params.orgId} orgs={orgs} navItems={orgNavSections(env.flags.enableClients)}>
{children}
</Layout>
</UserProvider>

View file

@ -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`
<>
<div className="flex justify-between">
<HeaderTitle
title={t('siteCreate')}
description={t('siteCreateDescription2')}
title={t("siteCreate")}
description={t("siteCreateDescription2")}
/>
<Button
variant="outline"
@ -535,7 +552,7 @@ WantedBy=default.target`
router.push(`/${orgId}/settings/sites`);
}}
>
{t('siteSeeAll')}
{t("siteSeeAll")}
</Button>
</div>
@ -545,7 +562,7 @@ WantedBy=default.target`
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('siteInfo')}
{t("siteInfo")}
</SettingsSectionTitle>
</SettingsSectionHeader>
<SettingsSectionBody>
@ -561,7 +578,7 @@ WantedBy=default.target`
render={({ field }) => (
<FormItem>
<FormLabel>
{t('name')}
{t("name")}
</FormLabel>
<FormControl>
<Input
@ -571,47 +588,54 @@ WantedBy=default.target`
</FormControl>
<FormMessage />
<FormDescription>
{t('siteNameDescription')}
{t(
"siteNameDescription"
)}
</FormDescription>
</FormItem>
)}
/>
{/* <FormField */}
{/* control={form.control} */}
{/* name="clientAddress" */}
{/* render={({ field }) => ( */}
{/* <FormItem> */}
{/* <FormLabel> */}
{/* Site Address */}
{/* </FormLabel> */}
{/* <FormControl> */}
{/* <Input */}
{/* autoComplete="off" */}
{/* value={ */}
{/* clientAddress */}
{/* } */}
{/* onChange={( */}
{/* e */}
{/* ) => { */}
{/* setClientAddress( */}
{/* e.target */}
{/* .value */}
{/* ); */}
{/* field.onChange( */}
{/* e.target */}
{/* .value */}
{/* ); */}
{/* }} */}
{/* /> */}
{/* </FormControl> */}
{/* <FormMessage /> */}
{/* <FormDescription> */}
{/* Specify the IP */}
{/* address of the host. */}
{/* </FormDescription> */}
{/* </FormItem> */}
{/* )} */}
{/* /> */}
{env.flags.enableClients && (
<FormField
control={form.control}
name="clientAddress"
render={({ field }) => (
<FormItem>
<FormLabel>
Site Address
</FormLabel>
<FormControl>
<Input
autoComplete="off"
value={
clientAddress
}
onChange={(
e
) => {
setClientAddress(
e
.target
.value
);
field.onChange(
e
.target
.value
);
}}
/>
</FormControl>
<FormMessage />
<FormDescription>
Specify the IP
address of the
host.
</FormDescription>
</FormItem>
)}
/>
)}
</form>
</Form>
</SettingsSectionForm>
@ -622,10 +646,10 @@ WantedBy=default.target`
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('tunnelType')}
{t("tunnelType")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('siteTunnelDescription')}
{t("siteTunnelDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@ -646,17 +670,19 @@ WantedBy=default.target`
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('siteNewtCredentials')}
{t("siteNewtCredentials")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('siteNewtCredentialsDescription')}
{t(
"siteNewtCredentialsDescription"
)}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<InfoSections cols={3}>
<InfoSection>
<InfoSectionTitle>
{t('newtEndpoint')}
{t("newtEndpoint")}
</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard
@ -668,7 +694,7 @@ WantedBy=default.target`
</InfoSection>
<InfoSection>
<InfoSectionTitle>
{t('newtId')}
{t("newtId")}
</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard
@ -678,7 +704,7 @@ WantedBy=default.target`
</InfoSection>
<InfoSection>
<InfoSectionTitle>
{t('newtSecretKey')}
{t("newtSecretKey")}
</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard
@ -691,10 +717,12 @@ WantedBy=default.target`
<Alert variant="neutral" className="">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
{t('siteCredentialsSave')}
{t("siteCredentialsSave")}
</AlertTitle>
<AlertDescription>
{t('siteCredentialsSaveDescription')}
{t(
"siteCredentialsSaveDescription"
)}
</AlertDescription>
</Alert>
@ -743,16 +771,16 @@ WantedBy=default.target`
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('siteInstallNewt')}
{t("siteInstallNewt")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('siteInstallNewtDescription')}
{t("siteInstallNewtDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<div>
<p className="font-bold mb-3">
{t('operatingSystem')}
{t("operatingSystem")}
</p>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{platforms.map((os) => (
@ -780,8 +808,8 @@ WantedBy=default.target`
{["docker", "podman"].includes(
platform
)
? t('method')
: t('architecture')}
? t("method")
: t("architecture")}
</p>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{getArchitectures().map(
@ -808,7 +836,7 @@ WantedBy=default.target`
</div>
<div className="pt-4">
<p className="font-bold mb-3">
{t('commands')}
{t("commands")}
</p>
<div className="mt-2">
<CopyTextBox
@ -829,10 +857,10 @@ WantedBy=default.target`
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('WgConfiguration')}
{t("WgConfiguration")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('WgConfigurationDescription')}
{t("WgConfigurationDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@ -853,10 +881,12 @@ WantedBy=default.target`
<Alert variant="neutral">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
{t('siteCredentialsSave')}
{t("siteCredentialsSave")}
</AlertTitle>
<AlertDescription>
{t('siteCredentialsSaveDescription')}
{t(
"siteCredentialsSaveDescription"
)}
</AlertDescription>
</Alert>
@ -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"
)}
</label>
</div>
<FormMessage />
@ -913,7 +945,7 @@ WantedBy=default.target`
router.push(`/${orgId}/settings/sites`);
}}
>
{t('cancel')}
{t("cancel")}
</Button>
<Button
type="button"
@ -923,7 +955,7 @@ WantedBy=default.target`
form.handleSubmit(onSubmit)();
}}
>
{t('siteCreate')}
{t("siteCreate")}
</Button>
</div>
</div>

View file

@ -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: <Waypoints className="h-4 w-4" />
},
...(enableClients ? [{
title: "sidebarClients",
href: "/{orgId}/settings/clients",
icon: <MonitorUp className="h-4 w-4" />
}] : []),
{
title: "sidebarDomains",
href: "/{orgId}/settings/domains",
icon: <Globe className="h-4 w-4" />
}
},
]
},
{

View file

@ -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<Step>("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<z.infer<typeof orgSchema>>({
@ -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() {
<>
<Card>
<CardHeader>
<CardTitle>{t('setupNewOrg')}</CardTitle>
<CardDescription>
{t('setupCreate')}
</CardDescription>
<CardTitle>{t("setupNewOrg")}</CardTitle>
<CardDescription>{t("setupCreate")}</CardDescription>
</CardHeader>
<CardContent>
<section className="space-y-6">
@ -172,7 +172,7 @@ export default function StepperForm() {
: "text-muted-foreground"
}`}
>
{t('setupCreateOrg')}
{t("setupCreateOrg")}
</span>
</div>
<div className="flex flex-col items-center">
@ -192,7 +192,7 @@ export default function StepperForm() {
: "text-muted-foreground"
}`}
>
{t('siteCreate')}
{t("siteCreate")}
</span>
</div>
<div className="flex flex-col items-center">
@ -212,7 +212,7 @@ export default function StepperForm() {
: "text-muted-foreground"
}`}
>
{t('setupCreateResources')}
{t("setupCreateResources")}
</span>
</div>
</div>
@ -231,7 +231,7 @@ export default function StepperForm() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('setupOrgName')}
{t("setupOrgName")}
</FormLabel>
<FormControl>
<Input
@ -239,8 +239,15 @@ export default function StepperForm() {
{...field}
onChange={(e) => {
// 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,
"-"
)}
/>
</FormControl>
<FormMessage />
<FormDescription>
{t('orgDisplayName')}
{t("orgDisplayName")}
</FormDescription>
</FormItem>
)}
@ -269,7 +279,7 @@ export default function StepperForm() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('orgId')}
{t("orgId")}
</FormLabel>
<FormControl>
<Input
@ -279,39 +289,44 @@ export default function StepperForm() {
</FormControl>
<FormMessage />
<FormDescription>
{t('setupIdentifierMessage')}
</FormDescription>
</FormItem>
)}
/>
<FormField
control={orgForm.control}
name="subnet"
render={({ field }) => (
<FormItem>
<FormLabel>
Subnet
</FormLabel>
<FormControl>
<Input
type="text"
{...field}
/>
</FormControl>
<FormMessage />
<FormDescription>
Network subnet for this organization.
A default value has been provided.
{t(
"setupIdentifierMessage"
)}
</FormDescription>
</FormItem>
)}
/>
{env.flags.enableClients && (
<FormField
control={orgForm.control}
name="subnet"
render={({ field }) => (
<FormItem>
<FormLabel>
Subnet
</FormLabel>
<FormControl>
<Input
type="text"
{...field}
/>
</FormControl>
<FormMessage />
<FormDescription>
Network subnet for this
organization. A default
value has been provided.
</FormDescription>
</FormItem>
)}
/>
)}
{orgIdTaken && !orgCreated ? (
<Alert variant="destructive">
<AlertDescription>
{t('setupErrorIdentifier')}
{t("setupErrorIdentifier")}
</AlertDescription>
</Alert>
) : null}
@ -334,7 +349,7 @@ export default function StepperForm() {
orgIdTaken
}
>
{t('setupCreateOrg')}
{t("setupCreateOrg")}
</Button>
</div>
</form>
@ -360,4 +375,4 @@ function debounce<T extends (...args: any[]) => any>(
func(...args);
}, wait);
};
}
}