Update hosts form

This commit is contained in:
2026-09-20 13:48:35 +01:00
parent a9ebd8fb75
commit 3c1d2b61f6
20 changed files with 308 additions and 66 deletions
+6
View File
@@ -1,3 +1,4 @@
import { AppData } from "@/src/data/app-data";
import { import {
ClientRuleProtocols, ClientRuleProtocols,
ClientRuleTypes, ClientRuleTypes,
@@ -231,6 +232,11 @@ const schema: BUN_SQLITE_DatabaseSchemaType = {
fieldName: "public_ip_address", fieldName: "public_ip_address",
dataType: "TEXT", dataType: "TEXT",
}, },
{
fieldName: "listen_port",
dataType: "INTEGER",
defaultValue: AppData["DefaultWGListenPort"],
},
{ {
fieldName: "interface", fieldName: "interface",
dataType: "TEXT", dataType: "TEXT",
+1
View File
@@ -134,6 +134,7 @@ export type BUN_SQLITE_WGUI_HOSTS = {
name?: string; name?: string;
short_description?: string; short_description?: string;
public_ip_address?: string; public_ip_address?: string;
listen_port?: number | "";
interface?: string; interface?: string;
wg_ip_address?: string; wg_ip_address?: string;
public_key?: string; public_key?: string;
@@ -5,17 +5,12 @@ import fetchApi from "../twui/utils/fetch/fetchApi";
import type { ApiReqParams } from "@/src/types"; import type { ApiReqParams } from "@/src/types";
import Row from "../twui/layout/Row"; import Row from "../twui/layout/Row";
import Stack from "../twui/layout/Stack"; import Stack from "../twui/layout/Stack";
import { import { CircleCheck, TriangleAlert } from "lucide-react";
CircleCheck,
TriangleAlert,
} from "lucide-react";
import Span from "../twui/layout/Span"; import Span from "../twui/layout/Span";
import Tag from "../twui/elements/Tag"; import Tag from "../twui/elements/Tag";
import { AppData } from "@/src/data/app-data"; import { AppData } from "@/src/data/app-data";
import type { APIResponseObject } from "@moduletrace/bunext/types"; import type { APIResponseObject } from "@moduletrace/bunext/types";
import HostWgIpField, { import HostWgIpField, { type HostWgIpFieldStatus } from "./host-wg-ip-field";
type HostWgIpFieldStatus,
} from "./host-wg-ip-field";
import GrabNextSubnetButton from "./grab-next-subnet-button"; import GrabNextSubnetButton from "./grab-next-subnet-button";
type Props = { type Props = {
@@ -30,12 +25,12 @@ export default function SetupMainHostButton({ button_props }: Props) {
return ( return (
<Stack className="w-full items-stretch gap-3"> <Stack className="w-full items-stretch gap-3">
<HostWgIpField {/* <HostWgIpField
value={wgIP} value={wgIP}
onChange={setWgIP} onChange={setWgIP}
onStatus={setIpStatus} onStatus={setIpStatus}
autoFocus autoFocus
/> /> */}
{status?.error && status.msg ? ( {status?.error && status.msg ? (
<Tag <Tag
@@ -1,4 +1,7 @@
import type { BUN_SQLITE_WGUI_HOSTS, BUN_SQLITE_WGUI_VARIABLES } from "@/db/types/db"; import type {
BUN_SQLITE_WGUI_HOSTS,
BUN_SQLITE_WGUI_VARIABLES,
} from "@/db/types/db";
import BunSQLite from "@moduletrace/bun-sqlite"; import BunSQLite from "@moduletrace/bun-sqlite";
import type { APIResponseObject } from "@moduletrace/bunext/types"; import type { APIResponseObject } from "@moduletrace/bunext/types";
import type { TableType, User } from "@/src/types"; import type { TableType, User } from "@/src/types";
@@ -9,7 +12,11 @@ type Params = {
wg_ip_address?: string | null; wg_ip_address?: string | null;
public_ip_address?: string | null; public_ip_address?: string | null;
interface?: string | null; interface?: string | null;
name?: string | null;
short_description?: string | null;
listen_port?: number | null;
user: User; user: User;
is_main_host?: boolean;
}; };
/** /**
@@ -24,7 +31,11 @@ export default async function createWireguardHost({
wg_ip_address, wg_ip_address,
public_ip_address, public_ip_address,
interface: iface, interface: iface,
name,
short_description,
listen_port,
user, user,
is_main_host,
}: Params): Promise<APIResponseObject> { }: Params): Promise<APIResponseObject> {
const wg_ip = (wg_ip_address || "").trim(); const wg_ip = (wg_ip_address || "").trim();
@@ -38,24 +49,34 @@ export default async function createWireguardHost({
const detected_public_ip = await grabHostPublicIPAddress(); const detected_public_ip = await grabHostPublicIPAddress();
const final_public_ip = public_ip_address?.trim() || detected_public_ip; const final_public_ip = public_ip_address?.trim() || detected_public_ip;
const final_interface = iface?.trim() || undefined; const final_interface = iface?.trim() || undefined;
const final_name = name?.trim() || undefined;
const final_short_description = short_description?.trim() || undefined;
try { try {
const insert_data: BUN_SQLITE_WGUI_HOSTS = {
user_id: user.id,
wg_ip_address: wg_ip,
public_ip_address: final_public_ip || undefined,
interface: final_interface,
name: final_name,
short_description: final_short_description,
listen_port: listen_port != null ? listen_port : undefined,
};
if (is_main_host) {
insert_data.id = 0;
}
const insert_host = await BunSQLite.insert< const insert_host = await BunSQLite.insert<
BUN_SQLITE_WGUI_HOSTS, BUN_SQLITE_WGUI_HOSTS,
TableType TableType
>({ >({
table: "hosts", table: "hosts",
data: [ data: [insert_data],
{ update_on_duplicate: true,
user_id: user.id,
wg_ip_address: wg_ip,
public_ip_address: final_public_ip || undefined,
interface: final_interface,
},
],
}); });
if (!insert_host.success) { if (!insert_host.success || !insert_host.postInsertReturn?.insertId) {
return { return {
success: false, success: false,
msg: `Could not create the host record: ${insert_host.msg}`, msg: `Could not create the host record: ${insert_host.msg}`,
@@ -70,13 +91,13 @@ export default async function createWireguardHost({
wg_ip_address: wg_ip, wg_ip_address: wg_ip,
public_ip_address: final_public_ip || undefined, public_ip_address: final_public_ip || undefined,
interface: final_interface, interface: final_interface,
name: final_name,
short_description: final_short_description,
listen_port: listen_port != null ? listen_port : undefined,
}; };
if (host_id === 0 && final_interface) { if (host_id === 0 && final_interface) {
await BunSQLite.insert< await BunSQLite.insert<BUN_SQLITE_WGUI_VARIABLES, TableType>({
BUN_SQLITE_WGUI_VARIABLES,
TableType
>({
table: "variables", table: "variables",
data: [ data: [
{ {
@@ -0,0 +1,40 @@
import { exec } from "node:child_process";
import { promisify } from "node:util";
import { AppData } from "@/src/data/app-data";
const execAsync = promisify(exec);
/**
* Function to get the next available listen port on the system.
* Checks both UDP and TCP listening ports.
*/
export default async function getNextAvailableListenPort(): Promise<number> {
const defaultPort = AppData["DefaultWGListenPort"] ?? 51820;
try {
const { stdout } = await execAsync("ss -tuln");
const usedPorts = new Set<number>();
for (const line of stdout.split("\n")) {
const match = line.match(/[:](\d+)\s/);
if (match && match[1]) {
const port = Number.parseInt(match[1], 10);
if (!Number.isNaN(port)) {
usedPorts.add(port);
}
}
}
let port = defaultPort;
while (usedPorts.has(port)) {
port++;
if (port > 65535) {
return defaultPort;
}
}
return port;
} catch (error) {
return defaultPort;
}
}
@@ -14,13 +14,8 @@ import BunSQLite from "@moduletrace/bun-sqlite";
import type { TableType, User } from "@/src/types"; import type { TableType, User } from "@/src/types";
import grabClientDirnames from "./grab-client-dir-names"; import grabClientDirnames from "./grab-client-dir-names";
const { const { WIREGUARD_PUBLIC_KEY_FILE_NAME, WIREGUARD_CLIENT_CONFIG_FILE_NAME } =
WGUI_LIB_HOSTS_CONFIGS_DIR, grabDirNames();
WIREGUARD_PRIVATE_KEY_FILE_NAME,
WIREGUARD_PUBLIC_KEY_FILE_NAME,
WGUI_LIB_HOST_CLIENTS_DIR_NAME,
WIREGUARD_CLIENT_CONFIG_FILE_NAME,
} = grabDirNames();
type Params = { type Params = {
client: BUN_SQLITE_WGUI_CLIENTS; client: BUN_SQLITE_WGUI_CLIENTS;
@@ -135,7 +130,7 @@ export default async function setupWireguardClient({
sh += `\n`; sh += `\n`;
sh += `[Peer]\n`; sh += `[Peer]\n`;
sh += `PublicKey = ${HOST_PUBLIC_KEY}\n`; sh += `PublicKey = ${HOST_PUBLIC_KEY}\n`;
sh += `Endpoint = ${PUBLIC_IP_ADDRESS}:51820\n`; sh += `Endpoint = ${PUBLIC_IP_ADDRESS}:${host?.listen_port || AppData["DefaultWGListenPort"]}\n`;
if (client?.allow_all_ips == 1) { if (client?.allow_all_ips == 1) {
sh += `AllowedIPs = 0.0.0.0/0, ::/0\n`; sh += `AllowedIPs = 0.0.0.0/0, ::/0\n`;
@@ -15,6 +15,7 @@ import manageWireguardHost from "./manage-wireguard-host";
import grabHostDirnames from "./grab-host-dir-names"; import grabHostDirnames from "./grab-host-dir-names";
import grabClientDirnames from "./grab-client-dir-names"; import grabClientDirnames from "./grab-client-dir-names";
import buildHostIptablesScripts from "./build-host-iptables-scripts"; import buildHostIptablesScripts from "./build-host-iptables-scripts";
import { AppData } from "@/src/data/app-data";
const { WIREGUARD_PRIVATE_KEY_FILE_NAME, WIREGUARD_PUBLIC_KEY_FILE_NAME } = const { WIREGUARD_PRIVATE_KEY_FILE_NAME, WIREGUARD_PUBLIC_KEY_FILE_NAME } =
grabDirNames(); grabDirNames();
@@ -100,7 +101,8 @@ export default async function setupWireguardHost({
rules_by_client_id.set(rule.client_id, existing_rules); rules_by_client_id.set(rule.client_id, existing_rules);
} }
const TARGET_INTERFACE = host?.interface || await grabHostNetworkInterface(); const TARGET_INTERFACE =
host?.interface || (await grabHostNetworkInterface());
const HOST_WG_IP = const HOST_WG_IP =
host?.wg_ip_address || host?.wg_ip_address ||
variables?.find((v) => v.key == "main_host_wg_ip_address")?.value || variables?.find((v) => v.key == "main_host_wg_ip_address")?.value ||
@@ -246,7 +248,7 @@ export default async function setupWireguardHost({
sh += `cat > ${INTERFACE_NAME}.conf << EOF\n`; sh += `cat > ${INTERFACE_NAME}.conf << EOF\n`;
sh += `[Interface]\n`; sh += `[Interface]\n`;
sh += `Address = ${HOST_WG_IP}/24\n`; sh += `Address = ${HOST_WG_IP}/24\n`;
sh += `ListenPort = 51820\n`; sh += `ListenPort = ${host?.listen_port || AppData["DefaultWGListenPort"]}\n`;
sh += `PrivateKey = ${HOST_PRIVATE_KEY}\n`; sh += `PrivateKey = ${HOST_PRIVATE_KEY}\n`;
sh += `PostUp = ${POST_UP_PATH}\n`; sh += `PostUp = ${POST_UP_PATH}\n`;
sh += `PostDown = ${POST_DOWN_PATH}\n`; sh += `PostDown = ${POST_DOWN_PATH}\n`;
@@ -284,6 +286,13 @@ export default async function setupWireguardHost({
const exec = execSync(sh, { encoding: "utf-8" }); const exec = execSync(sh, { encoding: "utf-8" });
if (!is_update_after_client_setup) {
execSync(
`ip link set ${INTERFACE_NAME} down 2>/dev/null; ip link del ${INTERFACE_NAME} 2>/dev/null; true`,
{ encoding: "utf-8" },
);
}
const manage_res = manageWireguardHost({ const manage_res = manageWireguardHost({
host_id: HOST_ID, host_id: HOST_ID,
action: "restart", action: "restart",
@@ -86,13 +86,13 @@ export default function EditHostFormSection() {
</P> </P>
</Stack> </Stack>
<HostWgIpField {/* <HostWgIpField
value={wgIP} value={wgIP}
onChange={setWgIP} onChange={setWgIP}
onStatus={setIpStatus} onStatus={setIpStatus}
current_value={host?.wg_ip_address || ""} current_value={host?.wg_ip_address || ""}
autoFocus autoFocus
/> /> */}
{status?.error && status.msg ? ( {status?.error && status.msg ? (
<Tag <Tag
@@ -24,6 +24,9 @@ export default async function submitAddHostForm({
wg_ip_address: form.wg_ip_address || "", wg_ip_address: form.wg_ip_address || "",
public_ip_address: form.public_ip_address || "", public_ip_address: form.public_ip_address || "",
interface: form.interface || "", interface: form.interface || "",
name: form.name || "",
short_description: form.short_description || "",
listen_port: form.listen_port != null ? Number(form.listen_port) : null,
}, },
}, },
); );
@@ -0,0 +1,26 @@
import Stack from "@/src/components/twui/layout/Stack";
import useFormInit from "@/src/hooks/use-form-init";
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
import Textarea from "@/src/components/twui/form/Textarea";
type Props = ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>>;
export default function AddHostFormDescription({ form, setForm }: Props) {
return (
<Stack className="w-full gap-2">
<Textarea
name="short_description"
label="Short Description"
showLabel
placeholder="e.g. Main server at home"
defaultValue={form.short_description || ""}
changeHandler={(v) => {
setForm((prev) => ({
...prev,
short_description: v,
}));
}}
/>
</Stack>
);
}
@@ -0,0 +1,70 @@
import { useState } from "react";
import Input from "@/src/components/twui/form/Input";
import Button from "@/src/components/twui/layout/Button";
import Stack from "@/src/components/twui/layout/Stack";
import Row from "@/src/components/twui/layout/Row";
import useFormInit from "@/src/hooks/use-form-init";
import { AppData } from "@/src/data/app-data";
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
import type { ApiReqParams } from "@/src/types";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
type Props = ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>>;
export default function AddHostFormListenPort({ form, setForm }: Props) {
const [busy, setBusy] = useState(false);
async function handleGrabNext() {
setBusy(true);
try {
const res = await fetchApi<ApiReqParams, APIResponseObject>(
"/api/admin/next-listen-port",
{ method: "GET" },
);
if (res.success && res.numberRes) {
setForm((prev) => ({
...prev,
listen_port: Number(res.numberRes),
}));
}
} catch {
} finally {
setBusy(false);
}
}
return (
<Stack className="w-full gap-2">
<Row className="w-full items-stretch gap-3 md:flex-nowrap">
<Input
name="listen_port"
label="Listen Port"
showLabel
placeholder={String(AppData["DefaultWGListenPort"])}
value={
form.listen_port != null ? String(form.listen_port) : ""
}
numberText
rawNumber
changeHandler={(v) => {
setForm((prev) => ({
...prev,
listen_port: Number(v) || undefined,
}));
}}
/>
<Button
title="Grab Next Port"
size="small"
variant="outlined"
disabled={busy}
loading={busy}
onClick={handleGrabNext}
>
{busy ? "Fetching…" : "Grab Next"}
</Button>
</Row>
</Stack>
);
}
@@ -0,0 +1,26 @@
import Input from "@/src/components/twui/form/Input";
import Stack from "@/src/components/twui/layout/Stack";
import useFormInit from "@/src/hooks/use-form-init";
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
type Props = ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>>;
export default function AddHostFormName({ form, setForm }: Props) {
return (
<Stack className="w-full gap-2">
<Input
name="name"
label="Host Name"
showLabel
placeholder="e.g. my-server"
defaultValue={form.name || ""}
changeHandler={(v) => {
setForm((prev) => ({
...prev,
name: v,
}));
}}
/>
</Stack>
);
}
@@ -12,6 +12,9 @@ import AddHostFormSubnet from "./add-host-form-subnet";
import AddHostFormWgIpAddress from "./add-host-form-wg-ip-address"; import AddHostFormWgIpAddress from "./add-host-form-wg-ip-address";
import AddHostFormPublicIP from "./add-host-form-public-ip"; import AddHostFormPublicIP from "./add-host-form-public-ip";
import AddHostFormInterface from "./add-host-form-interface"; import AddHostFormInterface from "./add-host-form-interface";
import AddHostFormName from "./add-host-form-name";
import AddHostFormDescription from "./add-host-form-description";
import AddHostFormListenPort from "./add-host-form-listen-port";
import LoadingRectangleBlock from "@/src/components/twui/layout/LoadingRectangleBlock"; import LoadingRectangleBlock from "@/src/components/twui/layout/LoadingRectangleBlock";
import type { HostWgIpFieldStatus } from "@/src/components/general/host-wg-ip-field"; import type { HostWgIpFieldStatus } from "@/src/components/general/host-wg-ip-field";
@@ -19,17 +22,21 @@ export default function AddHostForm() {
const init = useFormInit<BUN_SQLITE_WGUI_HOSTS>({ const init = useFormInit<BUN_SQLITE_WGUI_HOSTS>({
default: { default: {
wg_ip_address: AppData["DefaultPrivateIP"], wg_ip_address: AppData["DefaultPrivateIP"],
name: "Main",
}, },
title: "Add Host", title: "new_host",
async before_ready_function(params) { async before_ready_function(params) {
if (!params.form.interface) { params.setForm((prev) => ({
params.setForm((prev) => ({ ...prev,
...prev, interface:
interface: params.form.interface ||
params.app_context.pageProps?.network_interfaces?.[0] || params.app_context.pageProps?.network_interfaces?.[0] ||
undefined, undefined,
})); listen_port:
} params.form.listen_port ||
params.app_context.pageProps?.next_listen_port ||
undefined,
}));
}, },
}); });
@@ -49,7 +56,10 @@ export default function AddHostForm() {
{init.ready ? ( {init.ready ? (
<Stack className="w-full gap-6 items-stretch"> <Stack className="w-full gap-6 items-stretch">
{/* <AddHostFormSubnet /> */} {/* <AddHostFormSubnet /> */}
<AddHostFormName {...init} />
<AddHostFormDescription {...init} />
<AddHostFormInterface {...init} /> <AddHostFormInterface {...init} />
<AddHostFormListenPort {...init} />
<AddHostFormPublicIP {...init} /> <AddHostFormPublicIP {...init} />
<AddHostFormWgIpAddress <AddHostFormWgIpAddress
{...init} {...init}
@@ -1,10 +1,9 @@
import AdminCard from "@/src/components/general/admin-card";
import AddHostForm from "../(partials)/add-host-form/add-host-form"; import AddHostForm from "../(partials)/add-host-form/add-host-form";
export default function AddHostFormSection() { export default function AddHostFormSection() {
return ( return (
<AdminCard className="w-full p-5 flex flex-col gap-4"> <>
<AddHostForm /> <AddHostForm />
</AdminCard> </>
); );
} }
@@ -1,13 +1,16 @@
import getNextAvailableListenPort from "@/src/functions/backend/setup/get-next-available-listen-port";
import grabHostNetworkInterfaces from "@/src/functions/backend/setup/grab-host-network-interfaces"; import grabHostNetworkInterfaces from "@/src/functions/backend/setup/grab-host-network-interfaces";
import type { PagePropsType } from "@/src/types"; import type { PagePropsType } from "@/src/types";
import type { BunextPageServerFn } from "@moduletrace/bunext/types"; import type { BunextPageServerFn } from "@moduletrace/bunext/types";
const server: BunextPageServerFn<PagePropsType> = async ({ req }) => { const server: BunextPageServerFn<PagePropsType> = async ({ req }) => {
const network_interfaces = await grabHostNetworkInterfaces(); const network_interfaces = await grabHostNetworkInterfaces();
const next_listen_port = await getNextAvailableListenPort();
return { return {
props: { props: {
network_interfaces, network_interfaces,
next_listen_port,
}, },
}; };
}; };
+20 -12
View File
@@ -51,6 +51,14 @@ export default function AdminHostPage() {
description="WireGuard server configuration and interface details" description="WireGuard server configuration and interface details"
buttons={ buttons={
<> <>
<Button
title="View Hosts"
variant="outlined"
href="/admin/hosts"
color="gray"
>
View Hosts
</Button>
<Button <Button
title="Add New Host" title="Add New Host"
variant="outlined" variant="outlined"
@@ -107,18 +115,18 @@ export default function AdminHostPage() {
{other_hosts[0] ? ( {other_hosts[0] ? (
<Stack className="w-full gap-4 items-stretch"> <Stack className="w-full gap-4 items-stretch">
{other_hosts.map((host) => ( {other_hosts.map((host) => (
<OtherHostCard <OtherHostCard
key={host.id} key={host.id}
host={host} host={host}
client_count={ client_count={
clients?.filter( clients?.filter(
(client) => (client) =>
(client.host_id || 0) == (client.host_id || 0) ==
(host.id || 0), (host.id || 0),
).length || 0 ).length || 0
} }
/> />
))} ))}
</Stack> </Stack>
) : ( ) : (
<EmptyContent title="No other hosts found" /> <EmptyContent title="No other hosts found" />
+3
View File
@@ -43,6 +43,9 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
wg_ip_address: body?.wg_ip_address, wg_ip_address: body?.wg_ip_address,
public_ip_address: body?.public_ip_address, public_ip_address: body?.public_ip_address,
interface: body?.interface, interface: body?.interface,
name: body?.name,
short_description: body?.short_description,
listen_port: body?.listen_port,
user, user,
}); });
} catch (error: any) { } catch (error: any) {
+20
View File
@@ -0,0 +1,20 @@
import getNextAvailableListenPort from "@/src/functions/backend/setup/get-next-available-listen-port";
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 };
}
try {
const port = await getNextAvailableListenPort();
return { success: true, numberRes: port };
} catch (error: any) {
return { success: false, msg: error.message };
}
};
+7 -4
View File
@@ -66,11 +66,12 @@ bring_up() {
} }
bring_down() { bring_down() {
if ! interface_is_up; then # If forced or interface exists, run wg-quick down to clean up routes/IPs safely
if interface_is_up || [ "${FORCE_DOWN:-0}" -eq 1 ]; then
"$WG_QUICK_BIN" down "$CONFIG_PATH" 2>/dev/null || true
else
echo "interface $INTERFACE is not up — nothing to do." echo "interface $INTERFACE is not up — nothing to do."
return 0
fi fi
"$WG_QUICK_BIN" down "$CONFIG_PATH"
} }
case "$ACTION" in case "$ACTION" in
@@ -81,7 +82,9 @@ case "$ACTION" in
bring_down bring_down
;; ;;
restart) restart)
bring_down # Force wg-quick down to clean up stuck IPs/routes even if link is missing
FORCE_DOWN=1 bring_down
bring_up bring_up
;; ;;
esac esac
+4
View File
@@ -66,6 +66,7 @@ export type PagePropsType = {
url?: BunextPageModuleServerReturnURLObject; url?: BunextPageModuleServerReturnURLObject;
main_host_dir_names?: ReturnType<typeof grabHostDirnames> | null; main_host_dir_names?: ReturnType<typeof grabHostDirnames> | null;
network_interfaces?: string[] | null; network_interfaces?: string[] | null;
next_listen_port?: number | null;
}; };
export type AppContextObject = { export type AppContextObject = {
@@ -224,6 +225,9 @@ export type ApiReqParams<
client_id?: string | number | null; client_id?: string | number | null;
public_ip_address?: string | null; public_ip_address?: string | null;
interface?: string | null; interface?: string | null;
name?: string | null;
short_description?: string | null;
listen_port?: number | null;
// client_rules?: BUN_SQLITE_WGUI_CLIENT_RULES[]; // client_rules?: BUN_SQLITE_WGUI_CLIENT_RULES[];
media_base_64?: string; media_base_64?: string;