This commit is contained in:
2026-09-20 17:04:17 +01:00
parent 69111e461e
commit aa0a292943
16 changed files with 295 additions and 50 deletions
+3 -1
View File
@@ -76,7 +76,9 @@ export default function ClientRow({ client, rules }: Props) {
{client.wg_ip_address || "—"} {client.wg_ip_address || "—"}
</td> </td>
<td className="px-4 py-[9px] tabular text-[13px] text-foreground-light/45 dark:text-foreground-dark/45 whitespace-nowrap"> <td className="px-4 py-[9px] tabular text-[13px] text-foreground-light/45 dark:text-foreground-dark/45 whitespace-nowrap">
{client.allowed_ips || "—"} {client.allow_all_ips
? "0.0.0.0/0, ::/0"
: client.allowed_ips || "—"}
</td> </td>
<td className="px-4 py-[9px] whitespace-nowrap"> <td className="px-4 py-[9px] whitespace-nowrap">
<Span <Span
@@ -104,9 +104,11 @@ export default async function setupWireguardHost({
const is_ip_available = is_update_after_client_setup const is_ip_available = is_update_after_client_setup
? { success: true } ? { success: true }
: await checkPrivateIPAvailability({ : host?.wg_ip_address
ip_address: HOST_WG_IP, ? { success: true }
}); : await checkPrivateIPAvailability({
ip_address: HOST_WG_IP,
});
if (!is_ip_available.success && !is_update_after_client_setup) { if (!is_ip_available.success && !is_update_after_client_setup) {
return { return {
@@ -6,7 +6,12 @@ import Stack from "@/src/components/twui/layout/Stack";
import Span from "@/src/components/twui/layout/Span"; import Span from "@/src/components/twui/layout/Span";
import AdminCard from "@/src/components/general/admin-card"; import AdminCard from "@/src/components/general/admin-card";
import ClientRow from "@/src/components/general/client-row"; import ClientRow from "@/src/components/general/client-row";
import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db"; import type {
BUN_SQLITE_WGUI_CLIENT_RULES,
BUN_SQLITE_WGUI_CLIENTS,
} from "@/db/types/db";
import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get";
import { useMemo } from "react";
type Props = { type Props = {
clients?: BUN_SQLITE_WGUI_CLIENTS[]; clients?: BUN_SQLITE_WGUI_CLIENTS[];
@@ -23,6 +28,34 @@ const RECENT_LIMIT = 8;
export default function PeersTableSection({ clients }: Props) { export default function PeersTableSection({ clients }: Props) {
const recent = (clients || []).slice(0, RECENT_LIMIT); const recent = (clients || []).slice(0, RECENT_LIMIT);
const { res: client_rules } = useAdminCrudGet<BUN_SQLITE_WGUI_CLIENT_RULES>(
{
table: "client_rules",
sql_query: {
limit: 500,
},
},
);
const rules_by_client_id = useMemo(() => {
const map = new Map<number, BUN_SQLITE_WGUI_CLIENT_RULES[]>();
for (let i = 0; i < (client_rules || []).length; i++) {
const rule = client_rules?.[i];
const client_id = Number(rule?.client_id);
if (!rule || !client_id) {
continue;
}
const existing = map.get(client_id) || [];
existing.push(rule);
map.set(client_id, existing);
}
return map;
}, [client_rules]);
return ( return (
<AdminCard className="w-full overflow-hidden"> <AdminCard className="w-full overflow-hidden">
<Row className="justify-between gap-3 px-5 h-12 flex-wrap"> <Row className="justify-between gap-3 px-5 h-12 flex-wrap">
@@ -40,24 +73,40 @@ export default function PeersTableSection({ clients }: Props) {
{recent.length ? ( {recent.length ? (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full min-w-[860px]"> <table className="w-full min-w-[860px]">
<thead> <thead className="w-full">
<tr className="border-y border-slate-200 dark:border-white/10 bg-foreground-light/[0.02] dark:bg-foreground-dark/[0.03]"> <tr className="border-y border-slate-200 dark:border-white/10 bg-foreground-light/[0.02] dark:bg-foreground-dark/[0.03] w-full">
<th className={thClass}>Client</th> <th className={thClass}>Client</th>
<th className={thClass}>Host</th>
<th className={thClass}>Tunnel IP</th> <th className={thClass}>Tunnel IP</th>
<th className={thClass}>Allowed IPs</th> <th className={thClass}>Allowed IPs</th>
<th className={thClass}>Access List</th>
<th className={thClass}>Public key</th> <th className={thClass}>Public key</th>
<th className={thRightClass}>Created</th> <th className={thRightClass}>Created</th>
<th className={thRightClass}>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{recent.map((client) => ( {recent.map((client) => (
<ClientRow key={client.id} client={client} /> <ClientRow
key={client.id}
client={client}
rules={
client.id
? rules_by_client_id.get(
Number(client.id),
)
: undefined
}
/>
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
) : ( ) : (
<Stack center className="w-full justify-center py-14 px-6 text-center gap-1"> <Stack
center
className="w-full justify-center py-14 px-6 text-center gap-1"
>
<Span className="text-[13.5px] font-medium text-foreground-light/50 dark:text-foreground-dark/50"> <Span className="text-[13.5px] font-medium text-foreground-light/50 dark:text-foreground-dark/50">
No clients yet No clients yet
</Span> </Span>
@@ -68,4 +117,4 @@ export default function PeersTableSection({ clients }: Props) {
)} )}
</AdminCard> </AdminCard>
); );
} }
@@ -1,4 +1,7 @@
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db"; import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
import getInterfaceLocalIP from "@/src/functions/backend/setup/get-interface-local-ip";
import grabHostNetworkInterfaces from "@/src/functions/backend/setup/grab-host-network-interfaces";
import grabHostPublicIPAddress from "@/src/functions/backend/setup/grab-host-public-ip-address";
import type { PagePropsType, PageQueryObject, TableType } from "@/src/types"; import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite"; import BunSQLite from "@moduletrace/bun-sqlite";
import type { BunextPageServerFn } from "@moduletrace/bunext/types"; import type { BunextPageServerFn } from "@moduletrace/bunext/types";
@@ -8,19 +11,23 @@ const server: BunextPageServerFn<PagePropsType> = async ({ query }) => {
const host_id = Number(page_query.host_id || 0); const host_id = Number(page_query.host_id || 0);
const host_res = await BunSQLite.select< const host_res = await BunSQLite.select<BUN_SQLITE_WGUI_HOSTS, TableType>({
BUN_SQLITE_WGUI_HOSTS,
TableType
>({
table: "hosts", table: "hosts",
targetId: host_id, targetId: host_id,
}); });
const host = host_res.singleRes || null; const host = host_res.singleRes || null;
const network_interfaces = await grabHostNetworkInterfaces();
const local_ip = host?.interface
? await getInterfaceLocalIP(host?.interface)
: null;
return { return {
props: { props: {
host, host,
network_interfaces,
is_local_ip: local_ip == host?.public_ip_address,
}, },
}; };
}; };
+11 -2
View File
@@ -9,6 +9,9 @@ import { ArrowLeft } from "lucide-react";
import { useContext } from "react"; import { useContext } from "react";
import { AppContext } from "@/src/pages/__root"; import { AppContext } from "@/src/pages/__root";
import EditHostFormSection from "./(sections)/edit-host-form-section"; import EditHostFormSection from "./(sections)/edit-host-form-section";
import AddHostForm from "../../add/(partials)/add-host-form/add-host-form";
import _ from "lodash";
import type { BUN_SQLITE_WGUI_HOSTS_JOIN } from "@/src/types/sql-joins";
export default function AdminEditHostPage() { export default function AdminEditHostPage() {
const { pageProps, query } = useContext(AppContext); const { pageProps, query } = useContext(AppContext);
@@ -40,7 +43,13 @@ export default function AdminEditHostPage() {
<Stack className="w-full px-6 pb-8 gap-5 items-stretch"> <Stack className="w-full px-6 pb-8 gap-5 items-stretch">
{pageProps?.host ? ( {pageProps?.host ? (
<EditHostFormSection /> <AddHostForm
existing_host={_.pick<
BUN_SQLITE_WGUI_HOSTS_JOIN,
keyof BUN_SQLITE_WGUI_HOSTS_JOIN
>(pageProps.host, ["name", "short_description"])}
existing_host_full={pageProps.host}
/>
) : ( ) : (
<EmptyContent title="Host not found" /> <EmptyContent title="Host not found" />
)} )}
@@ -52,4 +61,4 @@ export default function AdminEditHostPage() {
export const meta: BunextPageModuleMeta = { export const meta: BunextPageModuleMeta = {
title: `Edit Host | ${SiteData["SiteName"]}`, title: `Edit Host | ${SiteData["SiteName"]}`,
description: `WireGuard edit host page`, description: `WireGuard edit host page`,
}; };
@@ -1,14 +1,13 @@
import type { BUN_SQLITE_WGUI_CLIENTS, BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db"; import type {
BUN_SQLITE_WGUI_CLIENTS,
BUN_SQLITE_WGUI_HOSTS,
} from "@/db/types/db";
import grabHostDirnames from "@/src/functions/backend/setup/grab-host-dir-names"; import grabHostDirnames from "@/src/functions/backend/setup/grab-host-dir-names";
import type { PagePropsType, PageQueryObject, TableType } from "@/src/types"; import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite"; import BunSQLite from "@moduletrace/bun-sqlite";
import type { BunextPageServerFn } from "@moduletrace/bunext/types"; import type { BunextPageServerFn } from "@moduletrace/bunext/types";
const server: BunextPageServerFn<PagePropsType> = async ({ const server: BunextPageServerFn<PagePropsType> = async ({ query }) => {
req,
url,
query,
}) => {
const page_query = query as PageQueryObject; const page_query = query as PageQueryObject;
const host_record_res = await BunSQLite.select< const host_record_res = await BunSQLite.select<
+1 -1
View File
@@ -59,7 +59,7 @@ export default function AdminSingleHostPage() {
color="primary" color="primary"
href={`/admin/hosts/${host_id}/clients`} href={`/admin/hosts/${host_id}/clients`}
> >
View Clients Clients
</Button> </Button>
<Button <Button
title="Add Client" title="Add Client"
@@ -10,36 +10,53 @@ export default async function submitAddHostForm({
form, form,
app_context, app_context,
clearLocalForm, clearLocalForm,
existing_full,
}: ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>>) { }: ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>>) {
try { try {
if (!window.confirm("Create this new host?")) { const confirm_msg = existing_full?.id
? `Update this host?`
: `Create this new host?`;
if (!window.confirm(confirm_msg)) {
return; return;
} }
setLoading(true); setLoading(true);
const res = await fetchApi<ApiReqParams, APIResponseObject>( const res = await fetchApi<ApiReqParams, APIResponseObject>(
`/api/admin/add-host`, existing_full?.id
? `/api/admin/update-host`
: `/api/admin/add-host`,
{ {
method: "POST", method: "POST",
body: { body: existing_full?.id
wg_ip_address: form.wg_ip_address || "", ? {
public_ip_address: form.public_ip_address || "", host_id: existing_full.id,
interface: form.interface || "", update_data: form,
name: form.name || "", }
short_description: form.short_description || "", : {
listen_port: wg_ip_address: form.wg_ip_address || "",
form.listen_port != null public_ip_address: form.public_ip_address || "",
? Number(form.listen_port) interface: form.interface || "",
: null, name: form.name || "",
is_main_host: app_context.pageProps?.is_main_host, short_description: form.short_description || "",
}, listen_port:
form.listen_port != null
? Number(form.listen_port)
: null,
is_main_host: app_context.pageProps?.is_main_host,
},
}, },
); );
const host_id =
typeof existing_full?.id == "number"
? existing_full.id
: res.numberRes;
if (res.success) { if (res.success) {
clearLocalForm(); clearLocalForm();
window.location.pathname = `/admin/hosts/${res.numberRes}`; window.location.pathname = `/admin/hosts/${host_id}`;
return; return;
} }
@@ -10,6 +10,7 @@ type Props = ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>> & {
export default function AddHostFormAction({ export default function AddHostFormAction({
status, status,
ipStatus, ipStatus,
existing_full,
}: Props) { }: Props) {
if (status?.error) { if (status?.error) {
return null; return null;
@@ -19,9 +20,9 @@ export default function AddHostFormAction({
<Button <Button
title="Create Host" title="Create Host"
type="submit" type="submit"
disabled={ipStatus !== "available"} disabled={existing_full?.id ? false : ipStatus !== "available"}
> >
Create Host {existing_full?.id ? "Update Host" : "Create Host"}
</Button> </Button>
); );
} }
@@ -1,4 +1,5 @@
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 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";
@@ -9,9 +10,21 @@ export default function AddHostFormInterface({
form, form,
setForm, setForm,
app_context, app_context,
existing_full,
}: Props) { }: Props) {
const interfaces = app_context.pageProps?.network_interfaces || []; const interfaces = app_context.pageProps?.network_interfaces || [];
if (existing_full?.id) {
return (
<Input
value={existing_full.interface || "N/A"}
title="Network Interface"
readOnly
showLabel
/>
);
}
return ( return (
<Stack className="w-full gap-2 items-stretch"> <Stack className="w-full gap-2 items-stretch">
<Select <Select
@@ -12,9 +12,24 @@ import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
type Props = ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>>; type Props = ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>>;
export default function AddHostFormListenPort({ form, setForm }: Props) { export default function AddHostFormListenPort({
form,
setForm,
existing_full,
}: Props) {
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
if (existing_full?.id) {
return (
<Input
value={existing_full.listen_port || "N/A"}
title="Listen Port"
readOnly
showLabel
/>
);
}
async function handleGrabNext() { async function handleGrabNext() {
setBusy(true); setBusy(true);
try { try {
@@ -1,9 +1,6 @@
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, useState } 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";
@@ -16,10 +13,17 @@ 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,
app_context,
existing_full,
}: Props) {
const [useLocalIP, setUseLocalIP] = useState(false); const [useLocalIP, setUseLocalIP] = useState(false);
const { loading, setLoading } = useStatus({ initialLoading: true }); const { loading, setLoading } = useStatus({
initialLoading: existing_full?.id ? false : true,
});
function fetchIP(iface: string, local: boolean) { function fetchIP(iface: string, local: boolean) {
if (!iface) return; if (!iface) return;
@@ -53,6 +57,17 @@ export default function AddHostFormPublicIP({ form, setForm }: Props) {
fetchIP(form.interface, useLocalIP); fetchIP(form.interface, useLocalIP);
}, [form.interface, useLocalIP]); }, [form.interface, useLocalIP]);
if (existing_full?.id) {
return (
<Input
value={existing_full.public_ip_address || "N/A"}
title="IP Address"
readOnly
showLabel
/>
);
}
return ( return (
<Stack className={twMerge("w-full gap-2 relative")}> <Stack className={twMerge("w-full gap-2 relative")}>
{loading ? <LoadingOverlay /> : null} {loading ? <LoadingOverlay /> : null}
@@ -76,7 +91,9 @@ export default function AddHostFormPublicIP({ form, setForm }: Props) {
label={ label={
<span className="text-base opacity-80">Use Local IP?</span> <span className="text-base opacity-80">Use Local IP?</span>
} }
defaultChecked={useLocalIP} defaultChecked={
useLocalIP || Boolean(app_context.pageProps?.is_local_ip)
}
/> />
</Stack> </Stack>
); );
@@ -3,6 +3,7 @@ import GrabNextSubnetButton from "@/src/components/general/grab-next-subnet-butt
import HostWgIpField, { import HostWgIpField, {
type HostWgIpFieldStatus, type HostWgIpFieldStatus,
} from "@/src/components/general/host-wg-ip-field"; } from "@/src/components/general/host-wg-ip-field";
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 useFormInit from "@/src/hooks/use-form-init"; import useFormInit from "@/src/hooks/use-form-init";
@@ -14,6 +15,17 @@ export default function AddHostFormWgIpAddress({
setIpStatus, setIpStatus,
...props ...props
}: Props) { }: Props) {
if (props.existing_full?.wg_ip_address) {
return (
<Input
placeholder="WG IP address"
value={props.existing_full.wg_ip_address}
readOnly
showLabel
/>
);
}
return ( return (
<Stack className="w-full gap-2"> <Stack className="w-full gap-2">
<HostWgIpField <HostWgIpField
@@ -8,7 +8,6 @@ import useFormInit from "@/src/hooks/use-form-init";
import { AppData } from "@/src/data/app-data"; import { AppData } from "@/src/data/app-data";
import submitAddHostForm from "../../(functions)/submit-add-host-form"; import submitAddHostForm from "../../(functions)/submit-add-host-form";
import AddHostFormAction from "./add-host-form-action"; import AddHostFormAction from "./add-host-form-action";
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";
@@ -18,7 +17,15 @@ 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";
export default function AddHostForm() { type Props = {
existing_host?: BUN_SQLITE_WGUI_HOSTS;
existing_host_full?: BUN_SQLITE_WGUI_HOSTS;
};
export default function AddHostForm({
existing_host,
existing_host_full,
}: Props) {
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"],
@@ -26,6 +33,8 @@ export default function AddHostForm() {
}, },
title: "new_host", title: "new_host",
async before_ready_function(params) { async before_ready_function(params) {
if (existing_host_full?.id) return;
params.setForm((prev) => ({ params.setForm((prev) => ({
...prev, ...prev,
interface: interface:
@@ -38,6 +47,8 @@ export default function AddHostForm() {
undefined, undefined,
})); }));
}, },
existing: existing_host,
existing_full: existing_host_full,
}); });
const [ipStatus, setIpStatus] = useState<HostWgIpFieldStatus>("checking"); const [ipStatus, setIpStatus] = useState<HostWgIpFieldStatus>("checking");
+90
View File
@@ -0,0 +1,90 @@
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import userAuth from "@/src/functions/backend/auth/user-auth";
import createWireguardHost from "@/src/functions/backend/setup/create-wireguard-host";
import setupWireguardHost from "@/src/functions/backend/setup/setup-wireguard-host";
import type { ApiReqParams, TableType } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
params,
) => {
const req = params.req;
const body = params.body as ApiReqParams;
if (req.method !== "POST") {
return {
success: false,
};
}
const { user, user_types } = await userAuth({ req });
if (!user?.logged_in_status) {
return {
success: false,
msg: `Unauthorized`,
logoutUser: true,
};
}
try {
const is_super_admin = checkUserAccess({ user_types });
if (!is_super_admin.success) {
return {
success: false,
msg: `Unauthorized`,
};
}
if (typeof body.host_id !== "number") {
throw new Error(`Invalid host id`);
}
const host_record_res = await BunSQLite.select<
BUN_SQLITE_WGUI_HOSTS,
TableType
>({
table: "hosts",
targetId: body.host_id,
});
const host = host_record_res.singleRes;
if (!host?.created_at) {
throw new Error(`Couldn't find host record`);
}
if (!body.update_data) {
throw new Error(`No data to update`);
}
const update_host_record = await BunSQLite.update<
BUN_SQLITE_WGUI_HOSTS,
TableType
>({
table: "hosts",
data: body.update_data,
targetId: String(host.id),
});
const update = await setupWireguardHost({
host,
user,
});
return {
success: true,
};
} catch (error: any) {
return {
success: false,
msg: error.message,
};
}
};
+1
View File
@@ -70,6 +70,7 @@ export type PagePropsType = {
is_main_host?: boolean | null; is_main_host?: boolean | null;
main_host?: BUN_SQLITE_WGUI_HOSTS | null; main_host?: BUN_SQLITE_WGUI_HOSTS | null;
next_available_client_ip?: string | null; next_available_client_ip?: string | null;
is_local_ip?: boolean | null;
}; };
export type AppContextObject = { export type AppContextObject = {