Update hosts form
This commit is contained in:
@@ -218,6 +218,15 @@ const schema: BUN_SQLITE_DatabaseSchemaType = {
|
|||||||
fieldName: "user_id",
|
fieldName: "user_id",
|
||||||
dataType: "INTEGER",
|
dataType: "INTEGER",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldName: "name",
|
||||||
|
dataType: "TEXT",
|
||||||
|
defaultValue: "Main",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: "short_description",
|
||||||
|
dataType: "TEXT",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fieldName: "public_ip_address",
|
fieldName: "public_ip_address",
|
||||||
dataType: "TEXT",
|
dataType: "TEXT",
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ export type BUN_SQLITE_WGUI_HOSTS = {
|
|||||||
*/
|
*/
|
||||||
updated_at?: number | "";
|
updated_at?: number | "";
|
||||||
user_id?: number | "";
|
user_id?: number | "";
|
||||||
|
name?: string;
|
||||||
|
short_description?: string;
|
||||||
public_ip_address?: string;
|
public_ip_address?: string;
|
||||||
interface?: string;
|
interface?: string;
|
||||||
wg_ip_address?: string;
|
wg_ip_address?: string;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
type DetailedHTMLProps,
|
type DetailedHTMLProps,
|
||||||
type HTMLAttributes,
|
type HTMLAttributes,
|
||||||
|
type ReactNode,
|
||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import Row from "../layout/Row";
|
||||||
|
|
||||||
export type TWUI_TOGGLE_PROPS = DetailedHTMLProps<
|
export type TWUI_TOGGLE_PROPS = DetailedHTMLProps<
|
||||||
HTMLAttributes<HTMLDivElement>,
|
HTMLAttributes<HTMLDivElement>,
|
||||||
@@ -16,6 +18,7 @@ export type TWUI_TOGGLE_PROPS = DetailedHTMLProps<
|
|||||||
>;
|
>;
|
||||||
default_active?: boolean;
|
default_active?: boolean;
|
||||||
changeHandler?: (checked: boolean) => void;
|
changeHandler?: (checked: boolean) => void;
|
||||||
|
label?: string | ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,6 +30,7 @@ export default function Toggle({
|
|||||||
circleProps,
|
circleProps,
|
||||||
default_active,
|
default_active,
|
||||||
changeHandler,
|
changeHandler,
|
||||||
|
label,
|
||||||
...props
|
...props
|
||||||
}: TWUI_TOGGLE_PROPS) {
|
}: TWUI_TOGGLE_PROPS) {
|
||||||
const [active, setActive] = useState(default_active || false);
|
const [active, setActive] = useState(default_active || false);
|
||||||
@@ -44,6 +48,12 @@ export default function Toggle({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Row
|
||||||
|
className={twMerge("cursor-pointer")}
|
||||||
|
onClick={() => {
|
||||||
|
setActive(!active);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
@@ -53,9 +63,6 @@ export default function Toggle({
|
|||||||
"twui-toggle-wrapper",
|
"twui-toggle-wrapper",
|
||||||
props.className,
|
props.className,
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
|
||||||
setActive(!active);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{typeof active == "undefined" ? (
|
{typeof active == "undefined" ? (
|
||||||
<div className="w-3.5 h-3.5 twui-toggle-circle"></div>
|
<div className="w-3.5 h-3.5 twui-toggle-circle"></div>
|
||||||
@@ -73,5 +80,7 @@ export default function Toggle({
|
|||||||
></div>
|
></div>
|
||||||
)}
|
)}
|
||||||
</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
|
* Function to grab the host's public
|
||||||
* IP address
|
* 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 {
|
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",
|
encoding: "utf-8",
|
||||||
})
|
})
|
||||||
.trim();
|
.trim();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import type { ImageInputToBase64FunctionReturn } from "../components/twui/utils/
|
|||||||
import EJSON from "../utils/ejson";
|
import EJSON from "../utils/ejson";
|
||||||
import twuiSlugify from "../components/twui/utils/slugify";
|
import twuiSlugify from "../components/twui/utils/slugify";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import type { AppContextObject } from "../types";
|
||||||
|
|
||||||
type Params<T extends {} = BUN_SQLITE_WGUI_ALL_TYPEDEFS> = {
|
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: {
|
before_ready_function?: (params: {
|
||||||
form: T;
|
form: T;
|
||||||
setForm: Dispatch<SetStateAction<T>>;
|
setForm: Dispatch<SetStateAction<T>>;
|
||||||
|
app_context: AppContextObject;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,7 +113,11 @@ export default function useFormInit<
|
|||||||
} finally {
|
} finally {
|
||||||
if (params?.before_ready_function) {
|
if (params?.before_ready_function) {
|
||||||
params
|
params
|
||||||
.before_ready_function({ form, setForm })
|
.before_ready_function({
|
||||||
|
form,
|
||||||
|
setForm,
|
||||||
|
app_context: appContext,
|
||||||
|
})
|
||||||
.then(() => {})
|
.then(() => {})
|
||||||
.catch((e2) => {
|
.catch((e2) => {
|
||||||
console.log(
|
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 Select from "@/src/components/twui/form/Select";
|
||||||
import Stack from "@/src/components/twui/layout/Stack";
|
import Stack from "@/src/components/twui/layout/Stack";
|
||||||
import useFormInit from "@/src/hooks/use-form-init";
|
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>>;
|
type Props = ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>>;
|
||||||
|
|
||||||
export default function AddHostFormInterface({ form, setForm }: Props) {
|
export default function AddHostFormInterface({
|
||||||
const [interfaces, setInterfaces] = useState<string[]>([]);
|
form,
|
||||||
|
setForm,
|
||||||
useEffect(() => {
|
app_context,
|
||||||
fetchApi<ApiReqParams, APIResponseObject>(
|
}: Props) {
|
||||||
"/api/admin/network-interfaces",
|
const interfaces = app_context.pageProps?.network_interfaces || [];
|
||||||
{ method: "GET" },
|
|
||||||
).then((res) => {
|
|
||||||
if (res.success && res.stringRes) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(res.stringRes) as string[];
|
|
||||||
setInterfaces(parsed);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className="w-full gap-2 items-stretch">
|
<Stack className="w-full gap-2 items-stretch">
|
||||||
|
|||||||
@@ -1,36 +1,66 @@
|
|||||||
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
|
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
|
||||||
import Input from "@/src/components/twui/form/Input";
|
import Input from "@/src/components/twui/form/Input";
|
||||||
import Stack from "@/src/components/twui/layout/Stack";
|
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 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 fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
|
||||||
import type { ApiReqParams } from "@/src/types";
|
import type { ApiReqParams } from "@/src/types";
|
||||||
import type { APIResponseObject } from "@moduletrace/bunext/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>>;
|
type Props = ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>>;
|
||||||
|
|
||||||
export default function AddHostFormPublicIP({ form, setForm }: Props) {
|
export default function AddHostFormPublicIP({ form, setForm }: Props) {
|
||||||
useEffect(() => {
|
const [useLocalIP, setUseLocalIP] = useState(false);
|
||||||
fetchApi<ApiReqParams, APIResponseObject>(
|
|
||||||
"/api/admin/public-ip",
|
const { loading, setLoading } = useStatus({ initialLoading: true });
|
||||||
{ method: "GET" },
|
|
||||||
).then((res) => {
|
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) {
|
if (res.success && res.stringRes) {
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
public_ip_address: res.stringRes!,
|
public_ip_address: res.stringRes!,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, 1000);
|
||||||
});
|
});
|
||||||
}, []);
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!form.interface) return;
|
||||||
|
fetchIP(form.interface, useLocalIP);
|
||||||
|
}, [form.interface, useLocalIP]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className="w-full gap-2">
|
<Stack className={twMerge("w-full gap-2 relative")}>
|
||||||
|
{loading ? <LoadingOverlay /> : null}
|
||||||
<Input
|
<Input
|
||||||
name="public_ip_address"
|
name="public_ip_address"
|
||||||
label="Public IP Address"
|
label="IP Address"
|
||||||
showLabel
|
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 || ""}
|
value={form.public_ip_address || ""}
|
||||||
changeHandler={(v) => {
|
changeHandler={(v) => {
|
||||||
setForm((prev) => ({
|
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>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ export default function AddHostForm() {
|
|||||||
wg_ip_address: AppData["DefaultPrivateIP"],
|
wg_ip_address: AppData["DefaultPrivateIP"],
|
||||||
},
|
},
|
||||||
title: "Add Host",
|
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");
|
const [ipStatus, setIpStatus] = useState<HostWgIpFieldStatus>("checking");
|
||||||
@@ -39,8 +49,8 @@ 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 /> */}
|
||||||
<AddHostFormPublicIP {...init} />
|
|
||||||
<AddHostFormInterface {...init} />
|
<AddHostFormInterface {...init} />
|
||||||
|
<AddHostFormPublicIP {...init} />
|
||||||
<AddHostFormWgIpAddress
|
<AddHostFormWgIpAddress
|
||||||
{...init}
|
{...init}
|
||||||
setIpStatus={setIpStatus}
|
setIpStatus={setIpStatus}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -11,8 +11,10 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
|||||||
if (params.req.method !== "GET") {
|
if (params.req.method !== "GET") {
|
||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
const url = new URL(params.req.url || "");
|
||||||
|
const iface = url.searchParams.get("interface") || undefined;
|
||||||
try {
|
try {
|
||||||
const public_ip = await grabHostPublicIPAddress();
|
const public_ip = await grabHostPublicIPAddress(iface);
|
||||||
return { success: true, stringRes: public_ip };
|
return { success: true, stringRes: public_ip };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return { success: false, msg: error.message };
|
return { success: false, msg: error.message };
|
||||||
|
|||||||
@@ -65,6 +65,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;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AppContextObject = {
|
export type AppContextObject = {
|
||||||
|
|||||||
Reference in New Issue
Block a user