From a9ebd8fb753dd185c909ccb984dbf58858db7b68 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Sun, 20 Sep 2026 12:51:12 +0100 Subject: [PATCH] Update hosts form --- db/schema.ts | 9 +++ db/types/db.ts | 2 + src/components/twui/elements/Toggle.tsx | 59 ++++++++------- .../backend/setup/get-interface-local-ip.ts | 25 +++++++ .../setup/grab-host-public-ip-address.ts | 11 ++- src/hooks/use-form-init.ts | 8 ++- .../add-host-form/add-host-form-interface.tsx | 26 ++----- .../add-host-form/add-host-form-public-ip.tsx | 71 ++++++++++++++----- .../add-host-form/add-host-form.tsx | 12 +++- src/pages/admin/hosts/add/index.server.ts | 15 ++++ src/pages/api/admin/interface-local-ip.ts | 22 ++++++ src/pages/api/admin/public-ip.ts | 4 +- src/types/index.ts | 1 + 13 files changed, 198 insertions(+), 67 deletions(-) create mode 100644 src/functions/backend/setup/get-interface-local-ip.ts create mode 100644 src/pages/admin/hosts/add/index.server.ts create mode 100644 src/pages/api/admin/interface-local-ip.ts diff --git a/db/schema.ts b/db/schema.ts index 1b1f2fe..c5ba6a3 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -218,6 +218,15 @@ const schema: BUN_SQLITE_DatabaseSchemaType = { fieldName: "user_id", dataType: "INTEGER", }, + { + fieldName: "name", + dataType: "TEXT", + defaultValue: "Main", + }, + { + fieldName: "short_description", + dataType: "TEXT", + }, { fieldName: "public_ip_address", dataType: "TEXT", diff --git a/db/types/db.ts b/db/types/db.ts index 4ee51a0..77818a6 100644 --- a/db/types/db.ts +++ b/db/types/db.ts @@ -131,6 +131,8 @@ export type BUN_SQLITE_WGUI_HOSTS = { */ updated_at?: number | ""; user_id?: number | ""; + name?: string; + short_description?: string; public_ip_address?: string; interface?: string; wg_ip_address?: string; diff --git a/src/components/twui/elements/Toggle.tsx b/src/components/twui/elements/Toggle.tsx index 0cf557e..796b089 100644 --- a/src/components/twui/elements/Toggle.tsx +++ b/src/components/twui/elements/Toggle.tsx @@ -1,10 +1,12 @@ import { type DetailedHTMLProps, type HTMLAttributes, + type ReactNode, useEffect, useState, } from "react"; import { twMerge } from "tailwind-merge"; +import Row from "../layout/Row"; export type TWUI_TOGGLE_PROPS = DetailedHTMLProps< HTMLAttributes, @@ -16,6 +18,7 @@ export type TWUI_TOGGLE_PROPS = DetailedHTMLProps< >; default_active?: boolean; changeHandler?: (checked: boolean) => void; + label?: string | ReactNode; }; /** @@ -27,6 +30,7 @@ export default function Toggle({ circleProps, default_active, changeHandler, + label, ...props }: TWUI_TOGGLE_PROPS) { const [active, setActive] = useState(default_active || false); @@ -44,34 +48,39 @@ export default function Toggle({ }, []); return ( -
{ setActive(!active); }} > - {typeof active == "undefined" ? ( -
- ) : ( -
- )} -
+
+ {typeof active == "undefined" ? ( +
+ ) : ( +
+ )} +
+ {label} + ); } diff --git a/src/functions/backend/setup/get-interface-local-ip.ts b/src/functions/backend/setup/get-interface-local-ip.ts new file mode 100644 index 0000000..a76282f --- /dev/null +++ b/src/functions/backend/setup/get-interface-local-ip.ts @@ -0,0 +1,25 @@ +import { execSync } from "node:child_process"; + +/** + * Get the local IPv4 address of a specific network interface + * @param interface_name Network interface name (e.g. "enp2s0") + * @returns Local IP address or empty string + */ +export default async function getInterfaceLocalIP( + interface_name: string | null, +): Promise { + if (!interface_name) return ``; + try { + const output = execSync( + `ip -4 addr show ${interface_name}`, + { encoding: "utf-8" }, + ).trim(); + const match = output.match( + /inet\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/\d+/, + ); + if (match && match[1]) { + return match[1]; + } + } catch (error) {} + return ``; +} diff --git a/src/functions/backend/setup/grab-host-public-ip-address.ts b/src/functions/backend/setup/grab-host-public-ip-address.ts index 8b1de5c..4888254 100644 --- a/src/functions/backend/setup/grab-host-public-ip-address.ts +++ b/src/functions/backend/setup/grab-host-public-ip-address.ts @@ -3,11 +3,16 @@ import { execSync } from "node:child_process"; /** * Function to grab the host's public * IP address - * @param param0 + * @param interface_name Optional network interface to use */ -export default async function grabHostPublicIPAddress() { +export default async function grabHostPublicIPAddress( + interface_name?: string | null, +) { try { - const public_ip = execSync(`curl -sS --max-time 15 https://api.ipify.org`, { + const curl_cmd = interface_name + ? `curl -sS --max-time 15 --interface ${interface_name} https://api.ipify.org` + : `curl -sS --max-time 15 https://api.ipify.org`; + const public_ip = execSync(curl_cmd, { encoding: "utf-8", }) .trim(); diff --git a/src/hooks/use-form-init.ts b/src/hooks/use-form-init.ts index 3facf9f..e4f72e7 100644 --- a/src/hooks/use-form-init.ts +++ b/src/hooks/use-form-init.ts @@ -13,6 +13,7 @@ import type { ImageInputToBase64FunctionReturn } from "../components/twui/utils/ import EJSON from "../utils/ejson"; import twuiSlugify from "../components/twui/utils/slugify"; import _ from "lodash"; +import type { AppContextObject } from "../types"; type Params = { /** @@ -46,6 +47,7 @@ type Params = { before_ready_function?: (params: { form: T; setForm: Dispatch>; + app_context: AppContextObject; }) => Promise; }; @@ -111,7 +113,11 @@ export default function useFormInit< } finally { if (params?.before_ready_function) { params - .before_ready_function({ form, setForm }) + .before_ready_function({ + form, + setForm, + app_context: appContext, + }) .then(() => {}) .catch((e2) => { console.log( diff --git a/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-interface.tsx b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-interface.tsx index 2e31136..d9131de 100644 --- a/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-interface.tsx +++ b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-interface.tsx @@ -2,29 +2,15 @@ import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db"; import Select from "@/src/components/twui/form/Select"; import Stack from "@/src/components/twui/layout/Stack"; import useFormInit from "@/src/hooks/use-form-init"; -import { useEffect, useState } from "react"; -import fetchApi from "@/src/components/twui/utils/fetch/fetchApi"; -import type { ApiReqParams } from "@/src/types"; -import type { APIResponseObject } from "@moduletrace/bunext/types"; type Props = ReturnType>; -export default function AddHostFormInterface({ form, setForm }: Props) { - const [interfaces, setInterfaces] = useState([]); - - useEffect(() => { - fetchApi( - "/api/admin/network-interfaces", - { method: "GET" }, - ).then((res) => { - if (res.success && res.stringRes) { - try { - const parsed = JSON.parse(res.stringRes) as string[]; - setInterfaces(parsed); - } catch {} - } - }); - }, []); +export default function AddHostFormInterface({ + form, + setForm, + app_context, +}: Props) { + const interfaces = app_context.pageProps?.network_interfaces || []; return ( diff --git a/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-public-ip.tsx b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-public-ip.tsx index 756a471..5ccc573 100644 --- a/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-public-ip.tsx +++ b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form-public-ip.tsx @@ -1,36 +1,66 @@ import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db"; import Input from "@/src/components/twui/form/Input"; import Stack from "@/src/components/twui/layout/Stack"; +import Row from "@/src/components/twui/layout/Row"; +import Span from "@/src/components/twui/layout/Span"; +import Toggle from "@/src/components/twui/elements/Toggle"; import useFormInit from "@/src/hooks/use-form-init"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import fetchApi from "@/src/components/twui/utils/fetch/fetchApi"; import type { ApiReqParams } from "@/src/types"; import type { APIResponseObject } from "@moduletrace/bunext/types"; +import Checkbox from "@/src/components/twui/form/Checkbox"; +import { twMerge } from "tailwind-merge"; +import useStatus from "@/src/components/twui/hooks/useStatus"; +import LoadingOverlay from "@/src/components/twui/elements/LoadingOverlay"; type Props = ReturnType>; export default function AddHostFormPublicIP({ form, setForm }: Props) { + const [useLocalIP, setUseLocalIP] = useState(false); + + const { loading, setLoading } = useStatus({ initialLoading: true }); + + function fetchIP(iface: string, local: boolean) { + if (!iface) return; + + const endpoint = local + ? `/api/admin/interface-local-ip?interface=${iface}` + : `/api/admin/public-ip?interface=${iface}`; + + setLoading(true); + + fetchApi(endpoint, { + method: "GET", + }) + .then((res) => { + if (res.success && res.stringRes) { + setForm((prev) => ({ + ...prev, + public_ip_address: res.stringRes!, + })); + } + }) + .finally(() => { + setTimeout(() => { + setLoading(false); + }, 1000); + }); + } + useEffect(() => { - fetchApi( - "/api/admin/public-ip", - { method: "GET" }, - ).then((res) => { - if (res.success && res.stringRes) { - setForm((prev) => ({ - ...prev, - public_ip_address: res.stringRes!, - })); - } - }); - }, []); + if (!form.interface) return; + fetchIP(form.interface, useLocalIP); + }, [form.interface, useLocalIP]); return ( - + + {loading ? : null} { setForm((prev) => ({ @@ -39,6 +69,15 @@ export default function AddHostFormPublicIP({ form, setForm }: Props) { })); }} /> + { + setUseLocalIP(checked); + }} + label={ + Use Local IP? + } + defaultChecked={useLocalIP} + /> ); } diff --git a/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form.tsx b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form.tsx index d1da526..57a11a7 100644 --- a/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form.tsx +++ b/src/pages/admin/hosts/add/(partials)/add-host-form/add-host-form.tsx @@ -21,6 +21,16 @@ export default function AddHostForm() { wg_ip_address: AppData["DefaultPrivateIP"], }, title: "Add Host", + async before_ready_function(params) { + if (!params.form.interface) { + params.setForm((prev) => ({ + ...prev, + interface: + params.app_context.pageProps?.network_interfaces?.[0] || + undefined, + })); + } + }, }); const [ipStatus, setIpStatus] = useState("checking"); @@ -39,8 +49,8 @@ export default function AddHostForm() { {init.ready ? ( {/* */} - + = async ({ req }) => { + const network_interfaces = await grabHostNetworkInterfaces(); + + return { + props: { + network_interfaces, + }, + }; +}; + +export default server; diff --git a/src/pages/api/admin/interface-local-ip.ts b/src/pages/api/admin/interface-local-ip.ts new file mode 100644 index 0000000..e98f1f1 --- /dev/null +++ b/src/pages/api/admin/interface-local-ip.ts @@ -0,0 +1,22 @@ +import getInterfaceLocalIP from "@/src/functions/backend/setup/get-interface-local-ip"; +import type { ApiReqParams } from "@/src/types"; +import type { + APIResponseObject, + BunextAPIRouteHandler, +} from "@moduletrace/bunext/types"; + +export const handler: BunextAPIRouteHandler = async ( + params, +) => { + if (params.req.method !== "GET") { + return { success: false }; + } + const url = new URL(params.req.url || ""); + const iface = url.searchParams.get("interface") || undefined; + try { + const local_ip = await getInterfaceLocalIP(iface || null); + return { success: true, stringRes: local_ip }; + } catch (error: any) { + return { success: false, msg: error.message }; + } +}; diff --git a/src/pages/api/admin/public-ip.ts b/src/pages/api/admin/public-ip.ts index 837b448..edb4605 100644 --- a/src/pages/api/admin/public-ip.ts +++ b/src/pages/api/admin/public-ip.ts @@ -11,8 +11,10 @@ export const handler: BunextAPIRouteHandler = async ( if (params.req.method !== "GET") { return { success: false }; } + const url = new URL(params.req.url || ""); + const iface = url.searchParams.get("interface") || undefined; try { - const public_ip = await grabHostPublicIPAddress(); + const public_ip = await grabHostPublicIPAddress(iface); return { success: true, stringRes: public_ip }; } catch (error: any) { return { success: false, msg: error.message }; diff --git a/src/types/index.ts b/src/types/index.ts index 5d63f2b..7574a4c 100755 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -65,6 +65,7 @@ export type PagePropsType = { }; url?: BunextPageModuleServerReturnURLObject; main_host_dir_names?: ReturnType | null; + network_interfaces?: string[] | null; }; export type AppContextObject = {