Update hosts form

This commit is contained in:
2026-09-20 12:51:12 +01:00
parent 8b40a876a1
commit a9ebd8fb75
13 changed files with 198 additions and 67 deletions
+9
View File
@@ -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",
+2
View File
@@ -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;
+34 -25
View File
@@ -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<HTMLDivElement>,
@@ -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 (
<div
{...props}
className={twMerge(
"flex flex-row items-center w-[40px] p-[3px] transition-all",
"border border-slate-300 dark:border-white/30 border-solid rounded-full",
active ? "justify-end" : "justify-start",
"twui-toggle-wrapper",
props.className,
)}
<Row
className={twMerge("cursor-pointer")}
onClick={() => {
setActive(!active);
}}
>
{typeof active == "undefined" ? (
<div className="w-3.5 h-3.5 twui-toggle-circle"></div>
) : (
<div
{...circleProps}
className={twMerge(
"w-3.5 h-3.5 rounded-full ",
active
? "bg-blue-600 dark:bg-blue-500"
: "bg-slate-300 dark:bg-white/40",
"twui-toggle-circle",
circleProps?.className,
)}
></div>
)}
</div>
<div
{...props}
className={twMerge(
"flex flex-row items-center w-[40px] p-[3px] transition-all",
"border border-slate-300 dark:border-white/30 border-solid rounded-full",
active ? "justify-end" : "justify-start",
"twui-toggle-wrapper",
props.className,
)}
>
{typeof active == "undefined" ? (
<div className="w-3.5 h-3.5 twui-toggle-circle"></div>
) : (
<div
{...circleProps}
className={twMerge(
"w-3.5 h-3.5 rounded-full ",
active
? "bg-blue-600 dark:bg-blue-500"
: "bg-slate-300 dark:bg-white/40",
"twui-toggle-circle",
circleProps?.className,
)}
></div>
)}
</div>
{label}
</Row>
);
}
@@ -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<string> {
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 ``;
}
@@ -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();
+7 -1
View File
@@ -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<T extends {} = BUN_SQLITE_WGUI_ALL_TYPEDEFS> = {
/**
@@ -46,6 +47,7 @@ type Params<T extends {} = BUN_SQLITE_WGUI_ALL_TYPEDEFS> = {
before_ready_function?: (params: {
form: T;
setForm: Dispatch<SetStateAction<T>>;
app_context: AppContextObject;
}) => Promise<void>;
};
@@ -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(
@@ -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<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>>;
export default function AddHostFormInterface({ form, setForm }: Props) {
const [interfaces, setInterfaces] = useState<string[]>([]);
useEffect(() => {
fetchApi<ApiReqParams, APIResponseObject>(
"/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 (
<Stack className="w-full gap-2 items-stretch">
@@ -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<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>>;
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<ApiReqParams, APIResponseObject>(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<ApiReqParams, APIResponseObject>(
"/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 (
<Stack className="w-full gap-2">
<Stack className={twMerge("w-full gap-2 relative")}>
{loading ? <LoadingOverlay /> : null}
<Input
name="public_ip_address"
label="Public IP Address"
label="IP Address"
showLabel
placeholder="e.g. 203.0.113.10"
placeholder="e.g. 203.0.113.10 or 192.168.1.10"
value={form.public_ip_address || ""}
changeHandler={(v) => {
setForm((prev) => ({
@@ -39,6 +69,15 @@ export default function AddHostFormPublicIP({ form, setForm }: Props) {
}));
}}
/>
<Checkbox
changeHandler={(checked) => {
setUseLocalIP(checked);
}}
label={
<span className="text-base opacity-80">Use Local IP?</span>
}
defaultChecked={useLocalIP}
/>
</Stack>
);
}
@@ -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<HostWgIpFieldStatus>("checking");
@@ -39,8 +49,8 @@ export default function AddHostForm() {
{init.ready ? (
<Stack className="w-full gap-6 items-stretch">
{/* <AddHostFormSubnet /> */}
<AddHostFormPublicIP {...init} />
<AddHostFormInterface {...init} />
<AddHostFormPublicIP {...init} />
<AddHostFormWgIpAddress
{...init}
setIpStatus={setIpStatus}
+15
View File
@@ -0,0 +1,15 @@
import grabHostNetworkInterfaces from "@/src/functions/backend/setup/grab-host-network-interfaces";
import type { PagePropsType } from "@/src/types";
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
const server: BunextPageServerFn<PagePropsType> = async ({ req }) => {
const network_interfaces = await grabHostNetworkInterfaces();
return {
props: {
network_interfaces,
},
};
};
export default server;
+22
View File
@@ -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<APIResponseObject> = 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 };
}
};
+3 -1
View File
@@ -11,8 +11,10 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = 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 };
+1
View File
@@ -65,6 +65,7 @@ export type PagePropsType = {
};
url?: BunextPageModuleServerReturnURLObject;
main_host_dir_names?: ReturnType<typeof grabHostDirnames> | null;
network_interfaces?: string[] | null;
};
export type AppContextObject = {