Add host setup functions and client components
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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`,
|
||||
};
|
||||
Reference in New Issue
Block a user