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 next: NextFunction
) { ) {
try { try {
if (!config.getRawConfig().flags?.enable_redis) { if (!config.getRawConfig().flags?.enable_clients) {
return next( return next(
createHttpError( createHttpError(
HttpCode.NOT_IMPLEMENTED, HttpCode.NOT_IMPLEMENTED,

View file

@ -4,6 +4,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { getNextAvailableOrgSubnet } from "@server/lib/ip"; import { getNextAvailableOrgSubnet } from "@server/lib/ip";
import config from "@server/lib/config";
export type PickOrgDefaultsResponse = { export type PickOrgDefaultsResponse = {
subnet: string; subnet: string;
@ -15,7 +16,10 @@ export async function pickOrgDefaults(
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
try { 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, { return response<PickOrgDefaultsResponse>(res, {
data: { data: {

View file

@ -38,7 +38,7 @@ const createSiteSchema = z
subnet: z.string().optional(), subnet: z.string().optional(),
newtId: z.string().optional(), newtId: z.string().optional(),
secret: z.string().optional(), secret: z.string().optional(),
// address: z.string().optional(), address: z.string().optional(),
type: z.enum(["newt", "wireguard", "local"]) type: z.enum(["newt", "wireguard", "local"])
}) })
.strict() .strict()
@ -97,7 +97,7 @@ export async function createSite(
subnet, subnet,
newtId, newtId,
secret, secret,
// address address
} = parsedBody.data; } = parsedBody.data;
const parsedParams = createSiteParamsSchema.safeParse(req.params); const parsedParams = createSiteParamsSchema.safeParse(req.params);
@ -129,58 +129,58 @@ export async function createSite(
); );
} }
// let updatedAddress = null; let updatedAddress = null;
// if (address) { if (address) {
// if (!isValidIP(address)) { if (!isValidIP(address)) {
// return next( return next(
// createHttpError( createHttpError(
// HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
// "Invalid subnet format. Please provide a valid CIDR notation." "Invalid subnet format. Please provide a valid CIDR notation."
// ) )
// ); );
// } }
//
// if (!isIpInCidr(address, org.subnet)) { if (!isIpInCidr(address, org.subnet)) {
// return next( return next(
// createHttpError( createHttpError(
// HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
// "IP is not in the CIDR range of the subnet." "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 updatedAddress = `${address}/${org.subnet.split("/")[1]}`; // we want the block size of the whole org
//
// // make sure the subnet is unique // make sure the subnet is unique
// const addressExistsSites = await db const addressExistsSites = await db
// .select() .select()
// .from(sites) .from(sites)
// .where(eq(sites.address, updatedAddress)) .where(eq(sites.address, updatedAddress))
// .limit(1); .limit(1);
//
// if (addressExistsSites.length > 0) { if (addressExistsSites.length > 0) {
// return next( return next(
// createHttpError( createHttpError(
// HttpCode.CONFLICT, HttpCode.CONFLICT,
// `Subnet ${subnet} already exists` `Subnet ${subnet} already exists`
// ) )
// ); );
// } }
//
// const addressExistsClients = await db const addressExistsClients = await db
// .select() .select()
// .from(sites) .from(sites)
// .where(eq(sites.subnet, updatedAddress)) .where(eq(sites.subnet, updatedAddress))
// .limit(1); .limit(1);
// if (addressExistsClients.length > 0) { if (addressExistsClients.length > 0) {
// return next( return next(
// createHttpError( createHttpError(
// HttpCode.CONFLICT, HttpCode.CONFLICT,
// `Subnet ${subnet} already exists` `Subnet ${subnet} already exists`
// ) )
// ); );
// } }
// } }
const niceId = await getUniqueSiteName(orgId); const niceId = await getUniqueSiteName(orgId);

View file

@ -108,17 +108,17 @@ export async function pickSiteDefaults(
); );
} }
// const newClientAddress = await getNextAvailableClientSubnet(orgId); const newClientAddress = await getNextAvailableClientSubnet(orgId);
// if (!newClientAddress) { if (!newClientAddress) {
// return next( return next(
// createHttpError( createHttpError(
// HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,
// "No available subnet found" "No available subnet found"
// ) )
// ); );
// } }
// const clientAddress = newClientAddress.split("/")[0]; const clientAddress = newClientAddress.split("/")[0];
const newtId = generateId(15); const newtId = generateId(15);
const secret = generateId(48); const secret = generateId(48);
@ -133,7 +133,7 @@ export async function pickSiteDefaults(
endpoint: exitNode.endpoint, endpoint: exitNode.endpoint,
// subnet: `${newSubnet.split("/")[0]}/${config.getRawConfig().gerbil.block_size}`, // we want the block size of the whole subnet // subnet: `${newSubnet.split("/")[0]}/${config.getRawConfig().gerbil.block_size}`, // we want the block size of the whole subnet
subnet: newSubnet, subnet: newSubnet,
// clientAddress: clientAddress, clientAddress: clientAddress,
newtId, newtId,
newtSecret: secret newtSecret: secret
}, },

View file

@ -35,7 +35,7 @@ import {
SettingsSectionFooter SettingsSectionFooter
} from "@app/components/Settings"; } from "@app/components/Settings";
import { useUserContext } from "@app/hooks/useUserContext"; import { useUserContext } from "@app/hooks/useUserContext";
import { useTranslations } from 'next-intl'; import { useTranslations } from "next-intl";
// Updated schema to include subnet field // Updated schema to include subnet field
const GeneralFormSchema = z.object({ const GeneralFormSchema = z.object({
@ -53,6 +53,7 @@ export default function GeneralPage() {
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const { user } = useUserContext(); const { user } = useUserContext();
const t = useTranslations(); const t = useTranslations();
const { env } = useEnvContext();
const [loadingDelete, setLoadingDelete] = useState(false); const [loadingDelete, setLoadingDelete] = useState(false);
const [loadingSave, setLoadingSave] = useState(false); const [loadingSave, setLoadingSave] = useState(false);
@ -73,8 +74,8 @@ export default function GeneralPage() {
`/org/${org?.org.orgId}` `/org/${org?.org.orgId}`
); );
toast({ toast({
title: t('orgDeleted'), title: t("orgDeleted"),
description: t('orgDeletedMessage') description: t("orgDeletedMessage")
}); });
if (res.status === 200) { if (res.status === 200) {
pickNewOrgAndNavigate(); pickNewOrgAndNavigate();
@ -83,8 +84,8 @@ export default function GeneralPage() {
console.error(err); console.error(err);
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('orgErrorDelete'), title: t("orgErrorDelete"),
description: formatAxiosError(err, t('orgErrorDeleteMessage')) description: formatAxiosError(err, t("orgErrorDeleteMessage"))
}); });
} finally { } finally {
setLoadingDelete(false); setLoadingDelete(false);
@ -111,8 +112,8 @@ export default function GeneralPage() {
console.error(err); console.error(err);
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('orgErrorFetch'), title: t("orgErrorFetch"),
description: formatAxiosError(err, t('orgErrorFetchMessage')) description: formatAxiosError(err, t("orgErrorFetchMessage"))
}); });
} }
} }
@ -126,16 +127,16 @@ export default function GeneralPage() {
}) })
.then(() => { .then(() => {
toast({ toast({
title: t('orgUpdated'), title: t("orgUpdated"),
description: t('orgUpdatedDescription') description: t("orgUpdatedDescription")
}); });
router.refresh(); router.refresh();
}) })
.catch((e) => { .catch((e) => {
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('orgErrorUpdate'), title: t("orgErrorUpdate"),
description: formatAxiosError(e, t('orgErrorUpdateMessage')) description: formatAxiosError(e, t("orgErrorUpdateMessage"))
}); });
}) })
.finally(() => { .finally(() => {
@ -153,28 +154,26 @@ export default function GeneralPage() {
dialog={ dialog={
<div> <div>
<p className="mb-2"> <p className="mb-2">
{t('orgQuestionRemove', {selectedOrg: org?.org.name})} {t("orgQuestionRemove", {
</p> selectedOrg: org?.org.name
<p className="mb-2"> })}
{t('orgMessageRemove')}
</p>
<p>
{t('orgMessageConfirm')}
</p> </p>
<p className="mb-2">{t("orgMessageRemove")}</p>
<p>{t("orgMessageConfirm")}</p>
</div> </div>
} }
buttonText={t('orgDeleteConfirm')} buttonText={t("orgDeleteConfirm")}
onConfirm={deleteOrg} onConfirm={deleteOrg}
string={org?.org.name || ""} string={org?.org.name || ""}
title={t('orgDelete')} title={t("orgDelete")}
/> />
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t('orgGeneralSettings')} {t("orgGeneralSettings")}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t('orgGeneralSettingsDescription')} {t("orgGeneralSettingsDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
@ -190,37 +189,40 @@ export default function GeneralPage() {
name="name" name="name"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('name')}</FormLabel> <FormLabel>{t("name")}</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
<FormDescription> <FormDescription>
{t('orgDisplayName')} {t("orgDisplayName")}
</FormDescription> </FormDescription>
</FormItem> </FormItem>
)} )}
/> />
{env.flags.enableClients && (
{/* <FormField */} <FormField
{/* control={form.control} */} control={form.control}
{/* name="subnet" */} name="subnet"
{/* render={({ field }) => ( */} render={({ field }) => (
{/* <FormItem> */} <FormItem>
{/* <FormLabel>Subnet</FormLabel> */} <FormLabel>Subnet</FormLabel>
{/* <FormControl> */} <FormControl>
{/* <Input */} <Input
{/* {...field} */} {...field}
{/* disabled={true} */} disabled={true}
{/* /> */} />
{/* </FormControl> */} </FormControl>
{/* <FormMessage /> */} <FormMessage />
{/* <FormDescription> */} <FormDescription>
{/* The subnet for this organization's network configuration. */} The subnet for this
{/* </FormDescription> */} organization's network
{/* </FormItem> */} configuration.
{/* )} */} </FormDescription>
{/* /> */} </FormItem>
)}
/>
)}
</form> </form>
</Form> </Form>
</SettingsSectionForm> </SettingsSectionForm>
@ -232,15 +234,17 @@ export default function GeneralPage() {
loading={loadingSave} loading={loadingSave}
disabled={loadingSave} disabled={loadingSave}
> >
{t('saveGeneralSettings')} {t("saveGeneralSettings")}
</Button> </Button>
</SettingsSectionFooter> </SettingsSectionFooter>
</SettingsSection> </SettingsSection>
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle>{t('orgDangerZone')}</SettingsSectionTitle> <SettingsSectionTitle>
{t("orgDangerZone")}
</SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t('orgDangerZoneDescription')} {t("orgDangerZoneDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionFooter> <SettingsSectionFooter>
@ -251,7 +255,7 @@ export default function GeneralPage() {
loading={loadingDelete} loading={loadingDelete}
disabled={loadingDelete} disabled={loadingDelete}
> >
{t('orgDelete')} {t("orgDelete")}
</Button> </Button>
</SettingsSectionFooter> </SettingsSectionFooter>
</SettingsSection> </SettingsSection>

View file

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

View file

@ -98,9 +98,9 @@ export default function Page() {
.object({ .object({
name: z name: z
.string() .string()
.min(2, { message: t('nameMin', {len: 2}) }) .min(2, { message: t("nameMin", { len: 2 }) })
.max(30, { .max(30, {
message: t('nameMax', {len: 30}) message: t("nameMax", { len: 30 })
}), }),
method: z.enum(["newt", "wireguard", "local"]), method: z.enum(["newt", "wireguard", "local"]),
copied: z.boolean(), copied: z.boolean(),
@ -115,7 +115,7 @@ export default function Page() {
return true; return true;
}, },
{ {
message: t('sitesConfirmCopy'), message: t("sitesConfirmCopy"),
path: ["copied"] path: ["copied"]
} }
); );
@ -127,21 +127,29 @@ export default function Page() {
>([ >([
{ {
id: "newt", id: "newt",
title: t('siteNewtTunnel'), title: t("siteNewtTunnel"),
description: t('siteNewtTunnelDescription'), description: t("siteNewtTunnelDescription"),
disabled: true disabled: true
}, },
...(env.flags.disableBasicWireguardSites ? [] : [{ ...(env.flags.disableBasicWireguardSites
? []
: [
{
id: "wireguard" as SiteType, id: "wireguard" as SiteType,
title: t('siteWg'), title: t("siteWg"),
description: t('siteWgDescription'), description: t("siteWgDescription"),
disabled: true disabled: true
}]), }
...(env.flags.disableLocalSites ? [] : [{ ]),
...(env.flags.disableLocalSites
? []
: [
{
id: "local" as SiteType, id: "local" as SiteType,
title: t('local'), title: t("local"),
description: t('siteLocalDescription') description: t("siteLocalDescription")
}]) }
])
]); ]);
const [loadingPage, setLoadingPage] = useState(true); const [loadingPage, setLoadingPage] = useState(true);
@ -319,7 +327,7 @@ WantedBy=default.target`
}; };
const getCommand = () => { const getCommand = () => {
const placeholder = [t('unknownCommand')]; const placeholder = [t("unknownCommand")];
if (!commands) { if (!commands) {
return placeholder; return placeholder;
} }
@ -384,8 +392,8 @@ WantedBy=default.target`
if (!siteDefaults || !wgConfig) { if (!siteDefaults || !wgConfig) {
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('siteErrorCreate'), title: t("siteErrorCreate"),
description: t('siteErrorCreateKeyPair') description: t("siteErrorCreateKeyPair")
}); });
setCreateLoading(false); setCreateLoading(false);
return; return;
@ -402,8 +410,8 @@ WantedBy=default.target`
if (!siteDefaults) { if (!siteDefaults) {
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('siteErrorCreate'), title: t("siteErrorCreate"),
description: t('siteErrorCreateDefaults') description: t("siteErrorCreateDefaults")
}); });
setCreateLoading(false); setCreateLoading(false);
return; return;
@ -415,7 +423,7 @@ WantedBy=default.target`
exitNodeId: siteDefaults.exitNodeId, exitNodeId: siteDefaults.exitNodeId,
secret: siteDefaults.newtSecret, secret: siteDefaults.newtSecret,
newtId: siteDefaults.newtId, newtId: siteDefaults.newtId,
// address: clientAddress address: clientAddress
}; };
} }
@ -426,7 +434,7 @@ WantedBy=default.target`
.catch((e) => { .catch((e) => {
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('siteErrorCreate'), title: t("siteErrorCreate"),
description: formatAxiosError(e) description: formatAxiosError(e)
}); });
}); });
@ -452,14 +460,23 @@ WantedBy=default.target`
); );
if (!response.ok) { if (!response.ok) {
throw new Error( throw new Error(
t('newtErrorFetchReleases', {err: response.statusText}) t("newtErrorFetchReleases", {
err: response.statusText
})
); );
} }
const data = await response.json(); const data = await response.json();
const latestVersion = data.tag_name; const latestVersion = data.tag_name;
newtVersion = latestVersion; newtVersion = latestVersion;
} catch (error) { } 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(); const generatedKeypair = generateKeypair();
@ -526,8 +543,8 @@ WantedBy=default.target`
<> <>
<div className="flex justify-between"> <div className="flex justify-between">
<HeaderTitle <HeaderTitle
title={t('siteCreate')} title={t("siteCreate")}
description={t('siteCreateDescription2')} description={t("siteCreateDescription2")}
/> />
<Button <Button
variant="outline" variant="outline"
@ -535,7 +552,7 @@ WantedBy=default.target`
router.push(`/${orgId}/settings/sites`); router.push(`/${orgId}/settings/sites`);
}} }}
> >
{t('siteSeeAll')} {t("siteSeeAll")}
</Button> </Button>
</div> </div>
@ -545,7 +562,7 @@ WantedBy=default.target`
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t('siteInfo')} {t("siteInfo")}
</SettingsSectionTitle> </SettingsSectionTitle>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
@ -561,7 +578,7 @@ WantedBy=default.target`
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t('name')} {t("name")}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
@ -571,47 +588,54 @@ WantedBy=default.target`
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
<FormDescription> <FormDescription>
{t('siteNameDescription')} {t(
"siteNameDescription"
)}
</FormDescription> </FormDescription>
</FormItem> </FormItem>
)} )}
/> />
{/* <FormField */} {env.flags.enableClients && (
{/* control={form.control} */} <FormField
{/* name="clientAddress" */} control={form.control}
{/* render={({ field }) => ( */} name="clientAddress"
{/* <FormItem> */} render={({ field }) => (
{/* <FormLabel> */} <FormItem>
{/* Site Address */} <FormLabel>
{/* </FormLabel> */} Site Address
{/* <FormControl> */} </FormLabel>
{/* <Input */} <FormControl>
{/* autoComplete="off" */} <Input
{/* value={ */} autoComplete="off"
{/* clientAddress */} value={
{/* } */} clientAddress
{/* onChange={( */} }
{/* e */} onChange={(
{/* ) => { */} e
{/* setClientAddress( */} ) => {
{/* e.target */} setClientAddress(
{/* .value */} e
{/* ); */} .target
{/* field.onChange( */} .value
{/* e.target */} );
{/* .value */} field.onChange(
{/* ); */} e
{/* }} */} .target
{/* /> */} .value
{/* </FormControl> */} );
{/* <FormMessage /> */} }}
{/* <FormDescription> */} />
{/* Specify the IP */} </FormControl>
{/* address of the host. */} <FormMessage />
{/* </FormDescription> */} <FormDescription>
{/* </FormItem> */} Specify the IP
{/* )} */} address of the
{/* /> */} host.
</FormDescription>
</FormItem>
)}
/>
)}
</form> </form>
</Form> </Form>
</SettingsSectionForm> </SettingsSectionForm>
@ -622,10 +646,10 @@ WantedBy=default.target`
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t('tunnelType')} {t("tunnelType")}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t('siteTunnelDescription')} {t("siteTunnelDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
@ -646,17 +670,19 @@ WantedBy=default.target`
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t('siteNewtCredentials')} {t("siteNewtCredentials")}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t('siteNewtCredentialsDescription')} {t(
"siteNewtCredentialsDescription"
)}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<InfoSections cols={3}> <InfoSections cols={3}>
<InfoSection> <InfoSection>
<InfoSectionTitle> <InfoSectionTitle>
{t('newtEndpoint')} {t("newtEndpoint")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
<CopyToClipboard <CopyToClipboard
@ -668,7 +694,7 @@ WantedBy=default.target`
</InfoSection> </InfoSection>
<InfoSection> <InfoSection>
<InfoSectionTitle> <InfoSectionTitle>
{t('newtId')} {t("newtId")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
<CopyToClipboard <CopyToClipboard
@ -678,7 +704,7 @@ WantedBy=default.target`
</InfoSection> </InfoSection>
<InfoSection> <InfoSection>
<InfoSectionTitle> <InfoSectionTitle>
{t('newtSecretKey')} {t("newtSecretKey")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
<CopyToClipboard <CopyToClipboard
@ -691,10 +717,12 @@ WantedBy=default.target`
<Alert variant="neutral" className=""> <Alert variant="neutral" className="">
<InfoIcon className="h-4 w-4" /> <InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold"> <AlertTitle className="font-semibold">
{t('siteCredentialsSave')} {t("siteCredentialsSave")}
</AlertTitle> </AlertTitle>
<AlertDescription> <AlertDescription>
{t('siteCredentialsSaveDescription')} {t(
"siteCredentialsSaveDescription"
)}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@ -743,16 +771,16 @@ WantedBy=default.target`
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t('siteInstallNewt')} {t("siteInstallNewt")}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t('siteInstallNewtDescription')} {t("siteInstallNewtDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<div> <div>
<p className="font-bold mb-3"> <p className="font-bold mb-3">
{t('operatingSystem')} {t("operatingSystem")}
</p> </p>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2"> <div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{platforms.map((os) => ( {platforms.map((os) => (
@ -780,8 +808,8 @@ WantedBy=default.target`
{["docker", "podman"].includes( {["docker", "podman"].includes(
platform platform
) )
? t('method') ? t("method")
: t('architecture')} : t("architecture")}
</p> </p>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2"> <div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{getArchitectures().map( {getArchitectures().map(
@ -808,7 +836,7 @@ WantedBy=default.target`
</div> </div>
<div className="pt-4"> <div className="pt-4">
<p className="font-bold mb-3"> <p className="font-bold mb-3">
{t('commands')} {t("commands")}
</p> </p>
<div className="mt-2"> <div className="mt-2">
<CopyTextBox <CopyTextBox
@ -829,10 +857,10 @@ WantedBy=default.target`
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t('WgConfiguration')} {t("WgConfiguration")}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t('WgConfigurationDescription')} {t("WgConfigurationDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
@ -853,10 +881,12 @@ WantedBy=default.target`
<Alert variant="neutral"> <Alert variant="neutral">
<InfoIcon className="h-4 w-4" /> <InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold"> <AlertTitle className="font-semibold">
{t('siteCredentialsSave')} {t("siteCredentialsSave")}
</AlertTitle> </AlertTitle>
<AlertDescription> <AlertDescription>
{t('siteCredentialsSaveDescription')} {t(
"siteCredentialsSaveDescription"
)}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@ -891,7 +921,9 @@ WantedBy=default.target`
htmlFor="terms" htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
> >
{t('siteConfirmCopy')} {t(
"siteConfirmCopy"
)}
</label> </label>
</div> </div>
<FormMessage /> <FormMessage />
@ -913,7 +945,7 @@ WantedBy=default.target`
router.push(`/${orgId}/settings/sites`); router.push(`/${orgId}/settings/sites`);
}} }}
> >
{t('cancel')} {t("cancel")}
</Button> </Button>
<Button <Button
type="button" type="button"
@ -923,7 +955,7 @@ WantedBy=default.target`
form.handleSubmit(onSubmit)(); form.handleSubmit(onSubmit)();
}} }}
> >
{t('siteCreate')} {t("siteCreate")}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -11,7 +11,8 @@ import {
KeyRound, KeyRound,
TicketCheck, TicketCheck,
User, User,
Globe Globe,
MonitorUp
} from "lucide-react"; } from "lucide-react";
export type SidebarNavSection = { export type SidebarNavSection = {
@ -19,7 +20,7 @@ export type SidebarNavSection = {
items: SidebarNavItem[]; items: SidebarNavItem[];
}; };
export const orgNavSections: SidebarNavSection[] = [ export const orgNavSections = (enableClients: boolean = true): SidebarNavSection[] => [
{ {
heading: "General", heading: "General",
items: [ items: [
@ -33,11 +34,16 @@ export const orgNavSections: SidebarNavSection[] = [
href: "/{orgId}/settings/resources", href: "/{orgId}/settings/resources",
icon: <Waypoints className="h-4 w-4" /> icon: <Waypoints className="h-4 w-4" />
}, },
...(enableClients ? [{
title: "sidebarClients",
href: "/{orgId}/settings/clients",
icon: <MonitorUp className="h-4 w-4" />
}] : []),
{ {
title: "sidebarDomains", title: "sidebarDomains",
href: "/{orgId}/settings/domains", href: "/{orgId}/settings/domains",
icon: <Globe className="h-4 w-4" /> icon: <Globe className="h-4 w-4" />
} },
] ]
}, },
{ {

View file

@ -11,7 +11,7 @@ import {
CardHeader, CardHeader,
CardTitle CardTitle
} from "@app/components/ui/card"; } from "@app/components/ui/card";
import { formatAxiosError } from "@app/lib/api";; import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
@ -38,6 +38,7 @@ export default function StepperForm() {
const [currentStep, setCurrentStep] = useState<Step>("org"); const [currentStep, setCurrentStep] = useState<Step>("org");
const [orgIdTaken, setOrgIdTaken] = useState(false); const [orgIdTaken, setOrgIdTaken] = useState(false);
const t = useTranslations(); const t = useTranslations();
const { env } = useEnvContext();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isChecked, setIsChecked] = useState(false); const [isChecked, setIsChecked] = useState(false);
@ -45,9 +46,9 @@ export default function StepperForm() {
const [orgCreated, setOrgCreated] = useState(false); const [orgCreated, setOrgCreated] = useState(false);
const orgSchema = z.object({ const orgSchema = z.object({
orgName: z.string().min(1, { message: t('orgNameRequired') }), orgName: z.string().min(1, { message: t("orgNameRequired") }),
orgId: z.string().min(1, { message: t('orgIdRequired') }), orgId: z.string().min(1, { message: t("orgIdRequired") }),
subnet: z.string().min(1, { message: t('subnetRequired') }) subnet: z.string().min(1, { message: t("subnetRequired") })
}); });
const orgForm = useForm<z.infer<typeof orgSchema>>({ const orgForm = useForm<z.infer<typeof orgSchema>>({
@ -83,7 +84,8 @@ export default function StepperForm() {
} }
}; };
const checkOrgIdAvailability = useCallback(async (value: string) => { const checkOrgIdAvailability = useCallback(
async (value: string) => {
if (loading || orgCreated) { if (loading || orgCreated) {
return; return;
} }
@ -97,7 +99,9 @@ export default function StepperForm() {
} catch (error) { } catch (error) {
setOrgIdTaken(false); setOrgIdTaken(false);
} }
}, [loading, orgCreated, api]); },
[loading, orgCreated, api]
);
const debouncedCheckOrgIdAvailability = useCallback( const debouncedCheckOrgIdAvailability = useCallback(
debounce(checkOrgIdAvailability, 300), debounce(checkOrgIdAvailability, 300),
@ -135,9 +139,7 @@ export default function StepperForm() {
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
setError( setError(formatAxiosError(e, t("orgErrorCreate")));
formatAxiosError(e, t('orgErrorCreate'))
);
} }
setLoading(false); setLoading(false);
@ -147,10 +149,8 @@ export default function StepperForm() {
<> <>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>{t('setupNewOrg')}</CardTitle> <CardTitle>{t("setupNewOrg")}</CardTitle>
<CardDescription> <CardDescription>{t("setupCreate")}</CardDescription>
{t('setupCreate')}
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<section className="space-y-6"> <section className="space-y-6">
@ -172,7 +172,7 @@ export default function StepperForm() {
: "text-muted-foreground" : "text-muted-foreground"
}`} }`}
> >
{t('setupCreateOrg')} {t("setupCreateOrg")}
</span> </span>
</div> </div>
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
@ -192,7 +192,7 @@ export default function StepperForm() {
: "text-muted-foreground" : "text-muted-foreground"
}`} }`}
> >
{t('siteCreate')} {t("siteCreate")}
</span> </span>
</div> </div>
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
@ -212,7 +212,7 @@ export default function StepperForm() {
: "text-muted-foreground" : "text-muted-foreground"
}`} }`}
> >
{t('setupCreateResources')} {t("setupCreateResources")}
</span> </span>
</div> </div>
</div> </div>
@ -231,7 +231,7 @@ export default function StepperForm() {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t('setupOrgName')} {t("setupOrgName")}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
@ -239,8 +239,15 @@ export default function StepperForm() {
{...field} {...field}
onChange={(e) => { onChange={(e) => {
// Prevent "/" in orgName input // Prevent "/" in orgName input
const sanitizedValue = e.target.value.replace(/\//g, "-"); const sanitizedValue =
const orgId = generateId(sanitizedValue); e.target.value.replace(
/\//g,
"-"
);
const orgId =
generateId(
sanitizedValue
);
orgForm.setValue( orgForm.setValue(
"orgId", "orgId",
orgId orgId
@ -253,12 +260,15 @@ export default function StepperForm() {
orgId orgId
); );
}} }}
value={field.value.replace(/\//g, "-")} value={field.value.replace(
/\//g,
"-"
)}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
<FormDescription> <FormDescription>
{t('orgDisplayName')} {t("orgDisplayName")}
</FormDescription> </FormDescription>
</FormItem> </FormItem>
)} )}
@ -269,7 +279,7 @@ export default function StepperForm() {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t('orgId')} {t("orgId")}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
@ -279,12 +289,15 @@ export default function StepperForm() {
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
<FormDescription> <FormDescription>
{t('setupIdentifierMessage')} {t(
"setupIdentifierMessage"
)}
</FormDescription> </FormDescription>
</FormItem> </FormItem>
)} )}
/> />
{env.flags.enableClients && (
<FormField <FormField
control={orgForm.control} control={orgForm.control}
name="subnet" name="subnet"
@ -301,17 +314,19 @@ export default function StepperForm() {
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
<FormDescription> <FormDescription>
Network subnet for this organization. Network subnet for this
A default value has been provided. organization. A default
value has been provided.
</FormDescription> </FormDescription>
</FormItem> </FormItem>
)} )}
/> />
)}
{orgIdTaken && !orgCreated ? ( {orgIdTaken && !orgCreated ? (
<Alert variant="destructive"> <Alert variant="destructive">
<AlertDescription> <AlertDescription>
{t('setupErrorIdentifier')} {t("setupErrorIdentifier")}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
) : null} ) : null}
@ -334,7 +349,7 @@ export default function StepperForm() {
orgIdTaken orgIdTaken
} }
> >
{t('setupCreateOrg')} {t("setupCreateOrg")}
</Button> </Button>
</div> </div>
</form> </form>