This commit is contained in:
2026-09-20 04:14:32 +01:00
parent cda6cff34f
commit d5536b49d9
42 changed files with 1544 additions and 143 deletions
@@ -0,0 +1,127 @@
import { useEffect, useState } from "react";
import AdminCard from "@/src/components/general/admin-card";
import AdminButton from "@/src/components/general/admin-button";
import Input from "@/src/components/twui/form/Input";
import H3 from "@/src/components/twui/layout/H3";
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 Tag from "@/src/components/twui/elements/Tag";
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import Button from "@/src/components/twui/layout/Button";
type Props = {
host_id: number | string;
current_ip?: string;
};
export default function HostPublicIpSection({ host_id, current_ip }: Props) {
const [ip, setIp] = useState(current_ip || "");
const [displayedIp, setDisplayedIp] = useState(current_ip || "");
const [typed, setTyped] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string>();
const [success, setSuccess] = useState<string>();
useEffect(() => {
if (!current_ip || typed) return;
setIp(current_ip);
setDisplayedIp(current_ip);
}, [current_ip, typed]);
async function handleUpdate() {
setError(undefined);
setSuccess(undefined);
const value = ip.trim();
if (!value) {
setError("Please enter the new public IP address");
return;
}
setBusy(true);
try {
const res = await fetchApi<{ [k: string]: any }, APIResponseObject>(
"/api/admin/update-host-public-ip",
{
method: "POST",
body: {
host_id,
public_ip_address: value,
},
},
);
if (!res.success) {
setError(res.msg || "Could not update the host IP");
return;
}
setDisplayedIp(value);
setSuccess(res.msg || "Host IP updated");
} catch (error: any) {
setError(error.message || "Could not update the host IP");
} finally {
setBusy(false);
}
}
return (
<AdminCard className="w-full p-5 flex flex-col gap-4">
<Stack className="gap-1">
<H3 className="text-[14px] font-semibold mb-0!">
Host public IP
</H3>
<P
noMargin
className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45"
>
The public IP address clients use to reach this host.
Updating it regenerates every client config with the new
Endpoint.
</P>
</Stack>
<Row className="gap-3 items-center flex-wrap">
<Span className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45 whitespace-nowrap">
Current:
</Span>
<Span
className={
displayedIp
? "font-mono text-[12.5px] text-foreground-light/70 dark:text-foreground-dark/70"
: "text-[12.5px] text-foreground-light/40 dark:text-foreground-dark/40"
}
>
{displayedIp || "Auto-detected"}
</Span>
<Input
name="host_public_ip"
id="host_public_ip"
placeholder="e.g. 203.0.113.10"
label="New public IP"
showLabel
value={ip}
onChange={(e) => {
setTyped(true);
setIp(e.target.value);
}}
/>
<Button
title="Update IP Button"
variant="outlined"
disabled={busy}
onClick={handleUpdate}
size="small"
>
{busy ? "Updating…" : "Update IP & refresh configs"}
</Button>
</Row>
{error ? <Tag color="error">{error}</Tag> : null}
{success ? <Tag color="success">{success}</Tag> : null}
</AdminCard>
);
}
@@ -3,6 +3,7 @@ import Paper from "@/src/components/twui/elements/Paper";
import Center from "@/src/components/twui/layout/Center";
import Span from "@/src/components/twui/layout/Span";
import Stack from "@/src/components/twui/layout/Stack";
import ClientRow from "@/src/components/general/client-row";
import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get";
import { AppContext } from "@/src/pages/__root";
import { useContext } from "react";
@@ -11,6 +12,12 @@ type Props = {
host_id: string | number;
};
const thClass =
"px-4 py-2 text-left text-[11px] font-semibold uppercase tracking-[0.08em] " +
"text-foreground-light/40 dark:text-foreground-dark/40 whitespace-nowrap";
const thRightClass = `${thClass} text-right`;
export default function ClientsList({ host_id }: Props) {
const { pageProps } = useContext(AppContext);
@@ -20,6 +27,7 @@ export default function ClientsList({ host_id }: Props) {
query: {
host_id: { value: host_id },
},
order: { field: "created_at", strategy: "DESC" },
},
initial_res: pageProps?.clients || undefined,
});
@@ -44,5 +52,25 @@ export default function ClientsList({ host_id }: Props) {
);
}
return <Stack>{}</Stack>;
}
return (
<Paper className="overflow-x-auto">
<table className="w-full min-w-[860px]">
<thead>
<tr className="border-y border-slate-200 dark:border-white/10 bg-foreground-light/[0.02] dark:bg-foreground-dark/[0.03]">
<th className={thClass}>Client</th>
<th className={thClass}>Tunnel IP</th>
<th className={thClass}>Allowed IPs</th>
<th className={thClass}>Public key</th>
<th className={thRightClass}>Created</th>
<th className={thRightClass}>Actions</th>
</tr>
</thead>
<tbody>
{clients.map((client) => (
<ClientRow key={client.id} client={client} />
))}
</tbody>
</table>
</Paper>
);
}
@@ -0,0 +1,65 @@
import { useState } from "react";
import { Pencil, Trash2 } from "lucide-react";
import Button from "@/src/components/twui/layout/Button";
import adminCrudHandler from "@/src/functions/frontend/admin-crud-handler";
import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
type Props = {
client: BUN_SQLITE_WGUI_CLIENTS;
};
export default function ClientDetailActions({ client }: Props) {
const [busy, setBusy] = useState(false);
const host_id = Number(client?.host_id || 0);
async function handleDelete() {
if (
!window.confirm(
`Delete client "${client?.name || `#${client?.id}`}"? The client will be removed from the host and its config file deleted.`,
)
) {
return;
}
setBusy(true);
const res = await adminCrudHandler({
action: "delete",
table: "clients",
id: client?.id,
});
if (!res.success) {
setBusy(false);
window.alert(res.msg || "Could not delete client");
return;
}
window.location.pathname = `/admin/hosts/${host_id}/clients`;
}
return (
<>
<Button
title={`Edit client ${client?.name}`}
variant="outlined"
beforeIcon={<Pencil />}
href={`/admin/hosts/${host_id}/clients/${client?.id}/edit`}
>
Edit client
</Button>
<Button
title="Delete client"
variant="outlined"
color="error"
beforeIcon={<Trash2 />}
disabled={busy}
onClick={handleDelete}
className="hover:bg-error/10"
>
{busy ? "Deleting…" : "Delete client"}
</Button>
</>
);
}
@@ -0,0 +1,54 @@
import { useState } from "react";
import { Check, Copy } from "lucide-react";
import Row from "@/src/components/twui/layout/Row";
import Span from "@/src/components/twui/layout/Span";
type Props = {
label: string;
value: string;
mono?: boolean;
copyable?: boolean;
};
export default function ClientInfoRow({ label, value, mono, copyable }: Props) {
const [copied, setCopied] = useState(false);
return (
<Row className="justify-between gap-4 px-5 py-2.5 border-t border-slate-200/60 dark:border-white/5">
<Span className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45 whitespace-nowrap">
{label}
</Span>
<Row className="items-center gap-2 min-w-0">
<Span
className={
mono
? "font-mono text-[12.5px] text-foreground-light/70 dark:text-foreground-dark/70 break-all"
: "text-[12.5px] text-foreground-light/70 dark:text-foreground-dark/70 text-right break-all"
}
>
{value}
</Span>
{copyable && value && value != "—" ? (
<button
type="button"
title="Copy value"
aria-label={`Copy ${label}`}
onClick={() => {
navigator.clipboard?.writeText(value).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
}}
className="text-foreground-light/40 dark:text-foreground-dark/40 hover:text-foreground-light dark:hover:text-foreground-dark transition-colors duration-150 p-1 focus:outline-2"
>
{copied ? (
<Check size={14} />
) : (
<Copy size={14} />
)}
</button>
) : null}
</Row>
</Row>
);
}
@@ -0,0 +1,61 @@
import { useContext } from "react";
import AdminCard from "@/src/components/general/admin-card";
import CodeBlock from "@/src/components/twui/elements/CodeBlock";
import EmptyContent from "@/src/components/twui/elements/EmptyContent";
import H3 from "@/src/components/twui/layout/H3";
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 { AppContext } from "@/src/pages/__root";
import AceEditor from "@/src/components/twui/editors/AceEditor";
export default function ClientConfigSection() {
const { pageProps } = useContext(AppContext);
const config = pageProps?.client_config;
if (!config) {
return (
<AdminCard className="w-full overflow-hidden">
<Stack className="w-full items-stretch py-4">
<Row className="justify-between gap-3 px-5 flex-wrap">
<H3 className="mb-0!">Client configuration</H3>
</Row>
<EmptyContent title="No configuration file found" />
<Stack className="w-full justify-center py-1 px-6 text-center gap-1">
<Span className="text-[12.5px] text-foreground-light/35 dark:text-foreground-dark/35">
Run the client setup for this client to generate its
wg.conf on the server.
{pageProps?.client_config_path
? ` Expected at ${pageProps.client_config_path}.`
: ""}
</Span>
</Stack>
</Stack>
</AdminCard>
);
}
return (
<AdminCard className="w-full overflow-hidden">
<Stack className="w-full items-stretch py-4">
<Row className="justify-between gap-3 px-5 flex-wrap">
<Stack className="gap-0.5">
<H3 className="mb-0!">Client configuration</H3>
<P
noMargin
className="font-mono text-[11.5px] text-foreground-light/40 dark:text-foreground-dark/40"
>
{pageProps?.client_config_path}
</P>
</Stack>
<Span variant="faded">
Use this file with the WireGuard app
</Span>
</Row>
<AceEditor content={config} mode="ini" />
</Stack>
</AdminCard>
);
}
@@ -0,0 +1,58 @@
import { useContext } from "react";
import AdminCard from "@/src/components/general/admin-card";
import H3 from "@/src/components/twui/layout/H3";
import Row from "@/src/components/twui/layout/Row";
import Stack from "@/src/components/twui/layout/Stack";
import { AppContext } from "@/src/pages/__root";
import ClientInfoRow from "../(partials)/client-info-row";
export default function ClientInfoSection() {
const { pageProps } = useContext(AppContext);
const client = pageProps?.client;
const host_id = Number(client?.host_id || 0);
if (!client) {
return null;
}
const rows = [
{
label: "Host",
value: host_id ? `Host #${host_id}` : "Main host",
},
{
label: "Tunnel IP",
value: client.wg_ip_address || "—",
mono: true,
},
{
label: "Allowed IPs",
value: client.allowed_ips || "—",
mono: true,
},
{
label: "Public key",
value: client.public_key || "—",
mono: true,
copyable: true,
},
{
label: "Notes",
value: client.notes || "—",
},
];
return (
<AdminCard className="w-full overflow-hidden">
<Stack className="w-full items-stretch py-4">
<Row className="justify-between gap-3 px-5 flex-wrap">
<H3 className="mb-0!">Client information</H3>
</Row>
{rows.map((row) => (
<ClientInfoRow key={row.label} {...row} />
))}
</Stack>
</AdminCard>
);
}
@@ -0,0 +1,61 @@
import { useContext } from "react";
import AdminCard from "@/src/components/general/admin-card";
import H3 from "@/src/components/twui/layout/H3";
import Img from "@/src/components/twui/layout/Img";
import P from "@/src/components/twui/layout/P";
import Row from "@/src/components/twui/layout/Row";
import Stack from "@/src/components/twui/layout/Stack";
import { AppContext } from "@/src/pages/__root";
export default function ClientQRSection() {
const { pageProps } = useContext(AppContext);
const qr_data_uri = pageProps?.client_qr_data_uri;
return (
<AdminCard className="w-full overflow-hidden">
<Stack className="w-full items-stretch py-4">
<Row className="justify-between gap-3 px-5 flex-wrap">
<H3 className="mb-0!">Add via QR code</H3>
</Row>
{qr_data_uri ? (
<Row className="justify-center gap-4 px-5 py-5">
<Stack className="gap-2 items-center">
<Img
src={qr_data_uri}
alt="WireGuard configuration QR code for importing this client into the phone app"
width={260}
height={260}
circle={false}
className="rounded-default"
/>
<P
noMargin
className="text-[12px] text-foreground-light/50 dark:text-foreground-dark/50 max-w-[260px] text-center"
>
Scan with the official WireGuard app, or import the
config file manually via the config tab
</P>
</Stack>
</Row>
) : (
<Stack className="w-full justify-center py-10 px-6 text-center gap-1">
<P
noMargin
className="text-[13px] font-medium text-foreground-light/50 dark:text-foreground-dark/50"
>
No QR code available
</P>
<P
noMargin
className="text-[12.5px] text-foreground-light/35 dark:text-foreground-dark/35"
>
Generate the client config first — the QR code is
created from the stored wg.conf
</P>
</Stack>
)}
</Stack>
</AdminCard>
);
}
@@ -0,0 +1,20 @@
import Section from "@/src/components/twui/layout/Section";
import AddClientForm from "../../../add-client/(partials)/client-form/add-client-form";
import { useContext } from "react";
import { AppContext } from "@/src/pages/__root";
export default function EditClientFormSection() {
const { pageProps, query } = useContext(AppContext);
const host_id = Number(query?.host_id || 0);
return (
<Section>
<AddClientForm
existing_client={pageProps?.client || undefined}
existing_client_full={pageProps?.client || undefined}
host_id={host_id}
/>
</Section>
);
}
@@ -0,0 +1,28 @@
import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
type ClientEditPageQuery = PageQueryObject & {
client_id?: string;
};
const server: BunextPageServerFn<PagePropsType> = async ({ query }) => {
const page_query = query as ClientEditPageQuery;
const client_res = await BunSQLite.select<
BUN_SQLITE_WGUI_CLIENTS,
TableType
>({
table: "clients",
targetId: page_query.client_id,
});
return {
props: {
client: client_res.singleRes || null,
},
};
};
export default server;
@@ -0,0 +1,55 @@
import { ArrowLeft } from "lucide-react";
import { useContext } from "react";
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
import AdminHero from "@/src/components/general/admin-hero";
import { SiteData } from "@/src/data/site-data";
import { AppContext } from "@/src/pages/__root";
import Button from "@/src/components/twui/layout/Button";
import Divider from "@/src/components/twui/layout/Divider";
import Stack from "@/src/components/twui/layout/Stack";
import EmptyContent from "@/src/components/twui/elements/EmptyContent";
import EditClientFormSection from "./(sections)/edit-client-form-section";
export default function AdminEditClientPage() {
const { pageProps, query } = useContext(AppContext);
const host_id = Number(query?.host_id || pageProps?.client?.host_id || 0);
return (
<>
<AdminHero
title="Edit Client"
description={
pageProps?.client?.name
? `Edit client "${pageProps.client.name}"`
: "Edit client"
}
buttons={
<Button
title="View client"
variant="outlined"
beforeIcon={<ArrowLeft />}
href={`/admin/hosts/${host_id}/clients/${pageProps?.client?.id}`}
>
View client
</Button>
}
/>
<Divider className="mb-6" />
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
{pageProps?.client ? (
<EditClientFormSection />
) : (
<EmptyContent title="Client not found" />
)}
</Stack>
</>
);
}
export const meta: BunextPageModuleMeta = {
title: `Edit Client | ${SiteData["SiteName"]}`,
description: `WireGuard edit client page`,
};
@@ -0,0 +1,69 @@
import type {
BUN_SQLITE_WGUI_CLIENTS,
BUN_SQLITE_WGUI_HOSTS,
} from "@/db/types/db";
import { AppData } from "@/src/data/app-data";
import generateWireguardClientQR from "@/src/functions/backend/setup/generate-wireguard-client-qr";
import readWireguardClientConfig from "@/src/functions/backend/setup/read-wireguard-client-config";
import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
type ClientPageQuery = PageQueryObject & {
client_id?: string;
};
const server: BunextPageServerFn<PagePropsType> = async ({ query }) => {
const page_query = query as ClientPageQuery;
const client_res = await BunSQLite.select<
BUN_SQLITE_WGUI_CLIENTS,
TableType
>({
table: "clients",
targetId: page_query.client_id,
});
const client = client_res.singleRes || null;
const host_id = Number(
client?.host_id ||
page_query.host_id ||
AppData["WireguardHostID"],
);
let host: BUN_SQLITE_WGUI_HOSTS | null = null;
if (host_id && host_id != AppData["WireguardHostID"]) {
const host_res = await BunSQLite.select<
BUN_SQLITE_WGUI_HOSTS,
TableType
>({
table: "hosts",
targetId: host_id,
});
host = host_res.singleRes || null;
}
const config_res = await readWireguardClientConfig({
client_id: client?.id,
host_id,
});
const qr_res = await generateWireguardClientQR({
config: config_res.config,
});
return {
props: {
client,
host,
client_config: config_res.config,
client_config_path: config_res.config_path,
client_qr_data_uri: qr_res.qr_data_uri,
},
};
};
export default server;
@@ -0,0 +1,69 @@
import { ArrowLeft } from "lucide-react";
import { useContext } from "react";
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
import AdminHero from "@/src/components/general/admin-hero";
import { SiteData } from "@/src/data/site-data";
import { AppContext } from "@/src/pages/__root";
import Button from "@/src/components/twui/layout/Button";
import Divider from "@/src/components/twui/layout/Divider";
import Stack from "@/src/components/twui/layout/Stack";
import EmptyContent from "@/src/components/twui/elements/EmptyContent";
import ClientInfoSection from "./(sections)/client-info-section";
import ClientConfigSection from "./(sections)/client-config-section";
import ClientQRSection from "./(sections)/client-qr-section";
import ClientDetailActions from "./(partials)/client-detail-actions";
export default function AdminClientDetailPage() {
const { pageProps } = useContext(AppContext);
const host_id = Number(
pageProps?.host?.id || pageProps?.client?.host_id || 0,
);
const desc =
pageProps?.client?.notes ||
"Client details and WireGuard configuration";
return (
<>
<AdminHero
title={pageProps?.client?.name || "Client"}
description={desc}
buttons={
<>
<Button
title="Back to clients"
variant="outlined"
beforeIcon={<ArrowLeft />}
href={`/admin/hosts/${host_id}/clients`}
>
Back to clients
</Button>
{pageProps?.client ? (
<ClientDetailActions client={pageProps.client} />
) : null}
</>
}
/>
<Divider className="mb-6" />
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
{pageProps?.client ? (
<>
<ClientInfoSection />
<ClientQRSection />
<ClientConfigSection />
</>
) : (
<EmptyContent title="Client not found" />
)}
</Stack>
</>
);
}
export const meta: BunextPageModuleMeta = {
title: `Client | ${SiteData["SiteName"]}`,
description: `WireGuard client details page`,
};
@@ -31,7 +31,7 @@ export default async function grabNextAvailableClientIp({
setForm((prev) => ({
...prev,
wg_ip_address: res.msg,
allowed_ips: `${res.msg}/32`,
allowed_ips: `${res.msg}/24`,
}));
return {
@@ -18,23 +18,29 @@ export default async function submitAddClientForm(
{ host_id }: Params,
) {
try {
if (!window.confirm(`Create new client?`)) {
const confirm_msg = existing_full?.id
? `Update this Client?`
: `Create new client?`;
if (!window.confirm(confirm_msg)) {
return;
}
const final_host_id = Number(app_context.query?.host_id || 0);
const new_client_data: BUN_SQLITE_WGUI_CLIENTS = _.omitBy(
{
name: form.name || "",
wg_ip_address: form.wg_ip_address || "",
allowed_ips: form.allowed_ips || "",
notes: form.notes || "",
host_id: final_host_id,
user_id: app_context?.user?.id,
},
(value) => value === undefined,
);
const new_client_data: BUN_SQLITE_WGUI_CLIENTS =
_.omitBy<BUN_SQLITE_WGUI_CLIENTS>(
{
name: form.name || "",
wg_ip_address: form.wg_ip_address || "",
allowed_ips: form.allowed_ips || "",
notes: form.notes || "",
host_id: final_host_id,
user_id: app_context?.user?.id,
allow_all_ips: form.allow_all_ips,
},
(value) => value === undefined,
);
setLoading(true);
@@ -53,7 +59,7 @@ export default async function submitAddClientForm(
if (res.success) {
if (existing_full?.id) {
window.location.reload();
window.location.pathname = `/admin/hosts/${final_host_id}/clients/${existing_full.id}`;
} else {
window.location.pathname = `/admin/hosts/${final_host_id}/clients`;
}
@@ -1,5 +1,7 @@
import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
import Checkbox from "@/src/components/twui/form/Checkbox";
import Input from "@/src/components/twui/form/Input";
import Stack from "@/src/components/twui/layout/Stack";
import useFormInit from "@/src/hooks/use-form-init";
export default function AddClientFormAllowedIps({
@@ -7,17 +9,31 @@ export default function AddClientFormAllowedIps({
setForm,
}: ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_CLIENTS>>) {
return (
<Input
title="Allowed IPs"
placeholder="Eg. 10.0.0.2/32"
onChange={(e) => {
setForm((prev) => ({
...prev,
allowed_ips: e.target.value,
}));
}}
value={form.allowed_ips}
showLabel
/>
<Stack className="gap-2 my-4 w-full">
{form.allow_all_ips ? null : (
<Input
title="Allowed IPs"
placeholder="Eg. 10.0.0.2/24"
onChange={(e) => {
setForm((prev) => ({
...prev,
allowed_ips: e.target.value,
}));
}}
value={form.allowed_ips}
showLabel
/>
)}
<Checkbox
label={`Allow all IPs? That is 0.0.0.0/0, ::/0`}
defaultChecked={form.allow_all_ips == 1}
changeHandler={(value) => {
setForm((prev) => ({
...prev,
allow_all_ips: value ? 1 : 0,
}));
}}
/>
</Stack>
);
}
@@ -48,7 +48,7 @@ export default function AddClientFormWgIpAddress({
setForm((prev) => ({
...prev,
wg_ip_address: e.target.value,
allowed_ips: `${e.target.value}/32`,
allowed_ips: `${e.target.value.replace(/\.\d+$/, "")}.0/24`,
}));
}}
value={form.wg_ip_address}
+18 -1
View File
@@ -3,8 +3,16 @@ import AdminHero from "@/src/components/general/admin-hero";
import { SiteData } from "@/src/data/site-data";
import Button from "@/src/components/twui/layout/Button";
import Divider from "@/src/components/twui/layout/Divider";
import Stack from "@/src/components/twui/layout/Stack";
import HostPublicIpSection from "../(sections)/host-public-ip-section";
import { useContext } from "react";
import { AppContext } from "@/src/pages/__root";
export default function AdminSingleHostPage() {
const { pageProps } = useContext(AppContext);
const host_id = Number(pageProps?.host?.id || 0);
return (
<>
<AdminHero
@@ -20,6 +28,15 @@ export default function AdminSingleHostPage() {
/>
<Divider className="mb-6" />
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
{pageProps?.host ? (
<HostPublicIpSection
host_id={host_id}
current_ip={pageProps.host.public_ip_address || ""}
/>
) : null}
</Stack>
</>
);
}
@@ -27,4 +44,4 @@ export default function AdminSingleHostPage() {
export const meta: BunextPageModuleMeta = {
title: `Host | ${SiteData["SiteName"]}`,
description: `WireGuard host page`,
};
};
+19 -1
View File
@@ -14,10 +14,15 @@ import checkIfMainHostIsSet from "@/src/functions/check-if-main-host-is-set";
import SetupMainHostButton from "@/src/components/general/setup-main-host-button";
import Row from "@/src/components/twui/layout/Row";
import { Plus } from "lucide-react";
import HostPublicIpSection from "./(sections)/host-public-ip-section";
export default function AdminHostPage() {
const { hosts, variables, clients } = useHostData();
const main_host_public_ip = variables?.find(
(v) => v.key == "main_host_public_ip_address",
)?.value;
const is_main_host_set = checkIfMainHostIsSet({ variables });
if (!is_main_host_set) {
@@ -57,11 +62,20 @@ export default function AdminHostPage() {
<H2>Main Host</H2>
<Row>
<Button
title="View Clients"
size="small"
variant="outlined"
color="primary"
href={`/admin/hosts/0/clients`}
>
View Clients
</Button>
<Button
title="Add Client"
beforeIcon={<Plus />}
size="small"
href={`/admin/hosts/0/clients/add`}
href={`/admin/hosts/0/clients/add-client`}
>
Add Client
</Button>
@@ -71,6 +85,10 @@ export default function AdminHostPage() {
variables={variables}
clients={clients}
/>
<HostPublicIpSection
host_id={0}
current_ip={main_host_public_ip || ""}
/>
<InterfaceConfigSection
variables={variables}
clients={clients}