From 86eb3a0b05cc90c162b9cd11f1011cd7185dd7a0 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Sun, 13 Sep 2026 10:52:30 +0100 Subject: [PATCH] Add host setup functions and client components --- db/schema.ts | 22 ++ db/types/db.ts | 24 +- deploy/deploy-prod.sh | 2 +- src/components/general/admin-hero.tsx | 3 +- src/components/general/client-row.tsx | 35 +++ .../general/setup-main-host-button.tsx | 77 ++++++ src/components/general/status-dot.tsx | 52 ---- src/data/app-data.ts | 1 + src/dict/variables-dict.ts | 10 + .../setup/check-private-ip-availability.ts | 81 +++++++ .../setup/grab-next-available-client-ip.ts | 102 ++++++++ .../grab-next-available-private-ip-subnet.ts | 47 ++++ .../backend/setup/setup-wireguard-client.ts | 13 +- .../backend/setup/setup-wireguard-host.ts | 35 ++- src/functions/check-if-main-host-is-set.ts | 20 ++ .../(partials)/admin-aside-links-dict.tsx | 12 +- src/pages/admin/(data)/dashboard-mock-data.ts | 227 ------------------ src/pages/admin/(hooks)/use-dashboard-data.ts | 31 +++ src/pages/admin/(partials)/activity-row.tsx | 57 ----- src/pages/admin/(partials)/aside-panel.tsx | 38 --- .../admin/(partials)/distribution-row.tsx | 56 ----- src/pages/admin/(partials)/peer-row.tsx | 39 --- src/pages/admin/(partials)/sparkline.tsx | 62 ----- src/pages/admin/(partials)/stat-card.tsx | 36 +-- src/pages/admin/(partials)/traffic-chart.tsx | 130 ---------- .../admin/(sections)/activity-section.tsx | 20 -- .../admin/(sections)/kpi-hero-section.tsx | 25 +- .../(sections)/kpi-secondary-section.tsx | 47 +++- .../admin/(sections)/peers-table-section.tsx | 77 +++--- .../(sections)/traffic-chart-section.tsx | 78 ------ .../admin/clients/(data)/clients-mock-data.ts | 122 ---------- .../clients/(partials)/client-form-modal.tsx | 160 ++++++------ .../admin/clients/(partials)/client-row.tsx | 47 ---- .../(sections)/clients-table-section.tsx | 79 +++--- src/pages/admin/host/(data)/host-mock-data.ts | 33 --- .../admin/host/(partials)/host-stat-cell.tsx | 17 -- .../host/(partials)/interface-config-row.tsx | 17 -- .../host/(sections)/host-status-section.tsx | 46 ---- .../(sections)/interface-config-section.tsx | 60 ----- src/pages/admin/host/index.tsx | 33 --- .../hosts/(functions)/derive-host-config.ts | 42 ++++ .../admin/hosts/(hooks)/use-host-data.ts | 28 +++ .../admin/hosts/(partials)/host-stat-cell.tsx | 20 ++ .../hosts/(partials)/interface-config-row.tsx | 20 ++ .../(sections)/interface-config-section.tsx | 97 ++++++++ .../(sections)/main-host-status-section.tsx | 80 ++++++ src/pages/admin/hosts/index.tsx | 78 ++++++ src/pages/admin/index.tsx | 19 +- .../check-private-ip-address-availability.ts | 46 ++++ .../admin/grab-next-available-private-ip.ts | 35 +++ src/pages/api/admin/setup-main-host.ts | 52 ++++ src/types/index.ts | 2 + src/utils/format-unix-timestamp.ts | 17 ++ 53 files changed, 1284 insertions(+), 1325 deletions(-) create mode 100644 src/components/general/client-row.tsx create mode 100755 src/components/general/setup-main-host-button.tsx delete mode 100644 src/components/general/status-dot.tsx create mode 100644 src/functions/backend/setup/check-private-ip-availability.ts create mode 100644 src/functions/backend/setup/grab-next-available-client-ip.ts create mode 100644 src/functions/backend/setup/grab-next-available-private-ip-subnet.ts create mode 100644 src/functions/check-if-main-host-is-set.ts delete mode 100644 src/pages/admin/(data)/dashboard-mock-data.ts create mode 100644 src/pages/admin/(hooks)/use-dashboard-data.ts delete mode 100644 src/pages/admin/(partials)/activity-row.tsx delete mode 100644 src/pages/admin/(partials)/aside-panel.tsx delete mode 100644 src/pages/admin/(partials)/distribution-row.tsx delete mode 100644 src/pages/admin/(partials)/peer-row.tsx delete mode 100644 src/pages/admin/(partials)/sparkline.tsx delete mode 100644 src/pages/admin/(partials)/traffic-chart.tsx delete mode 100644 src/pages/admin/(sections)/activity-section.tsx delete mode 100644 src/pages/admin/(sections)/traffic-chart-section.tsx delete mode 100644 src/pages/admin/clients/(data)/clients-mock-data.ts delete mode 100644 src/pages/admin/clients/(partials)/client-row.tsx delete mode 100644 src/pages/admin/host/(data)/host-mock-data.ts delete mode 100644 src/pages/admin/host/(partials)/host-stat-cell.tsx delete mode 100644 src/pages/admin/host/(partials)/interface-config-row.tsx delete mode 100644 src/pages/admin/host/(sections)/host-status-section.tsx delete mode 100644 src/pages/admin/host/(sections)/interface-config-section.tsx delete mode 100644 src/pages/admin/host/index.tsx create mode 100644 src/pages/admin/hosts/(functions)/derive-host-config.ts create mode 100644 src/pages/admin/hosts/(hooks)/use-host-data.ts create mode 100644 src/pages/admin/hosts/(partials)/host-stat-cell.tsx create mode 100644 src/pages/admin/hosts/(partials)/interface-config-row.tsx create mode 100644 src/pages/admin/hosts/(sections)/interface-config-section.tsx create mode 100644 src/pages/admin/hosts/(sections)/main-host-status-section.tsx create mode 100644 src/pages/admin/hosts/index.tsx create mode 100644 src/pages/api/admin/check-private-ip-address-availability.ts create mode 100644 src/pages/api/admin/grab-next-available-private-ip.ts create mode 100644 src/pages/api/admin/setup-main-host.ts create mode 100644 src/utils/format-unix-timestamp.ts diff --git a/db/schema.ts b/db/schema.ts index daa4779..60da3f9 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -186,6 +186,10 @@ const schema: BUN_SQLITE_DatabaseSchemaType = { fieldName: "allowed_ips", dataType: "TEXT", }, + { + fieldName: "notes", + dataType: "TEXT", + }, ], }, { @@ -209,6 +213,24 @@ const schema: BUN_SQLITE_DatabaseSchemaType = { }, ], }, + { + tableName: "client_rules", + tableDescription: `IP table rules for this client`, + fields: [ + { + fieldName: "user_id", + dataType: "INTEGER", + }, + { + fieldName: "client_id", + dataType: "INTEGER", + }, + { + fieldName: "rule", + dataType: "TEXT", + }, + ], + }, { tableName: "variables", fields: [ diff --git a/db/types/db.ts b/db/types/db.ts index 3eb1053..53dfafe 100644 --- a/db/types/db.ts +++ b/db/types/db.ts @@ -5,6 +5,7 @@ export const BunSQLiteTables = [ "media_paradigms", "clients", "hosts", + "client_rules", "variables", ] as const @@ -113,6 +114,7 @@ export type BUN_SQLITE_WGUI_CLIENTS = { private_key?: string; public_key?: string; allowed_ips?: string; + notes?: string; } export type BUN_SQLITE_WGUI_HOSTS = { @@ -134,6 +136,24 @@ export type BUN_SQLITE_WGUI_HOSTS = { public_key?: string; } +export type BUN_SQLITE_WGUI_CLIENT_RULES = { + /** + * The unique identifier of the record. + */ + id?: number | ""; + /** + * The time when the record was created. (Unix Timestamp) + */ + created_at?: number | ""; + /** + * The time when the record was updated. (Unix Timestamp) + */ + updated_at?: number | ""; + user_id?: number | ""; + client_id?: number | ""; + rule?: string; +} + export type BUN_SQLITE_WGUI_VARIABLES = { /** * The unique identifier of the record. @@ -147,8 +167,8 @@ export type BUN_SQLITE_WGUI_VARIABLES = { * The time when the record was updated. (Unix Timestamp) */ updated_at?: number | ""; - key?: "main_host_wg_ip_address" | ""; + key?: "main_host_wg_ip_address" | "main_host_wg_public_key" | "main_host_wg_private_key" | ""; value?: string; } -export type BUN_SQLITE_WGUI_ALL_TYPEDEFS = BUN_SQLITE_WGUI_USERS & BUN_SQLITE_WGUI_USER_TYPES & BUN_SQLITE_WGUI_MEDIA & BUN_SQLITE_WGUI_MEDIA_PARADIGMS & BUN_SQLITE_WGUI_CLIENTS & BUN_SQLITE_WGUI_HOSTS & BUN_SQLITE_WGUI_VARIABLES \ No newline at end of file +export type BUN_SQLITE_WGUI_ALL_TYPEDEFS = BUN_SQLITE_WGUI_USERS & BUN_SQLITE_WGUI_USER_TYPES & BUN_SQLITE_WGUI_MEDIA & BUN_SQLITE_WGUI_MEDIA_PARADIGMS & BUN_SQLITE_WGUI_CLIENTS & BUN_SQLITE_WGUI_HOSTS & BUN_SQLITE_WGUI_CLIENT_RULES & BUN_SQLITE_WGUI_VARIABLES \ No newline at end of file diff --git a/deploy/deploy-prod.sh b/deploy/deploy-prod.sh index 7713913..6cef277 100755 --- a/deploy/deploy-prod.sh +++ b/deploy/deploy-prod.sh @@ -10,7 +10,7 @@ if [ -z "${SOURCE_PATH:-}" ] || [ "$(realpath -m "$SOURCE_PATH")" = "/" ]; then fi -SERVER_IP="$DEPLOY_SERVER_IP" +SERVER_IP="195.26.246.243" SERVER_PATH="/docker/bunext-mariadb" cd "$SOURCE_PATH" diff --git a/src/components/general/admin-hero.tsx b/src/components/general/admin-hero.tsx index 33f1e5f..e78a4f9 100644 --- a/src/components/general/admin-hero.tsx +++ b/src/components/general/admin-hero.tsx @@ -21,7 +21,7 @@ export default function AdminHero({ title, description, buttons }: Props) {
- +

{title}

{description ? ( typeof description == "string" ? ( @@ -45,7 +45,6 @@ export default function AdminHero({ title, description, buttons }: Props) {
{buttons}
-
); diff --git a/src/components/general/client-row.tsx b/src/components/general/client-row.tsx new file mode 100644 index 0000000..d6d8547 --- /dev/null +++ b/src/components/general/client-row.tsx @@ -0,0 +1,35 @@ +import Span from "@/src/components/twui/layout/Span"; +import formatUnixTimestamp from "@/src/utils/format-unix-timestamp"; +import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db"; + +type Props = { + client: BUN_SQLITE_WGUI_CLIENTS; +}; + +export default function ClientRow({ client }: Props) { + return ( + + + + {client.name || "—"} + + + + {client.wg_ip_address || "—"} + + + {client.allowed_ips || "—"} + + + + {client.public_key + ? `${client.public_key.slice(0, 16)}…` + : "—"} + + + + {formatUnixTimestamp({ timestamp: client.created_at })} + + + ); +} \ No newline at end of file diff --git a/src/components/general/setup-main-host-button.tsx b/src/components/general/setup-main-host-button.tsx new file mode 100755 index 0000000..6b15c97 --- /dev/null +++ b/src/components/general/setup-main-host-button.tsx @@ -0,0 +1,77 @@ +import { useEffect, useState, type ComponentProps } from "react"; +import Button from "../twui/layout/Button"; +import useStatus from "../twui/hooks/useStatus"; +import fetchApi from "../twui/utils/fetch/fetchApi"; +import type { ApiReqParams } from "@/src/types"; +import Row from "../twui/layout/Row"; +import Input from "../twui/form/Input"; +import Stack from "../twui/layout/Stack"; +import { Network } from "lucide-react"; +import Span from "../twui/layout/Span"; +import { AppData } from "@/src/data/app-data"; + +type Props = { + button_props?: Omit, "title">; +}; + +export default function SetupMainHostButton({ button_props }: Props) { + const { loading, setLoading, ready, setReady } = useStatus(); + + const [wgIP, setWgIP] = useState(AppData["DefaultPrivateIP"]); + + useEffect(() => { + fetchApi( + `/api/admin/check-private-ip-address-availability`, + { + method: "POST", + body: { ip_address: wgIP }, + }, + ).then((res) => { + console.log(`res`, res); + }); + }, [wgIP]); + + return ( + + { + setWgIP(v); + }} + prefix={} + suffix={ + + Selected Wireguard IP Address + + } + autoFocus + /> + + + + ); +} diff --git a/src/components/general/status-dot.tsx b/src/components/general/status-dot.tsx deleted file mode 100644 index c18d3c8..0000000 --- a/src/components/general/status-dot.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { twMerge } from "tailwind-merge"; - -export type WgStatus = "connected" | "idle" | "error"; - -type Props = { - status: WgStatus; - showLabel?: boolean; -}; - -const STATUS_META: Record< - WgStatus, - { label: string; dot: string; text: string } -> = { - connected: { - label: "Connected", - dot: "bg-success", - text: "text-success dark:text-success", - }, - idle: { - label: "Idle", - dot: "bg-warning", - text: "text-warning dark:text-warning", - }, - error: { - label: "Error", - dot: "bg-error", - text: "text-error dark:text-error", - }, -}; - -export default function StatusDot({ status, showLabel }: Props) { - const meta = STATUS_META[status]; - - return ( - - - ); -} diff --git a/src/data/app-data.ts b/src/data/app-data.ts index 313f5e8..6431e28 100644 --- a/src/data/app-data.ts +++ b/src/data/app-data.ts @@ -18,4 +18,5 @@ export const AppData = { PaystackEndpoint: "https://api.paystack.co", WireguardHostID: 0, + DefaultPrivateIP: `10.1.0.1`, } as const; diff --git a/src/dict/variables-dict.ts b/src/dict/variables-dict.ts index 9095e88..d2e7e26 100644 --- a/src/dict/variables-dict.ts +++ b/src/dict/variables-dict.ts @@ -4,4 +4,14 @@ export const Variables = [ value: "main_host_wg_ip_address", description: `Private IP address to use for the main Wireguard host. Eg. 10.1.0.1`, }, + { + title: `Main Host Wireguard Public Key`, + value: "main_host_wg_public_key", + description: `Public Key for the main host`, + }, + { + title: `Main Host Wireguard Private Key`, + value: "main_host_wg_private_key", + description: `Private Key for the main host`, + }, ] as const; diff --git a/src/functions/backend/setup/check-private-ip-availability.ts b/src/functions/backend/setup/check-private-ip-availability.ts new file mode 100644 index 0000000..7be8fff --- /dev/null +++ b/src/functions/backend/setup/check-private-ip-availability.ts @@ -0,0 +1,81 @@ +import type { APIResponseObject } from "@moduletrace/bunext/types"; +import { execSync } from "node:child_process"; + +type Params = { + ip_address: string; +}; + +/** + * Function to check the availability of a private + * IP address (like 10.1.0.1) + * @param param0 + */ +export default async function checkPrivateIPAvailability({ + ip_address, +}: Params): Promise { + const ip_pattern = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; + + const matches = ip_address.match(ip_pattern); + + if (!matches) { + return { + success: false, + msg: `Invalid IP address: ${ip_address}`, + }; + } + + const octets = matches.slice(1).map(Number); + + if (octets.some((o) => o < 0 || o > 255)) { + return { + success: false, + msg: `Invalid IP address: ${ip_address}`, + }; + } + + if (octets[3] !== 1) { + return { + success: false, + msg: `IP address must end with 1: ${ip_address}`, + }; + } + + const [octet1, octet2, octet3] = octets; + + try { + const addr_output = execSync(`ip -4 addr show`, { + encoding: "utf-8", + }); + + for (const match of addr_output.matchAll( + /inet\s+(\d+\.\d+\.\d+\.\d+)\/\d+/g, + )) { + if (!match[1]) continue; + + const [existing1, existing2, existing3] = match[1] + .split(".") + .map(Number); + + if ( + existing1 === octet1 && + existing2 === octet2 && + existing3 === octet3 + ) { + return { + success: false, + msg: `IP address ${ip_address} is already in use`, + }; + } + } + + return { + success: true, + msg: `IP address ${ip_address} is available`, + }; + } catch (error: any) { + return { + success: false, + msg: error.message, + }; + } +} \ No newline at end of file diff --git a/src/functions/backend/setup/grab-next-available-client-ip.ts b/src/functions/backend/setup/grab-next-available-client-ip.ts new file mode 100644 index 0000000..38ca4db --- /dev/null +++ b/src/functions/backend/setup/grab-next-available-client-ip.ts @@ -0,0 +1,102 @@ +import type { + BUN_SQLITE_WGUI_CLIENTS, + BUN_SQLITE_WGUI_HOSTS, + BUN_SQLITE_WGUI_VARIABLES, +} from "@/db/types/db"; +import type { APIResponseObject } from "@moduletrace/bunext/types"; +import BunSQLite from "@moduletrace/bun-sqlite"; +import type { TableType } from "@/src/types"; + +type Params = { + host?: BUN_SQLITE_WGUI_HOSTS; +}; + +/** + * Function to grab the next available client + * private IP address within the host's subnet. + * Checks the clients records and increments + * the last octet of the last client + * @param param0 + */ +export default async function grabNextAvailableClientIPAddress({ + host, +}: Params): Promise { + const variables_res = await BunSQLite.select< + BUN_SQLITE_WGUI_VARIABLES, + TableType + >({ + table: "variables", + }); + + const variables = variables_res.payload; + + const HOST_WG_IP = + host?.wg_ip_address || + variables?.find((v) => v.key == "main_host_wg_ip_address")?.value; + + if (!HOST_WG_IP) { + return { + success: false, + msg: `No Host Private IP address provided`, + }; + } + + const parts = HOST_WG_IP.split("."); + + if (parts.length !== 4) { + return { + success: false, + msg: `Invalid Host Private IP address: ${HOST_WG_IP}`, + }; + } + + const host_last_octet = Number(parts[3]); + + if (Number.isNaN(host_last_octet)) { + return { + success: false, + msg: `Invalid Host Private IP address: ${HOST_WG_IP}`, + }; + } + + const subnet = `${parts[0]}.${parts[1]}.${parts[2]}.`; + + const clients_res = await BunSQLite.select< + BUN_SQLITE_WGUI_CLIENTS, + TableType + >({ + table: "clients", + }); + + const clients = clients_res.payload || []; + + let max_last_octet = host_last_octet; + + for (const client of clients) { + const client_ip = client?.wg_ip_address; + + if (!client_ip || !client_ip.startsWith(subnet)) continue; + + const client_last_octet = Number(client_ip.split(".")[3]); + + if (Number.isNaN(client_last_octet)) continue; + + if (client_last_octet > max_last_octet) { + max_last_octet = client_last_octet; + } + } + + const next_last_octet = max_last_octet + 1; + + if (next_last_octet > 254) { + return { + success: false, + msg: `No available client IP address in subnet ${subnet}0/24`, + }; + } + + return { + success: true, + msg: `${subnet}${next_last_octet}`, + }; +} \ No newline at end of file diff --git a/src/functions/backend/setup/grab-next-available-private-ip-subnet.ts b/src/functions/backend/setup/grab-next-available-private-ip-subnet.ts new file mode 100644 index 0000000..fd65df9 --- /dev/null +++ b/src/functions/backend/setup/grab-next-available-private-ip-subnet.ts @@ -0,0 +1,47 @@ +import { AppData } from "@/src/data/app-data"; +import type { APIResponseObject } from "@moduletrace/bunext/types"; +import checkPrivateIPAvailability from "./check-private-ip-availability"; + +/** + * Function to grab the next available private + * IP subnet. Beginning with `10.1.0.1`. If not + * available then `10.2.0.1`, etc. + * @param param0 + */ +export default async function grabNextAvailablePrivateIPSubnet(): Promise { + const default_ip = AppData["DefaultPrivateIP"]; + + const parts = default_ip.split("."); + + if (parts.length !== 4) { + return { + success: false, + msg: `Invalid default private IP address: ${default_ip}`, + }; + } + + const [octet1, octet3, octet4] = [parts[0], parts[2], parts[3]]; + + const start_octet = Number(parts[1]); + const max_octet = Number.isNaN(start_octet) ? 254 : 255; + + for (let i = start_octet; i <= max_octet; i++) { + const candidate = `${octet1}.${i}.${octet3}.${octet4}`; + + const availability_res = await checkPrivateIPAvailability({ + ip_address: candidate, + }); + + if (availability_res.success) { + return { + success: true, + msg: candidate, + }; + } + } + + return { + success: false, + msg: `No available private IP subnet found`, + }; +} \ No newline at end of file diff --git a/src/functions/backend/setup/setup-wireguard-client.ts b/src/functions/backend/setup/setup-wireguard-client.ts index c849870..d5fce2d 100644 --- a/src/functions/backend/setup/setup-wireguard-client.ts +++ b/src/functions/backend/setup/setup-wireguard-client.ts @@ -25,7 +25,6 @@ const { type Params = { client: BUN_SQLITE_WGUI_CLIENTS; host?: BUN_SQLITE_WGUI_HOSTS; - variables?: BUN_SQLITE_WGUI_VARIABLES[]; }; /** @@ -38,10 +37,18 @@ type Params = { export default async function setupWireguardClient({ client, host, - variables, }: Params): Promise { const host_id = host?.id || AppData["WireguardHostID"]; + const variables_res = await BunSQLite.select< + BUN_SQLITE_WGUI_VARIABLES, + TableType + >({ + table: "variables", + }); + + const variables = variables_res.payload; + const CLIENT_WG_IP = client?.wg_ip_address; if (!CLIENT_WG_IP) { @@ -161,7 +168,7 @@ EOF encoding: "utf-8", }); - const host_setup_res = await setupWireguardHost({ host, variables }); + const host_setup_res = await setupWireguardHost({ host }); if (!host_setup_res.success) { return { diff --git a/src/functions/backend/setup/setup-wireguard-host.ts b/src/functions/backend/setup/setup-wireguard-host.ts index 5bb5381..769705f 100644 --- a/src/functions/backend/setup/setup-wireguard-host.ts +++ b/src/functions/backend/setup/setup-wireguard-host.ts @@ -11,6 +11,7 @@ import grabHostNetworkInterface from "./grab-host-network-interface"; import type { APIResponseObject } from "@moduletrace/bunext/types"; import BunSQLite from "@moduletrace/bun-sqlite"; import type { TableType } from "@/src/types"; +import checkPrivateIPAvailability from "./check-private-ip-availability"; const { WGUI_LIB_IP_TABLES_DIR, @@ -21,15 +22,24 @@ const { type Params = { host?: BUN_SQLITE_WGUI_HOSTS; - variables?: BUN_SQLITE_WGUI_VARIABLES[]; + wg_subnet_ip?: string; }; export default async function setupWireguardHost({ host, - variables, + wg_subnet_ip, }: Params): Promise { const host_id = host?.id || AppData["WireguardHostID"]; + const variables_res = await BunSQLite.select< + BUN_SQLITE_WGUI_VARIABLES, + TableType + >({ + table: "variables", + }); + + const variables = variables_res.payload; + const host_clients_res = await BunSQLite.select< BUN_SQLITE_WGUI_CLIENTS, TableType @@ -49,7 +59,8 @@ export default async function setupWireguardHost({ const TARGET_INTERFACE = await grabHostNetworkInterface(); const HOST_WG_IP = host?.wg_ip_address || - variables?.find((v) => v.key == "main_host_wg_ip_address")?.value; + variables?.find((v) => v.key == "main_host_wg_ip_address")?.value || + wg_subnet_ip; if (!HOST_WG_IP) { return { @@ -58,6 +69,17 @@ export default async function setupWireguardHost({ }; } + const is_ip_available = await checkPrivateIPAvailability({ + ip_address: HOST_WG_IP, + }); + + if (!is_ip_available.success) { + return { + success: false, + msg: `IP not available`, + }; + } + let pre_sh = ``; pre_sh += `cd ${WIREGUARD_HOST_CONFIG_DIR}\n`; @@ -67,6 +89,8 @@ export default async function setupWireguardHost({ try { const exec_pre_setup = execSync(pre_sh, { encoding: "utf-8" }); + + console.log("exec_pre_setup", exec_pre_setup); } catch (error: any) { return { success: false, @@ -155,6 +179,11 @@ export default async function setupWireguardHost({ sh += `\n`; const exec = execSync(sh, { encoding: "utf-8" }); + + return { + success: true, + msg: exec, + }; } catch (error: any) { return { success: false, diff --git a/src/functions/check-if-main-host-is-set.ts b/src/functions/check-if-main-host-is-set.ts new file mode 100644 index 0000000..1097c24 --- /dev/null +++ b/src/functions/check-if-main-host-is-set.ts @@ -0,0 +1,20 @@ +import type { BUN_SQLITE_WGUI_VARIABLES } from "@/db/types/db"; + +type Params = { + variables?: BUN_SQLITE_WGUI_VARIABLES[]; +}; + +export default function checkIfMainHostIsSet({ variables }: Params) { + const is_main_host_set = + Boolean( + variables?.find((v) => v.key == "main_host_wg_ip_address")?.value, + ) && + Boolean( + variables?.find((v) => v.key == "main_host_wg_private_key")?.value, + ) && + Boolean( + variables?.find((v) => v.key == "main_host_wg_public_key")?.value, + ); + + return is_main_host_set; +} diff --git a/src/layouts/admin/(partials)/admin-aside-links-dict.tsx b/src/layouts/admin/(partials)/admin-aside-links-dict.tsx index 58026b0..46b8631 100644 --- a/src/layouts/admin/(partials)/admin-aside-links-dict.tsx +++ b/src/layouts/admin/(partials)/admin-aside-links-dict.tsx @@ -43,16 +43,16 @@ export default function AdminAsideLinks() { }, { component: sectionLabel("WireGuard") }, { - title: "Clients", - url: "/admin/clients", + title: "Hosts", + url: "/admin/hosts", icon: ( - + ), }, { - title: "Host", - url: "/admin/host", - icon: , + title: "Clients", + url: "/admin/clients", + icon: , }, { component: sectionLabel("Account") }, { diff --git a/src/pages/admin/(data)/dashboard-mock-data.ts b/src/pages/admin/(data)/dashboard-mock-data.ts deleted file mode 100644 index 3ef070f..0000000 --- a/src/pages/admin/(data)/dashboard-mock-data.ts +++ /dev/null @@ -1,227 +0,0 @@ -export type DeltaTone = "positive" | "negative" | "neutral"; - -export type PeerStatus = "connected" | "idle" | "error"; - -export type DashboardPeer = { - id: string; - name: string; - tunnelIp: string; - endpoint: string; - sent: string; - received: string; - lastHandshake: string; - status: PeerStatus; -}; - -export type TrafficPoint = { - label: string; - up: number; - down: number; -}; - -export type ActivityKind = "peer" | "client" | "config" | "auth" | "system"; - -export type ActivityEvent = { - id: string; - kind: ActivityKind; - title: string; - detail: string; - time: string; -}; - -export type SecondaryKpi = { - id: string; - label: string; - value: string; - delta: string; - deltaTone: DeltaTone; -}; - -export const HERO_KPI = { - label: "Connected peers", - value: "42", - delta: "+6", - deltaTone: "positive" as DeltaTone, - subtext: "since yesterday", - sparkline: [ - 24, 26, 25, 28, 31, 30, 33, 36, 35, 38, 37, 39, 41, 40, 42, 44, 43, 41, - 42, 40, 41, 42, 42, 42, - ], -}; - -export const SECONDARY_KPIS: SecondaryKpi[] = [ - { - id: "k-total", - label: "Total clients", - value: "54", - delta: "+3 this week", - deltaTone: "positive", - }, - { - id: "k-traffic", - label: "Data transferred · 24h", - value: "61.4 GB", - delta: "+12.4%", - deltaTone: "positive", - }, - { - id: "k-status", - label: "Server status", - value: "Operational", - delta: "Uptime 99.98%", - deltaTone: "neutral", - }, -]; - -export const TRAFFIC_SERIES: TrafficPoint[] = [ - { label: "00", up: 0.3, down: 0.9 }, - { label: "01", up: 0.2, down: 0.7 }, - { label: "02", up: 0.2, down: 0.6 }, - { label: "03", up: 0.2, down: 0.5 }, - { label: "04", up: 0.3, down: 0.8 }, - { label: "05", up: 0.5, down: 1.2 }, - { label: "06", up: 0.8, down: 1.9 }, - { label: "07", up: 1.1, down: 2.6 }, - { label: "08", up: 1.3, down: 3.1 }, - { label: "09", up: 1.2, down: 2.9 }, - { label: "10", up: 1.1, down: 2.7 }, - { label: "11", up: 1.2, down: 2.8 }, - { label: "12", up: 1.4, down: 3.2 }, - { label: "13", up: 1.3, down: 3.0 }, - { label: "14", up: 1.2, down: 2.8 }, - { label: "15", up: 1.3, down: 3.1 }, - { label: "16", up: 1.5, down: 3.4 }, - { label: "17", up: 1.7, down: 3.8 }, - { label: "18", up: 1.8, down: 4.0 }, - { label: "19", up: 1.6, down: 3.6 }, - { label: "20", up: 1.4, down: 3.3 }, - { label: "21", up: 1.2, down: 2.9 }, - { label: "22", up: 0.9, down: 2.2 }, - { label: "23", up: 0.5, down: 1.4 }, -]; - -export const PEERS: DashboardPeer[] = [ - { - id: "p-01", - name: "MacBook Pro", - tunnelIp: "10.0.0.2", - endpoint: "81.2.69.142:51820", - sent: "12.4 GB", - received: "48.2 GB", - lastHandshake: "just now", - status: "connected", - }, - { - id: "p-02", - name: "iPhone 15", - tunnelIp: "10.0.0.3", - endpoint: "92.28.211.234:51820", - sent: "3.1 GB", - received: "11.7 GB", - lastHandshake: "1m ago", - status: "connected", - }, - { - id: "p-03", - name: "Home Server", - tunnelIp: "10.0.0.4", - endpoint: "10.0.0.4:51820", - sent: "220.8 GB", - received: "84.3 GB", - lastHandshake: "4m ago", - status: "connected", - }, - { - id: "p-04", - name: "Office Desktop", - tunnelIp: "10.0.0.5", - endpoint: "77.111.247.28:51820", - sent: "18.9 GB", - received: "32.5 GB", - lastHandshake: "22m ago", - status: "connected", - }, - { - id: "p-05", - name: "Galaxy S24", - tunnelIp: "10.0.0.6", - endpoint: "151.101.1.69:51820", - sent: "1.2 GB", - received: "6.8 GB", - lastHandshake: "1h ago", - status: "idle", - }, - { - id: "p-06", - name: "iPad", - tunnelIp: "10.0.0.7", - endpoint: "89.187.168.36:51820", - sent: "0.9 GB", - received: "4.1 GB", - lastHandshake: "3h ago", - status: "idle", - }, - { - id: "p-07", - name: "Old Laptop", - tunnelIp: "10.0.0.8", - endpoint: "192.168.1.23:51820", - sent: "0.0 GB", - received: "0.0 GB", - lastHandshake: "3d ago", - status: "error", - }, -]; - -export const DISTRIBUTION: { label: string; value: number; tone: "success" | "warning" | "error" }[] = [ - { label: "Connected", value: 42, tone: "success" }, - { label: "Idle", value: 9, tone: "warning" }, - { label: "Error", value: 3, tone: "error" }, -]; - -export const DISTRIBUTION_TOTAL = 54; - -export const ACTIVITY: ActivityEvent[] = [ - { - id: "a-01", - kind: "peer", - title: "MacBook Pro connected", - detail: "10.0.0.2 · handshake ok", - time: "2m ago", - }, - { - id: "a-02", - kind: "client", - title: "New client added", - detail: "Galaxy S24 · 10.0.0.6", - time: "1h ago", - }, - { - id: "a-03", - kind: "config", - title: "Config generated", - detail: "galaxy-s24.conf · sent to owner", - time: "1h ago", - }, - { - id: "a-04", - kind: "auth", - title: "Admin sign-in", - detail: "admin@wgui.local", - time: "3h ago", - }, - { - id: "a-05", - kind: "system", - title: "Service restarted", - detail: "wg0 interface · took 0.8s", - time: "5h ago", - }, - { - id: "a-06", - kind: "peer", - title: "Old Laptop handshake failed", - detail: "10.0.0.8 · key mismatch", - time: "3d ago", - }, -]; diff --git a/src/pages/admin/(hooks)/use-dashboard-data.ts b/src/pages/admin/(hooks)/use-dashboard-data.ts new file mode 100644 index 0000000..4016d87 --- /dev/null +++ b/src/pages/admin/(hooks)/use-dashboard-data.ts @@ -0,0 +1,31 @@ +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 useDashboardData() { + const clients_res = useAdminCrudGet({ + table: "clients", + sql_query: { + order: { field: "created_at", strategy: "DESC" }, + limit: 100, + }, + }); + + const hosts_res = useAdminCrudGet({ + table: "hosts", + limit: 100, + }); + + const variables_res = useAdminCrudGet({ + table: "variables", + }); + + return { + clients: clients_res.res, + hosts: hosts_res.res, + variables: variables_res.res, + }; +} \ No newline at end of file diff --git a/src/pages/admin/(partials)/activity-row.tsx b/src/pages/admin/(partials)/activity-row.tsx deleted file mode 100644 index 57c5315..0000000 --- a/src/pages/admin/(partials)/activity-row.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { twMerge } from "tailwind-merge"; -import type { ActivityEvent } from "../(data)/dashboard-mock-data"; -import type { LucideIcon } from "lucide-react"; -import { Cable, FileDown, LogIn, RefreshCw, UserPlus } from "lucide-react"; - -type Props = { - event: ActivityEvent; -}; - -const KIND_META: Record< - ActivityEvent["kind"], - { Icon: LucideIcon; className: string } -> = { - peer: { - Icon: Cable, - className: "text-secondary dark:text-secondary", - }, - client: { Icon: UserPlus, className: "text-primary" }, - config: { - Icon: FileDown, - className: "text-link dark:text-link-dark", - }, - auth: { - Icon: LogIn, - className: "text-foreground-light/50 dark:text-foreground-dark/50", - }, - system: { Icon: RefreshCw, className: "text-warning dark:text-warning" }, -}; - -export default function ActivityRow({ event }: Props) { - const meta = KIND_META[event.kind]; - - return ( -
  • - - - -
    -

    - {event.title} -

    -

    - {event.detail} -

    -
    - - {event.time} - -
  • - ); -} diff --git a/src/pages/admin/(partials)/aside-panel.tsx b/src/pages/admin/(partials)/aside-panel.tsx deleted file mode 100644 index 50f8bdf..0000000 --- a/src/pages/admin/(partials)/aside-panel.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FileDown, Plus, RefreshCw } from "lucide-react"; -import AdminButton from "@/src/components/general/admin-button"; -import AdminCard from "@/src/components/general/admin-card"; -import DistributionRow from "./distribution-row"; -import { DISTRIBUTION, DISTRIBUTION_TOTAL } from "../(data)/dashboard-mock-data"; - -export default function AsidePanel() { - return ( - -
    -

    - Connection distribution -

    -
    - {DISTRIBUTION.map((d) => ( - - ))} -
    -
    -
    -

    - Quick actions -

    - - Add client - - Generate config - Restart service -
    -
    - ); -} diff --git a/src/pages/admin/(partials)/distribution-row.tsx b/src/pages/admin/(partials)/distribution-row.tsx deleted file mode 100644 index 736a07c..0000000 --- a/src/pages/admin/(partials)/distribution-row.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { twMerge } from "tailwind-merge"; - -type Props = { - label: string; - value: number; - total: number; - tone: "success" | "warning" | "error"; -}; - -const BAR_TONE: Record = { - success: "bg-success", - warning: "bg-warning", - error: "bg-error", -}; - -const TEXT_TONE: Record = { - success: "text-success dark:text-success", - warning: "text-warning dark:text-warning", - error: "text-error dark:text-error", -}; - -export default function DistributionRow({ - label, - value, - total, - tone, -}: Props) { - const pct = Math.round((value / total) * 100); - - return ( -
    -
    - - {label} - - - {value} - -
    -
    -
    -
    -
    - ); -} diff --git a/src/pages/admin/(partials)/peer-row.tsx b/src/pages/admin/(partials)/peer-row.tsx deleted file mode 100644 index 4e510c6..0000000 --- a/src/pages/admin/(partials)/peer-row.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import StatusDot from "@/src/components/general/status-dot"; -import type { DashboardPeer } from "../(data)/dashboard-mock-data"; - -type Props = { - peer: DashboardPeer; -}; - -export default function PeerRow({ peer }: Props) { - return ( - - -
    - - - {peer.name} - -
    - - - {peer.tunnelIp} - - - {peer.endpoint} - - - - ↓ {peer.received} - - - {" "} - ↑ {peer.sent} - - - - {peer.lastHandshake} - - - ); -} diff --git a/src/pages/admin/(partials)/sparkline.tsx b/src/pages/admin/(partials)/sparkline.tsx deleted file mode 100644 index 07ac224..0000000 --- a/src/pages/admin/(partials)/sparkline.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { twMerge } from "tailwind-merge"; - -type Props = { - data: number[]; - width?: number; - height?: number; - className?: string; -}; - -export default function Sparkline({ - data, - width = 120, - height = 44, - className, -}: Props) { - const min = Math.min(...data); - const max = Math.max(...data); - const range = max - min || 1; - const stepX = width / (data.length - 1); - const pad = 3; - - const points: [number, number][] = data.map((value, index) => [ - index * stepX, - height - pad - ((value - min) / range) * (height - pad * 2), - ]); - - const linePath = points - .map( - ([x, y], index) => - `${index == 0 ? "M" : "L"}${x.toFixed(2)},${y.toFixed(2)}`, - ) - .join(" "); - - const areaPath = `${linePath} L${width},${height} L0,${height} Z`; - const last = points[points.length - 1]!; - - return ( - - ); -} diff --git a/src/pages/admin/(partials)/stat-card.tsx b/src/pages/admin/(partials)/stat-card.tsx index 5202cac..5939453 100644 --- a/src/pages/admin/(partials)/stat-card.tsx +++ b/src/pages/admin/(partials)/stat-card.tsx @@ -1,7 +1,10 @@ import type { ReactNode } from "react"; import { twMerge } from "tailwind-merge"; import AdminCard from "@/src/components/general/admin-card"; -import type { DeltaTone } from "../(data)/dashboard-mock-data"; +import Span from "@/src/components/twui/layout/Span"; +import Stack from "@/src/components/twui/layout/Stack"; + +type DeltaTone = "positive" | "negative" | "neutral"; type Props = { label: string; @@ -35,11 +38,11 @@ export default function StatCard({ tier={tier} className="p-4 flex items-start justify-between gap-4" > -
    - + + {label} - - + {value} - + {delta || subtext ? ( - {delta} {delta && subtext ? ( - - {" "} - · {subtext} - + + {" "}· {subtext} + ) : null} {!delta && subtext ? ( - + {subtext} - + ) : null} - + ) : null} -
    + {trailing ? ( -
    {trailing}
    + {trailing} ) : null} ); -} +} \ No newline at end of file diff --git a/src/pages/admin/(partials)/traffic-chart.tsx b/src/pages/admin/(partials)/traffic-chart.tsx deleted file mode 100644 index db136e3..0000000 --- a/src/pages/admin/(partials)/traffic-chart.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { useMemo } from "react"; -import type { TrafficPoint } from "../(data)/dashboard-mock-data"; - -type Props = { - data: TrafficPoint[]; -}; - -const W = 800; -const H = 240; -const PAD_L = 46; -const PAD_R = 12; -const PAD_T = 12; -const PAD_B = 28; - -function formatAxis(value: number) { - return value >= 1 ? `${value}G` : `${Math.round(value * 1000)}M`; -} - -export default function TrafficChart({ data }: Props) { - const { max, downPath, downArea, upPath, yTicks, xTicks } = useMemo(() => { - const plotW = W - PAD_L - PAD_R; - const plotH = H - PAD_T - PAD_B; - const max = Math.ceil( - Math.max(...data.flatMap((p) => [p.up, p.down])), - ); - - const x = (i: number) => PAD_L + (i / (data.length - 1)) * plotW; - const y = (v: number) => PAD_T + (1 - v / max) * plotH; - - const toPath = (key: "up" | "down") => - data - .map( - (p, i) => - `${i == 0 ? "M" : "L"}${x(i).toFixed(2)},${y( - p[key], - ).toFixed(2)}`, - ) - .join(" "); - - const downPath = toPath("down"); - const upPath = toPath("up"); - const baseY = (H - PAD_B).toFixed(2); - const downArea = `${downPath} L${x(data.length - 1).toFixed( - 2, - )},${baseY} L${PAD_L},${baseY} Z`; - - const yTicks = Array.from({ length: 5 }, (_, i) => { - const v = (max / 4) * i; - return { v, label: formatAxis(v), y: y(v) }; - }); - - const xTicks: { label: string; x: number }[] = []; - for (let i = 0; i < data.length; i += 4) { - xTicks.push({ label: data[i]!.label, x: x(i) }); - } - - return { max, downPath, downArea, upPath, yTicks, xTicks }; - }, [data]); - - return ( - - - - - - - - - - - - - {yTicks.map((tick, i) => ( - - - - {tick.label} - - - ))} - - - - - - {xTicks.map((tick, i) => ( - - {tick.label} - - ))} - - ); -} diff --git a/src/pages/admin/(sections)/activity-section.tsx b/src/pages/admin/(sections)/activity-section.tsx deleted file mode 100644 index d6bf3f7..0000000 --- a/src/pages/admin/(sections)/activity-section.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import AdminCard from "@/src/components/general/admin-card"; -import ActivityRow from "../(partials)/activity-row"; -import { ACTIVITY } from "../(data)/dashboard-mock-data"; - -export default function ActivitySection() { - return ( - -
    -

    - Recent activity -

    -
    -
      - {ACTIVITY.map((event) => ( - - ))} -
    -
    - ); -} diff --git a/src/pages/admin/(sections)/kpi-hero-section.tsx b/src/pages/admin/(sections)/kpi-hero-section.tsx index 0e57e73..6aa0acb 100644 --- a/src/pages/admin/(sections)/kpi-hero-section.tsx +++ b/src/pages/admin/(sections)/kpi-hero-section.tsx @@ -1,23 +1,18 @@ import StatCard from "../(partials)/stat-card"; -import Sparkline from "../(partials)/sparkline"; -import { HERO_KPI } from "../(data)/dashboard-mock-data"; +import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db"; -export default function KpiHeroSection() { +type Props = { + clients?: BUN_SQLITE_WGUI_CLIENTS[]; +}; + +export default function KpiHeroSection({ clients }: Props) { return ( - } /> ); -} +} \ No newline at end of file diff --git a/src/pages/admin/(sections)/kpi-secondary-section.tsx b/src/pages/admin/(sections)/kpi-secondary-section.tsx index 99e0568..246805a 100644 --- a/src/pages/admin/(sections)/kpi-secondary-section.tsx +++ b/src/pages/admin/(sections)/kpi-secondary-section.tsx @@ -1,18 +1,51 @@ import StatCard from "../(partials)/stat-card"; -import { SECONDARY_KPIS } from "../(data)/dashboard-mock-data"; +import type { + BUN_SQLITE_WGUI_CLIENTS, + BUN_SQLITE_WGUI_HOSTS, + BUN_SQLITE_WGUI_VARIABLES, +} from "@/db/types/db"; + +type Props = { + clients?: BUN_SQLITE_WGUI_CLIENTS[]; + hosts?: BUN_SQLITE_WGUI_HOSTS[]; + variables?: BUN_SQLITE_WGUI_VARIABLES[]; +}; + +export default function KpiSecondarySection({ + clients, + hosts, + variables, +}: Props) { + const kpis = [ + { + key: "k-clients", + label: "Total clients", + value: String(clients?.length || 0), + }, + { + key: "k-hosts", + label: "Total hosts", + value: String(hosts?.length || 0), + }, + { + key: "k-host-address", + label: "Main host address", + value: + variables?.find( + (v) => v.key == "main_host_wg_ip_address", + )?.value || "—", + }, + ]; -export default function KpiSecondarySection() { return (
    - {SECONDARY_KPIS.map((kpi) => ( + {kpis.map((kpi) => ( ))}
    ); -} +} \ No newline at end of file diff --git a/src/pages/admin/(sections)/peers-table-section.tsx b/src/pages/admin/(sections)/peers-table-section.tsx index a44d0fb..8b043a6 100644 --- a/src/pages/admin/(sections)/peers-table-section.tsx +++ b/src/pages/admin/(sections)/peers-table-section.tsx @@ -1,9 +1,16 @@ import { ArrowUpRight } from "lucide-react"; import Link from "@/src/components/twui/layout/Link"; +import H2 from "@/src/components/twui/layout/H2"; +import Row from "@/src/components/twui/layout/Row"; +import Stack from "@/src/components/twui/layout/Stack"; +import Span from "@/src/components/twui/layout/Span"; import AdminCard from "@/src/components/general/admin-card"; -import PeerRow from "../(partials)/peer-row"; -import AsidePanel from "../(partials)/aside-panel"; -import { PEERS } from "../(data)/dashboard-mock-data"; +import ClientRow from "@/src/components/general/client-row"; +import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db"; + +type Props = { + clients?: BUN_SQLITE_WGUI_CLIENTS[]; +}; const thClass = "px-4 py-2 text-left text-[11px] font-semibold uppercase tracking-[0.08em] " + @@ -11,42 +18,54 @@ const thClass = const thRightClass = `${thClass} text-right`; -export default function PeersTableSection() { +const RECENT_LIMIT = 8; + +export default function PeersTableSection({ clients }: Props) { + const recent = (clients || []).slice(0, RECENT_LIMIT); + return ( -
    - -
    -

    - Recent peers -

    - - View all - - -
    + + +

    + Recent peers +

    + + View all + + +
    + {recent.length ? (
    - +
    - + - - - + + + - {PEERS.map((peer) => ( - + {recent.map((client) => ( + ))}
    PeerClient Tunnel IPEndpointTrafficLast handshakeAllowed IPsPublic keyCreated
    -
    - -
    + ) : ( + + + No clients yet + + + Manage peers from the Clients page + + + )} + ); -} +} \ No newline at end of file diff --git a/src/pages/admin/(sections)/traffic-chart-section.tsx b/src/pages/admin/(sections)/traffic-chart-section.tsx deleted file mode 100644 index 18cf4c0..0000000 --- a/src/pages/admin/(sections)/traffic-chart-section.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useMemo } from "react"; -import AdminCard from "@/src/components/general/admin-card"; -import TrafficChart from "../(partials)/traffic-chart"; -import { TRAFFIC_SERIES } from "../(data)/dashboard-mock-data"; - -function formatGb(value: number) { - return `${value.toFixed(1)} GB`; -} - -export default function TrafficChartSection() { - const footer = useMemo(() => { - const maxDown = Math.max(...TRAFFIC_SERIES.map((p) => p.down)); - const maxUp = Math.max(...TRAFFIC_SERIES.map((p) => p.up)); - const avgDown = - TRAFFIC_SERIES.reduce((sum, p) => sum + p.down, 0) / - TRAFFIC_SERIES.length; - const total = - TRAFFIC_SERIES.reduce((sum, p) => sum + p.down + p.up, 0); - return { - maxDown: formatGb(maxDown), - maxUp: formatGb(maxUp), - avgDown: formatGb(avgDown), - total: formatGb(total), - }; - }, []); - - const stats = [ - { label: "Peak download", value: footer.maxDown }, - { label: "Peak upload", value: footer.maxUp }, - { label: "Avg download", value: footer.avgDown }, - { label: "Total · 24h", value: footer.total }, - ]; - - return ( - -
    -
    -

    - Network traffic -

    -

    - Last 24 hours -

    -
    -
    - - - Download - - - - Upload - -
    -
    -
    - -
    -
    - {stats.map((stat, i) => ( -
    - {i > 0 ? ( - - ) : null} -
    - - {stat.label} - - - {stat.value} - -
    -
    - ))} -
    -
    - ); -} diff --git a/src/pages/admin/clients/(data)/clients-mock-data.ts b/src/pages/admin/clients/(data)/clients-mock-data.ts deleted file mode 100644 index ee16ccd..0000000 --- a/src/pages/admin/clients/(data)/clients-mock-data.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { WgStatus } from "@/src/components/general/status-dot"; - -export type ClientRecord = { - id: string; - name: string; - tunnelIp: string; - allowedIps: string; - publicKey: string; - endpoint: string; - sent: string; - received: string; - lastHandshake: string; - status: WgStatus; - createdAt: string; -}; - -export const CLIENTS: ClientRecord[] = [ - { - id: "c-01", - name: "MacBook Pro", - tunnelIp: "10.0.0.2", - allowedIps: "10.0.0.2/32", - publicKey: "gQ1k4Lv9MxWn7Vp2RzHsTb8FcJdKqY3eWa", - endpoint: "81.2.69.142:51820", - sent: "12.4 GB", - received: "48.2 GB", - lastHandshake: "just now", - status: "connected", - createdAt: "Jan 12, 2026", - }, - { - id: "c-02", - name: "iPhone 15", - tunnelIp: "10.0.0.3", - allowedIps: "10.0.0.3/32", - publicKey: "aZ9xN4cM6vBq2wEs8rT7yU1iO5pLkDfGhJ", - endpoint: "92.28.211.234:51820", - sent: "3.1 GB", - received: "11.7 GB", - lastHandshake: "1m ago", - status: "connected", - createdAt: "Feb 3, 2026", - }, - { - id: "c-03", - name: "Home Server", - tunnelIp: "10.0.0.4", - allowedIps: "10.0.0.4/32, 10.0.0.0/24", - publicKey: "qW5eRt7yU8iO9pL0kM2nB3vC4xD5fG6hJ7k", - endpoint: "10.0.0.4:51820", - sent: "220.8 GB", - received: "84.3 GB", - lastHandshake: "4m ago", - status: "connected", - createdAt: "Nov 21, 2025", - }, - { - id: "c-04", - name: "Office Desktop", - tunnelIp: "10.0.0.5", - allowedIps: "10.0.0.5/32", - publicKey: "zC1vB2nM3kL4jH5gF6dS7aA8sD9fG1hJ2kL", - endpoint: "77.111.247.28:51820", - sent: "18.9 GB", - received: "32.5 GB", - lastHandshake: "22m ago", - status: "connected", - createdAt: "Dec 9, 2025", - }, - { - id: "c-05", - name: "Galaxy S24", - tunnelIp: "10.0.0.6", - allowedIps: "10.0.0.6/32", - publicKey: "pL0kM2nB3vC4xD5fG6hJ7kQ8wE9rT1yU2iO", - endpoint: "151.101.1.69:51820", - sent: "1.2 GB", - received: "6.8 GB", - lastHandshake: "1h ago", - status: "idle", - createdAt: "Feb 20, 2026", - }, - { - id: "c-06", - name: "iPad", - tunnelIp: "10.0.0.7", - allowedIps: "10.0.0.7/32", - publicKey: "vB2nM3kL4jH5gF6dS7aA8sD9fG1hJ2kLqW", - endpoint: "89.187.168.36:51820", - sent: "0.9 GB", - received: "4.1 GB", - lastHandshake: "3h ago", - status: "idle", - createdAt: "Mar 2, 2026", - }, - { - id: "c-07", - name: "Old Laptop", - tunnelIp: "10.0.0.8", - allowedIps: "10.0.0.8/32", - publicKey: "nM3kL4jH5gF6dS7aA8sD9fG1hJ2kLqW5eR", - endpoint: "192.168.1.23:51820", - sent: "0.0 GB", - received: "0.0 GB", - lastHandshake: "3d ago", - status: "error", - createdAt: "Aug 17, 2025", - }, - { - id: "c-08", - name: "Travel Router", - tunnelIp: "10.0.0.9", - allowedIps: "10.0.0.9/32", - publicKey: "bV3cX4dZ5eA6sD7fG8hJ9kQ0wE1rT2yU3iO", - endpoint: "203.0.113.19:51820", - sent: "6.7 GB", - received: "21.9 GB", - lastHandshake: "2h ago", - status: "connected", - createdAt: "Jan 30, 2026", - }, -]; diff --git a/src/pages/admin/clients/(partials)/client-form-modal.tsx b/src/pages/admin/clients/(partials)/client-form-modal.tsx index ad137ad..3defb7b 100644 --- a/src/pages/admin/clients/(partials)/client-form-modal.tsx +++ b/src/pages/admin/clients/(partials)/client-form-modal.tsx @@ -1,12 +1,20 @@ -import type { Dispatch, ReactNode, SetStateAction } from "react"; +import { useState, type Dispatch, type ReactNode, type SetStateAction } from "react"; import { X } from "lucide-react"; import Modal from "@/src/components/twui/elements/Modal"; import Input from "@/src/components/twui/form/Input"; import AdminButton from "@/src/components/general/admin-button"; +import Form from "@/src/components/twui/form/Form"; +import Stack from "@/src/components/twui/layout/Stack"; +import Span from "@/src/components/twui/layout/Span"; +import H3 from "@/src/components/twui/layout/H3"; +import Row from "@/src/components/twui/layout/Row"; +import adminCrudHandler from "@/src/functions/frontend/admin-crud-handler"; +import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db"; type Props = { open: boolean; setOpen: Dispatch>; + onCreated?: () => void; }; type FormFieldProps = { @@ -15,94 +23,104 @@ type FormFieldProps = { children: ReactNode; }; -function FormField({ label, htmlFor, children }: FormFieldProps) { - return ( -
    - - {children} -
    - ); -} +type FormData = { + client_name?: string; + tunnel_ip?: string; + allowed_ips?: string; +}; + +export default function ClientFormModal({ open, setOpen, onCreated }: Props) { + const [busy, setBusy] = useState(false); + const [error, setError] = useState(); + + async function handleSubmit(data: FormData) { + setBusy(true); + setError(undefined); + + const res = await adminCrudHandler({ + action: "insert", + table: "clients", + insert_data: [ + { + name: data.client_name || "", + wg_ip_address: data.tunnel_ip || "", + allowed_ips: data.allowed_ips || "", + }, + ], + }); + + setBusy(false); + + if (!res.success) { + setError(res.msg || "Could not add client"); + return; + } + + setOpen(false); + onCreated?.(); + } -export default function ClientFormModal({ open, setOpen }: Props) { return ( - -
    -
    -

    - Add client -

    -

    - A WireGuard config will be generated on save -

    -
    - -
    -
    { - e.preventDefault(); - setOpen(false); + + +

    + Add client +

    + + The client is added to your WireGuard network + +
    + + { + handleSubmit(data as FormData); }} > - + - -
    - + - - - -
    - - - -
    - setOpen(false)}> - Cancel - - - Add client - -
    - + + {error ? ( + + {error} + + ) : null} + + setOpen(false)} disabled={busy}> + Cancel + + + {busy ? "Adding…" : "Add client"} + + + +
    ); -} +} \ No newline at end of file diff --git a/src/pages/admin/clients/(partials)/client-row.tsx b/src/pages/admin/clients/(partials)/client-row.tsx deleted file mode 100644 index c74bcf7..0000000 --- a/src/pages/admin/clients/(partials)/client-row.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import StatusDot from "@/src/components/general/status-dot"; -import type { ClientRecord } from "../(data)/clients-mock-data"; - -type Props = { - client: ClientRecord; -}; - -export default function ClientRow({ client }: Props) { - return ( - - -
    - - - {client.name} - -
    - - - {client.tunnelIp} - - - {client.allowedIps} - - - - {client.publicKey.slice(0, 16)}… - - - - - ↓ {client.received} - - - {" "} - ↑ {client.sent} - - - - {client.lastHandshake} - - - {client.createdAt} - - - ); -} diff --git a/src/pages/admin/clients/(sections)/clients-table-section.tsx b/src/pages/admin/clients/(sections)/clients-table-section.tsx index 3cdcdd4..526ea23 100644 --- a/src/pages/admin/clients/(sections)/clients-table-section.tsx +++ b/src/pages/admin/clients/(sections)/clients-table-section.tsx @@ -1,9 +1,15 @@ import { useMemo, useState, type Dispatch, type SetStateAction } from "react"; import Search from "@/src/components/twui/elements/Search"; +import Loading from "@/src/components/twui/elements/Loading"; import AdminCard from "@/src/components/general/admin-card"; -import ClientRow from "../(partials)/client-row"; +import ClientRow from "@/src/components/general/client-row"; +import H2 from "@/src/components/twui/layout/H2"; +import Span from "@/src/components/twui/layout/Span"; +import Row from "@/src/components/twui/layout/Row"; +import Stack from "@/src/components/twui/layout/Stack"; import ClientFormModal from "../(partials)/client-form-modal"; -import { CLIENTS } from "../(data)/clients-mock-data"; +import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get"; +import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db"; type Props = { addOpen: boolean; @@ -19,25 +25,36 @@ const thRightClass = `${thClass} text-right`; export default function ClientsTableSection({ addOpen, setAddOpen }: Props) { const [query, setQuery] = useState(""); + const { res, setRes } = useAdminCrudGet({ + table: "clients", + sql_query: { + order: { field: "created_at", strategy: "DESC" }, + limit: 100, + }, + }); + const filtered = useMemo(() => { + const clients = res || []; const q = query.trim().toLowerCase(); - if (!q) return CLIENTS; - return CLIENTS.filter((client) => - [client.name, client.tunnelIp, client.allowedIps].some((value) => - value.toLowerCase().includes(q), + if (!q) { + return clients; + } + return clients.filter((client) => + [client.name, client.wg_ip_address, client.allowed_ips].some( + (value) => (value || "").toLowerCase().includes(q), ), ); - }, [query]); + }, [res, query]); return ( -
    -

    + +

    Clients - - {filtered.length} - -

    + + {String(filtered.length)} + +

    -
    - {filtered.length ? ( + + {res === undefined ? ( + + + + ) : filtered.length ? (
    @@ -60,8 +81,6 @@ export default function ClientsTableSection({ addOpen, setAddOpen }: Props) { - - @@ -73,16 +92,22 @@ export default function ClientsTableSection({ addOpen, setAddOpen }: Props) {
    Tunnel IP Allowed IPs Public keyTrafficLast handshake Created
    ) : ( -
    - - No clients found - - - Try adjusting your search terms - -
    + + + {query ? "No clients found" : "No clients yet"} + + + {query + ? "Try adjusting your search terms" + : "Add a client to get started"} + + )} - + setRes(undefined)} + />
    ); -} +} \ No newline at end of file diff --git a/src/pages/admin/host/(data)/host-mock-data.ts b/src/pages/admin/host/(data)/host-mock-data.ts deleted file mode 100644 index 9c9a08d..0000000 --- a/src/pages/admin/host/(data)/host-mock-data.ts +++ /dev/null @@ -1,33 +0,0 @@ -export type InterfaceConfigRow = { - keyName: string; - value: string; -}; - -export const HOST_STATUS = { - state: "running" as const, - version: "v0.2.0", - uptime: "37d 14h 22m", - activePeers: 42, - handshakesPerSec: 0.4, - rxBytes: "1.2 TB", - txBytes: "420 GB", -}; - -export const HOST_INTERFACE = { - name: "wg0", - address: "10.0.0.1/24", - listenPort: 51820, - mtu: 1420, - dns: "1.1.1.1, 1.0.0.1", - publicKey: "sH8pZ0vN3mQ6wE9rT2yU5iO8pL1kM4nB7vC0xD3fG6hJ9kL", - endpoint: "203.0.113.10:51820", - configPath: "/etc/wireguard/wg0.conf", -}; - -export const INTERFACE_CONFIG: InterfaceConfigRow[] = [ - { keyName: "Address", value: HOST_INTERFACE.address }, - { keyName: "ListenPort", value: String(HOST_INTERFACE.listenPort) }, - { keyName: "PrivateKey", value: "•••••••••••• (redacted)" }, - { keyName: "MTU", value: String(HOST_INTERFACE.mtu) }, - { keyName: "DNS", value: HOST_INTERFACE.dns }, -]; diff --git a/src/pages/admin/host/(partials)/host-stat-cell.tsx b/src/pages/admin/host/(partials)/host-stat-cell.tsx deleted file mode 100644 index bde8826..0000000 --- a/src/pages/admin/host/(partials)/host-stat-cell.tsx +++ /dev/null @@ -1,17 +0,0 @@ -type Props = { - label: string; - value: string; -}; - -export default function HostStatCell({ label, value }: Props) { - return ( -
    - - {label} - - - {value} - -
    - ); -} diff --git a/src/pages/admin/host/(partials)/interface-config-row.tsx b/src/pages/admin/host/(partials)/interface-config-row.tsx deleted file mode 100644 index c057953..0000000 --- a/src/pages/admin/host/(partials)/interface-config-row.tsx +++ /dev/null @@ -1,17 +0,0 @@ -type Props = { - keyName: string; - value: string; -}; - -export default function InterfaceConfigRow({ keyName, value }: Props) { - return ( -
    - - {keyName} = - - - {value} - -
    - ); -} diff --git a/src/pages/admin/host/(sections)/host-status-section.tsx b/src/pages/admin/host/(sections)/host-status-section.tsx deleted file mode 100644 index 7372142..0000000 --- a/src/pages/admin/host/(sections)/host-status-section.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import AdminCard from "@/src/components/general/admin-card"; -import HostStatCell from "../(partials)/host-stat-cell"; -import { - HOST_INTERFACE, - HOST_STATUS, -} from "../(data)/host-mock-data"; - -const STATS = [ - { label: "Uptime", value: HOST_STATUS.uptime }, - { label: "Active peers", value: String(HOST_STATUS.activePeers) }, - { label: "Handshakes / s", value: HOST_STATUS.handshakesPerSec.toFixed(1) }, - { label: "Received", value: HOST_STATUS.rxBytes }, - { label: "Transmitted", value: HOST_STATUS.txBytes }, - { label: "Version", value: HOST_STATUS.version }, -]; - -export default function HostStatusSection() { - return ( - -
    -
    - - - - -
    -

    - Running -

    -

    - {HOST_INTERFACE.name} · {HOST_INTERFACE.endpoint} -

    -
    -
    - - wg-quick status · v{HOST_STATUS.version} - -
    -
    - {STATS.map((stat) => ( - - ))} -
    -
    - ); -} diff --git a/src/pages/admin/host/(sections)/interface-config-section.tsx b/src/pages/admin/host/(sections)/interface-config-section.tsx deleted file mode 100644 index 5990383..0000000 --- a/src/pages/admin/host/(sections)/interface-config-section.tsx +++ /dev/null @@ -1,60 +0,0 @@ -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 { - HOST_INTERFACE, - INTERFACE_CONFIG, -} from "../(data)/host-mock-data"; - -function buildConfigText() { - const lines = ["[Interface]"]; - for (const row of INTERFACE_CONFIG) { - lines.push(`${row.keyName} = ${row.value}`); - } - lines.push(""); - lines.push(`# Public key: ${HOST_INTERFACE.publicKey}`); - return lines.join("\n"); -} - -export default function InterfaceConfigSection() { - return ( - -
    -
    -

    - Interface configuration -

    -

    - {HOST_INTERFACE.configPath} -

    -
    - - navigator.clipboard?.writeText(buildConfigText()) - } - > - Copy config - -
    -
    -
    - [Interface] -
    - {INTERFACE_CONFIG.map((row) => ( - - ))} -
    - - # Public key · {HOST_INTERFACE.publicKey.slice(0, 24)}… - -
    -
    -
    - ); -} diff --git a/src/pages/admin/host/index.tsx b/src/pages/admin/host/index.tsx deleted file mode 100644 index 1362759..0000000 --- a/src/pages/admin/host/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { RefreshCw } from "lucide-react"; -import type { BunextPageModuleMeta } from "@moduletrace/bunext/types"; -import AdminHero from "@/src/components/general/admin-hero"; -import AdminButton from "@/src/components/general/admin-button"; -import Stack from "@/src/components/twui/layout/Stack"; -import { SiteData } from "@/src/data/site-data"; -import HostStatusSection from "./(sections)/host-status-section"; -import InterfaceConfigSection from "./(sections)/interface-config-section"; - -export default function AdminHostPage() { - return ( - <> - - Restart service - - } - /> - - - - - - ); -} - -export const meta: BunextPageModuleMeta = { - title: `Host | ${SiteData["SiteName"]}`, - description: `WireGuard host configuration`, -}; diff --git a/src/pages/admin/hosts/(functions)/derive-host-config.ts b/src/pages/admin/hosts/(functions)/derive-host-config.ts new file mode 100644 index 0000000..a138675 --- /dev/null +++ b/src/pages/admin/hosts/(functions)/derive-host-config.ts @@ -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, + }; +} \ No newline at end of file diff --git a/src/pages/admin/hosts/(hooks)/use-host-data.ts b/src/pages/admin/hosts/(hooks)/use-host-data.ts new file mode 100644 index 0000000..ca5c8ef --- /dev/null +++ b/src/pages/admin/hosts/(hooks)/use-host-data.ts @@ -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({ + table: "hosts", + limit: 100, + }); + + const variables_res = useAdminCrudGet({ + table: "variables", + }); + + const clients_res = useAdminCrudGet({ + table: "clients", + limit: 100, + }); + + return { + hosts: hosts_res.res, + variables: variables_res.res, + clients: clients_res.res, + }; +} diff --git a/src/pages/admin/hosts/(partials)/host-stat-cell.tsx b/src/pages/admin/hosts/(partials)/host-stat-cell.tsx new file mode 100644 index 0000000..c5cf144 --- /dev/null +++ b/src/pages/admin/hosts/(partials)/host-stat-cell.tsx @@ -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 ( + + + {label} + + + {value} + + + ); +} \ No newline at end of file diff --git a/src/pages/admin/hosts/(partials)/interface-config-row.tsx b/src/pages/admin/hosts/(partials)/interface-config-row.tsx new file mode 100644 index 0000000..bc26708 --- /dev/null +++ b/src/pages/admin/hosts/(partials)/interface-config-row.tsx @@ -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 ( + + + {keyName} = + + + {value} + + + ); +} \ No newline at end of file diff --git a/src/pages/admin/hosts/(sections)/interface-config-section.tsx b/src/pages/admin/hosts/(sections)/interface-config-section.tsx new file mode 100644 index 0000000..02c7843 --- /dev/null +++ b/src/pages/admin/hosts/(sections)/interface-config-section.tsx @@ -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 ( + + + +

    + Interface configuration +

    +

    + {config.config_path} +

    +
    + + navigator.clipboard?.writeText(buildConfigText()) + } + > + Copy config + +
    +
    + + [Interface] + + {rows.map((row) => ( + + ))} + + + # Public key ·{" "} + {config.public_key + ? `${config.public_key.slice(0, 24)}…` + : "Not generated"} + + +
    +
    + ); +} \ No newline at end of file diff --git a/src/pages/admin/hosts/(sections)/main-host-status-section.tsx b/src/pages/admin/hosts/(sections)/main-host-status-section.tsx new file mode 100644 index 0000000..f81c795 --- /dev/null +++ b/src/pages/admin/hosts/(sections)/main-host-status-section.tsx @@ -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 ( + + + + + + {config.config_path} + + +
    + {stats.map((stat) => ( + + ))} +
    +
    + ); +} diff --git a/src/pages/admin/hosts/index.tsx b/src/pages/admin/hosts/index.tsx new file mode 100644 index 0000000..06d9220 --- /dev/null +++ b/src/pages/admin/hosts/index.tsx @@ -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 ( + <> + + + + Main Host Not Set up + + + + ); + } + + return ( + <> + + + + } + /> + + + + +

    Main Host

    + + +
    + + +

    Other Hosts

    + {hosts?.[0] ? ( + <> + ) : ( + + )} +
    + + ); +} + +export const meta: BunextPageModuleMeta = { + title: `Host | ${SiteData["SiteName"]}`, + description: `WireGuard host configuration`, +}; diff --git a/src/pages/admin/index.tsx b/src/pages/admin/index.tsx index 410180d..385be7d 100644 --- a/src/pages/admin/index.tsx +++ b/src/pages/admin/index.tsx @@ -2,13 +2,14 @@ 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 useDashboardData from "./(hooks)/use-dashboard-data"; import KpiHeroSection from "./(sections)/kpi-hero-section"; import KpiSecondarySection from "./(sections)/kpi-secondary-section"; -import TrafficChartSection from "./(sections)/traffic-chart-section"; import PeersTableSection from "./(sections)/peers-table-section"; -import ActivitySection from "./(sections)/activity-section"; export default function AdminDashboardPage() { + const { clients, hosts, variables } = useDashboardData(); + return ( <> - - - - - + + + ); @@ -29,4 +32,4 @@ export default function AdminDashboardPage() { export const meta: BunextPageModuleMeta = { title: `Admin Dashboard | ${SiteData["SiteName"]}`, description: `Admin dashboard`, -}; +}; \ No newline at end of file diff --git a/src/pages/api/admin/check-private-ip-address-availability.ts b/src/pages/api/admin/check-private-ip-address-availability.ts new file mode 100644 index 0000000..04dadc9 --- /dev/null +++ b/src/pages/api/admin/check-private-ip-address-availability.ts @@ -0,0 +1,46 @@ +import userAuth from "@/src/functions/backend/auth/user-auth"; +import checkPrivateIPAvailability from "@/src/functions/backend/setup/check-private-ip-availability"; +import type { ApiReqParams } from "@/src/types"; +import type { + APIResponseObject, + BunextAPIRouteHandler, +} from "@moduletrace/bunext/types"; +import _ from "lodash"; + +export const handler: BunextAPIRouteHandler = async ( + params, +) => { + const req = params.req; + const body = params.body as ApiReqParams | undefined; + + 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 ip_address = body?.ip_address; + + if (!ip_address) { + throw new Error(`No IP address passed!`); + } + + return await checkPrivateIPAvailability({ ip_address }); + } catch (error: any) { + return { + success: false, + msg: error.message, + }; + } +}; diff --git a/src/pages/api/admin/grab-next-available-private-ip.ts b/src/pages/api/admin/grab-next-available-private-ip.ts new file mode 100644 index 0000000..f53326c --- /dev/null +++ b/src/pages/api/admin/grab-next-available-private-ip.ts @@ -0,0 +1,35 @@ +import checkUserAccess from "@/src/functions/backend/auth/check-user-access"; +import userAuth from "@/src/functions/backend/auth/user-auth"; +import grabNextAvailablePrivateIPSubnet from "@/src/functions/backend/setup/grab-next-available-private-ip-subnet"; +import setupWireguardHost from "@/src/functions/backend/setup/setup-wireguard-host"; +import type { ApiReqParams } from "@/src/types"; +import type { + APIResponseObject, + BunextAPIRouteHandler, +} from "@moduletrace/bunext/types"; +import _ from "lodash"; + +export const handler: BunextAPIRouteHandler = async ( + params, +) => { + const req = params.req; + + const { user, user_types } = await userAuth({ req }); + + if (!user?.logged_in_status) { + return { + success: false, + msg: `Unauthorized`, + logoutUser: true, + }; + } + + try { + return await grabNextAvailablePrivateIPSubnet(); + } catch (error: any) { + return { + success: false, + msg: error.message, + }; + } +}; diff --git a/src/pages/api/admin/setup-main-host.ts b/src/pages/api/admin/setup-main-host.ts new file mode 100644 index 0000000..18bef5f --- /dev/null +++ b/src/pages/api/admin/setup-main-host.ts @@ -0,0 +1,52 @@ +import checkUserAccess from "@/src/functions/backend/auth/check-user-access"; +import userAuth from "@/src/functions/backend/auth/user-auth"; +import setupWireguardHost from "@/src/functions/backend/setup/setup-wireguard-host"; +import type { ApiReqParams } from "@/src/types"; +import type { + APIResponseObject, + BunextAPIRouteHandler, +} from "@moduletrace/bunext/types"; +import _ from "lodash"; + +export const handler: BunextAPIRouteHandler = 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`, + }; + } + + return await setupWireguardHost({ + wg_subnet_ip: body.ip_address, + }); + } catch (error: any) { + return { + success: false, + msg: error.message, + }; + } +}; diff --git a/src/types/index.ts b/src/types/index.ts index ddea4bb..7c08038 100755 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -220,6 +220,8 @@ export type ApiReqParams< text?: string; return_media_text_content?: boolean; media?: BUN_SQLITE_WGUI_MEDIA; + + ip_address?: string; }; export type UserAuthReturn = { diff --git a/src/utils/format-unix-timestamp.ts b/src/utils/format-unix-timestamp.ts new file mode 100644 index 0000000..733e74d --- /dev/null +++ b/src/utils/format-unix-timestamp.ts @@ -0,0 +1,17 @@ +type Params = { + timestamp?: number | ""; +}; + +export default function formatUnixTimestamp({ timestamp }: Params) { + if (!timestamp) { + return "—"; + } + + const date = new Date(timestamp * 1000); + + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); +} \ No newline at end of file