diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index 1151feac..9a5e6f46 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -24,7 +24,7 @@ const createSiteParamsSchema = z const createSiteSchema = z .object({ name: z.string().min(1).max(255), - exitNodeId: z.number().int().positive(), + exitNodeId: z.number().int().positive().optional(), // subdomain: z // .string() // .min(1) @@ -32,7 +32,7 @@ const createSiteSchema = z // .transform((val) => val.toLowerCase()) // .optional(), pubKey: z.string().optional(), - subnet: z.string(), + subnet: z.string().optional(), newtId: z.string().optional(), secret: z.string().optional(), type: z.string() @@ -82,28 +82,46 @@ export async function createSite( const niceId = await getUniqueSiteName(orgId); - let payload: any = { - orgId, - exitNodeId, - name, - niceId, - subnet, - type - }; - - if (pubKey && type == "wireguard") { - // we dont add the pubKey for newts because the newt will generate it - payload = { - ...payload, - pubKey - }; - } - await db.transaction(async (trx) => { - const [newSite] = await trx - .insert(sites) - .values(payload) - .returning(); + let newSite: Site; + + if (exitNodeId) { + // we are creating a site with an exit node (tunneled) + if (!subnet) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Subnet is required for tunneled sites" + ) + ); + } + + [newSite] = await trx + .insert(sites) + .values({ + orgId, + exitNodeId, + name, + niceId, + subnet, + type, + ...(pubKey && type == "wireguard" && { pubKey }) + }) + .returning(); + } else { + // we are creating a site with no tunneling + + [newSite] = await trx + .insert(sites) + .values({ + orgId, + name, + niceId, + type, + subnet: "0.0.0.0/0" + }) + .returning(); + } const adminRole = await trx .select() @@ -149,6 +167,16 @@ export async function createSite( ) ); } + + if (!exitNodeId) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Exit node ID is required for wireguard sites" + ) + ); + } + await addPeer(exitNodeId, { publicKey: pubKey, allowedIps: [] diff --git a/src/app/[orgId]/settings/sites/CreateSiteForm.tsx b/src/app/[orgId]/settings/sites/CreateSiteForm.tsx index 2515284d..61bb61de 100644 --- a/src/app/[orgId]/settings/sites/CreateSiteForm.tsx +++ b/src/app/[orgId]/settings/sites/CreateSiteForm.tsx @@ -49,7 +49,7 @@ const createSiteFormSchema = z.object({ .max(30, { message: "Name must not be longer than 30 characters." }), - method: z.enum(["wireguard", "newt"]) + method: z.enum(["wireguard", "newt", "local"]) }); type CreateSiteFormValues = z.infer; @@ -79,17 +79,16 @@ export default function CreateSiteForm({ const [isLoading, setIsLoading] = useState(false); const [isChecked, setIsChecked] = useState(false); - const router = useRouter(); - const [keypair, setKeypair] = useState<{ publicKey: string; privateKey: string; } | null>(null); + const [siteDefaults, setSiteDefaults] = useState(null); const handleCheckboxChange = (checked: boolean) => { - setChecked?.(checked); + // setChecked?.(checked); setIsChecked(checked); }; @@ -98,6 +97,17 @@ export default function CreateSiteForm({ defaultValues }); + const nameField = form.watch("name"); + const methodField = form.watch("method"); + + useEffect(() => { + const nameIsValid = nameField?.length >= 2 && nameField?.length <= 30; + const isFormValid = methodField === "local" || isChecked; + + // Only set checked to true if name is valid AND (method is local OR checkbox is checked) + setChecked?.(nameIsValid && isFormValid); + }, [nameField, methodField, isChecked, setChecked]); + useEffect(() => { if (!open) return; @@ -114,11 +124,8 @@ export default function CreateSiteForm({ api.get(`/org/${orgId}/pick-site-defaults`) .catch((e) => { - toast({ - variant: "destructive", - title: "Error picking site defaults", - description: formatAxiosError(e) - }); + // update the default value of the form to be local method + form.setValue("method", "local"); }) .then((res) => { if (res && res.status === 200) { @@ -130,24 +137,54 @@ export default function CreateSiteForm({ async function onSubmit(data: CreateSiteFormValues) { setLoading?.(true); setIsLoading(true); - if (!siteDefaults || !keypair) { - return; - } let payload: CreateSiteBody = { name: data.name, - subnet: siteDefaults.subnet, - exitNodeId: siteDefaults.exitNodeId, - pubKey: keypair.publicKey, type: data.method }; - if (data.method === "newt") { - payload.secret = siteDefaults.newtSecret; - payload.newtId = siteDefaults.newtId; + + if (data.method == "wireguard") { + if (!keypair || !siteDefaults) { + toast({ + variant: "destructive", + title: "Error creating site", + description: "Key pair or site defaults not found" + }); + setLoading?.(false); + setIsLoading(false); + return; + } + + payload = { + ...payload, + subnet: siteDefaults.subnet, + exitNodeId: siteDefaults.exitNodeId, + pubKey: keypair.publicKey + }; } + if (data.method === "newt") { + if (!siteDefaults) { + toast({ + variant: "destructive", + title: "Error creating site", + description: "Site defaults not found" + }); + setLoading?.(false); + setIsLoading(false); + return; + } + + payload = { + ...payload, + secret: siteDefaults.newtSecret, + newtId: siteDefaults.newtId + }; + } + const res = await api - .put< - AxiosResponse - >(`/org/${orgId}/site/`, payload) + .put>( + `/org/${orgId}/site/`, + payload + ) .catch((e) => { toast({ variant: "destructive", @@ -157,18 +194,14 @@ export default function CreateSiteForm({ }); if (res && res.status === 201) { - const niceId = res.data.data.niceId; - // navigate to the site page - // router.push(`/${orgId}/settings/sites/${niceId}`); - const data = res.data.data; onCreate?.({ name: data.name, id: data.siteId, nice: data.niceId.toString(), - mbIn: "0 MB", - mbOut: "0 MB", + mbIn: data.type == "wireguard" || data.type == "newt" ? "0 MB" : "--", + mbOut: data.type == "wireguard" || data.type == "newt" ? "0 MB" : "--", orgId: orgId as string, type: data.type as any, online: false @@ -245,12 +278,21 @@ PersistentKeepalive = 5` - - WireGuard + + Local - + Newt + + WireGuard + @@ -264,19 +306,30 @@ PersistentKeepalive = 5`
{form.watch("method") === "wireguard" && !isLoading ? ( - + <> + + + You will only be able to see the + configuration once. + + ) : form.watch("method") === "wireguard" && isLoading ? (

Loading WireGuard configuration...

- ) : ( - - )} + ) : form.watch("method") === "newt" ? ( + <> + + + You will only be able to see the + configuration once. + + + ) : null}
- - You will only be able to see the configuration once. - - {form.watch("method") === "newt" && ( <>
@@ -295,19 +348,32 @@ PersistentKeepalive = 5` )} -
- - -
+ {form.watch("method") === "local" && ( + <> +
+

+ Data will leave Traefik and go wherever you + want; no tunneling involved. +

+ + )} + + {(form.watch("method") === "newt" || + form.watch("method") === "wireguard") && ( +
+ + +
+ )} diff --git a/src/app/[orgId]/settings/sites/SitesTable.tsx b/src/app/[orgId]/settings/sites/SitesTable.tsx index f4361177..c06a10a3 100644 --- a/src/app/[orgId]/settings/sites/SitesTable.tsx +++ b/src/app/[orgId]/settings/sites/SitesTable.tsx @@ -245,6 +245,14 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { ); } + + if (originalRow.type === "local") { + return ( +
+ Local +
+ ); + } } }, { diff --git a/src/app/[orgId]/settings/sites/page.tsx b/src/app/[orgId]/settings/sites/page.tsx index c5d83ce2..f3fa4957 100644 --- a/src/app/[orgId]/settings/sites/page.tsx +++ b/src/app/[orgId]/settings/sites/page.tsx @@ -23,7 +23,10 @@ export default async function SitesPage(props: SitesPageProps) { sites = res.data.data.sites; } catch (e) {} - function formatSize(mb: number): string { + function formatSize(mb: number, type: string): string { + if (type === "local") { + return "--"; // because we are not able to track the data use in a local site right now + } if (mb >= 1024 * 1024) { return `${(mb / (1024 * 1024)).toFixed(2)} TB`; } else if (mb >= 1024) { @@ -38,8 +41,8 @@ export default async function SitesPage(props: SitesPageProps) { name: site.name, id: site.siteId, nice: site.niceId.toString(), - mbIn: formatSize(site.megabytesIn || 0), - mbOut: formatSize(site.megabytesOut || 0), + mbIn: formatSize(site.megabytesIn || 0, site.type), + mbOut: formatSize(site.megabytesOut || 0, site.type), orgId: params.orgId, type: site.type as any, online: site.online