Add host setup functions and client components

This commit is contained in:
2026-09-13 10:52:30 +01:00
parent 76679aabe7
commit 86eb3a0b05
53 changed files with 1284 additions and 1325 deletions
@@ -0,0 +1,42 @@
import { AppData } from "@/src/data/app-data";
import type {
BUN_SQLITE_WGUI_CLIENTS,
BUN_SQLITE_WGUI_HOSTS,
BUN_SQLITE_WGUI_VARIABLES,
} from "@/db/types/db";
const LISTEN_PORT = 51820;
const IP_TABLES_DIR = `/var/lib/wgui/iptables`;
type Params = {
host?: BUN_SQLITE_WGUI_HOSTS;
variables?: BUN_SQLITE_WGUI_VARIABLES[];
clients?: BUN_SQLITE_WGUI_CLIENTS[];
};
export default function deriveHostConfig({
host,
variables,
clients,
}: Params) {
const host_id = host?.id || AppData["WireguardHostID"];
const wg_ip_address =
host?.wg_ip_address ||
variables?.find((v) => v.key == "main_host_wg_ip_address")?.value;
return {
host_id,
interface_name: `wg${host_id}`,
config_path: `/etc/wireguard/wg${host_id}.conf`,
address: wg_ip_address ? `${wg_ip_address}/24` : undefined,
listen_port: LISTEN_PORT,
post_up: `${IP_TABLES_DIR}/${host_id}-up.sh`,
post_down: `${IP_TABLES_DIR}/${host_id}-down.sh`,
private_key: host?.private_key,
public_key: host?.public_key,
client_count: clients?.filter(
(client) => (client.host_id || 0) == host_id,
).length || 0,
};
}
@@ -0,0 +1,28 @@
import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get";
import type {
BUN_SQLITE_WGUI_CLIENTS,
BUN_SQLITE_WGUI_HOSTS,
BUN_SQLITE_WGUI_VARIABLES,
} from "@/db/types/db";
export default function useHostData() {
const hosts_res = useAdminCrudGet<BUN_SQLITE_WGUI_HOSTS>({
table: "hosts",
limit: 100,
});
const variables_res = useAdminCrudGet<BUN_SQLITE_WGUI_VARIABLES>({
table: "variables",
});
const clients_res = useAdminCrudGet<BUN_SQLITE_WGUI_CLIENTS>({
table: "clients",
limit: 100,
});
return {
hosts: hosts_res.res,
variables: variables_res.res,
clients: clients_res.res,
};
}
@@ -0,0 +1,20 @@
import Span from "@/src/components/twui/layout/Span";
import Stack from "@/src/components/twui/layout/Stack";
type Props = {
label: string;
value: string;
};
export default function HostStatCell({ label, value }: Props) {
return (
<Stack className="gap-1">
<Span className="text-[11.5px] font-semibold uppercase tracking-[0.08em] text-foreground-light/40 dark:text-foreground-dark/40">
{label}
</Span>
<Span className="tabular text-[15px] font-semibold text-foreground-light/85 dark:text-foreground-dark/85">
{value}
</Span>
</Stack>
);
}
@@ -0,0 +1,20 @@
import Row from "@/src/components/twui/layout/Row";
import Span from "@/src/components/twui/layout/Span";
type Props = {
keyName: string;
value: string;
};
export default function InterfaceConfigRow({ keyName, value }: Props) {
return (
<Row className="justify-between gap-4 px-5 py-2.5 border-t border-slate-200/60 dark:border-white/5">
<Span className="font-mono text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45 whitespace-nowrap">
{keyName} =
</Span>
<Span className="font-mono text-[12.5px] text-foreground-light/70 dark:text-foreground-dark/70 text-right break-all">
{value}
</Span>
</Row>
);
}
@@ -0,0 +1,97 @@
import { Copy } from "lucide-react";
import AdminCard from "@/src/components/general/admin-card";
import AdminButton from "@/src/components/general/admin-button";
import InterfaceConfigRow from "../(partials)/interface-config-row";
import H2 from "@/src/components/twui/layout/H2";
import P from "@/src/components/twui/layout/P";
import Row from "@/src/components/twui/layout/Row";
import Span from "@/src/components/twui/layout/Span";
import Stack from "@/src/components/twui/layout/Stack";
import deriveHostConfig from "../(functions)/derive-host-config";
import type {
BUN_SQLITE_WGUI_CLIENTS,
BUN_SQLITE_WGUI_HOSTS,
BUN_SQLITE_WGUI_VARIABLES,
} from "@/db/types/db";
type Props = {
host?: BUN_SQLITE_WGUI_HOSTS;
variables?: BUN_SQLITE_WGUI_VARIABLES[];
clients?: BUN_SQLITE_WGUI_CLIENTS[];
};
export default function InterfaceConfigSection({
host,
variables,
clients,
}: Props) {
const config = deriveHostConfig({ host, variables, clients });
const rows = [
{ keyName: "Address", value: config.address || "—" },
{ keyName: "ListenPort", value: String(config.listen_port) },
{
keyName: "PrivateKey",
value: config.private_key
? "•••••••••••• (redacted)"
: "Not set",
},
{ keyName: "PostUp", value: config.post_up },
{ keyName: "PostDown", value: config.post_down },
];
function buildConfigText() {
const lines = ["[Interface]"];
for (const row of rows) {
lines.push(`${row.keyName} = ${row.value}`);
}
lines.push("");
lines.push(
`# Public key: ${config.public_key?.slice(0, 24) || "Not generated"}`,
);
return lines.join("\n");
}
return (
<AdminCard className="w-full overflow-hidden">
<Row className="justify-between gap-3 px-5 h-12 flex-wrap">
<Stack className="gap-0.5">
<H2 className="text-[13.5px] font-medium text-foreground-light/60 dark:text-foreground-dark/60 mb-0!">
Interface configuration
</H2>
<P noMargin className="font-mono text-[11.5px] text-foreground-light/40 dark:text-foreground-dark/40">
{config.config_path}
</P>
</Stack>
<AdminButton
Icon={Copy}
onClick={() =>
navigator.clipboard?.writeText(buildConfigText())
}
>
Copy config
</AdminButton>
</Row>
<div className="border-t border-slate-200 dark:border-white/10">
<Span className="px-5 py-3 font-mono text-[12.5px] font-semibold text-secondary dark:text-secondary block">
[Interface]
</Span>
{rows.map((row) => (
<InterfaceConfigRow
key={row.keyName}
keyName={row.keyName}
value={row.value}
/>
))}
<Row className="justify-between gap-4 px-5 py-2.5 border-t border-slate-200/60 dark:border-white/5">
<Span className="font-mono text-[12px] text-foreground-light/35 dark:text-foreground-dark/35">
# Public key ·{" "}
{config.public_key
? `${config.public_key.slice(0, 24)}…`
: "Not generated"}
</Span>
</Row>
</div>
</AdminCard>
);
}
@@ -0,0 +1,80 @@
import AdminCard from "@/src/components/general/admin-card";
import HostStatCell from "../(partials)/host-stat-cell";
import Row from "@/src/components/twui/layout/Row";
import Span from "@/src/components/twui/layout/Span";
import Stack from "@/src/components/twui/layout/Stack";
import deriveHostConfig from "../(functions)/derive-host-config";
import { twMerge } from "tailwind-merge";
import type {
BUN_SQLITE_WGUI_CLIENTS,
BUN_SQLITE_WGUI_VARIABLES,
} from "@/db/types/db";
type Props = {
variables?: BUN_SQLITE_WGUI_VARIABLES[];
clients?: BUN_SQLITE_WGUI_CLIENTS[];
};
export default function MainHostStatusSection({ variables, clients }: Props) {
const config = deriveHostConfig({ variables, clients });
const stats = [
{
label: "Address",
value: config.address || "—",
},
{
label: "Interface",
value: config.interface_name,
},
{
label: "Peers",
value: String(config.client_count),
},
{
label: "Public key",
value: config.public_key
? `${config.public_key.slice(0, 20)}…`
: "Not generated",
},
];
return (
<AdminCard className="w-full p-5 flex flex-col gap-5">
<Row className="justify-between flex-nowrap items-start gap-3">
<Row className="gap-3 items-start">
<Span
className={twMerge(
"relative flex w-2.5 h-2.5",
config.address
? "bg-success"
: "bg-slate-400 dark:bg-slate-600",
)}
aria-hidden="true"
/>
<Stack className="gap-1">
<Span className="text-[14px] font-semibold text-foreground-light dark:text-foreground-dark leading-none">
{config.address ? "Configured" : "Not configured"}
</Span>
<Span className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45">
{config.interface_name}
{config.address ? ` · ${config.address}` : ""}
</Span>
</Stack>
</Row>
<Span className="tabular text-[12px] text-foreground-light/40 dark:text-foreground-dark/40">
{config.config_path}
</Span>
</Row>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-x-4 gap-y-5 pt-4 border-t border-slate-200 dark:border-white/10">
{stats.map((stat) => (
<HostStatCell
key={stat.label}
label={stat.label}
value={stat.value}
/>
))}
</div>
</AdminCard>
);
}
+78
View File
@@ -0,0 +1,78 @@
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
import AdminHero from "@/src/components/general/admin-hero";
import Stack from "@/src/components/twui/layout/Stack";
import { SiteData } from "@/src/data/site-data";
import useHostData from "./(hooks)/use-host-data";
import InterfaceConfigSection from "./(sections)/interface-config-section";
import Button from "@/src/components/twui/layout/Button";
import MainHostStatusSection from "./(sections)/main-host-status-section";
import H2 from "@/src/components/twui/layout/H2";
import Divider from "@/src/components/twui/layout/Divider";
import EmptyContent from "@/src/components/twui/elements/EmptyContent";
import Span from "@/src/components/twui/layout/Span";
import checkIfMainHostIsSet from "@/src/functions/check-if-main-host-is-set";
import SetupMainHostButton from "@/src/components/general/setup-main-host-button";
export default function AdminHostPage() {
const { hosts, variables, clients } = useHostData();
const is_main_host_set = checkIfMainHostIsSet({ variables });
if (!is_main_host_set) {
return (
<>
<AdminHero
title="Hosts"
description="WireGuard server configuration and interface details"
/>
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
<Span variant="faded">Main Host Not Set up</Span>
<SetupMainHostButton />
</Stack>
</>
);
}
return (
<>
<AdminHero
title="Host"
description="WireGuard server configuration and interface details"
buttons={
<>
<Button title="Add New Host">Add New Host</Button>
</>
}
/>
<Divider className="mb-6" />
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
<H2>Main Host</H2>
<MainHostStatusSection
variables={variables}
clients={clients}
/>
<InterfaceConfigSection
variables={variables}
clients={clients}
/>
</Stack>
<Divider className="mb-6" />
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
<H2>Other Hosts</H2>
{hosts?.[0] ? (
<></>
) : (
<EmptyContent title="No other hosts found" />
)}
</Stack>
</>
);
}
export const meta: BunextPageModuleMeta = {
title: `Host | ${SiteData["SiteName"]}`,
description: `WireGuard host configuration`,
};