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 (
-
-
- {showLabel ? (
-
- {meta.label}
-
- ) : null}
-
- );
-}
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 (
-
- );
-}
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 ? (
-
+
- | Peer |
+ Client |
Tunnel IP |
- Endpoint |
- Traffic |
- Last handshake |
+ Allowed IPs |
+ Public key |
+ Created |
- {PEERS.map((peer) => (
-
+ {recent.map((client) => (
+
))}
-
-
-
+ ) : (
+
+
+ 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
-
-
-
-
-
);
-}
+}
\ 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) {
| Tunnel IP |
Allowed IPs |
Public key |
- Traffic |
- Last handshake |
Created |
@@ -73,16 +92,22 @@ export default function ClientsTableSection({ addOpen, setAddOpen }: Props) {
) : (
-
-
- 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.address ? "Configured" : "Not configured"}
+
+
+ {config.interface_name}
+ {config.address ? ` · ${config.address}` : ""}
+
+
+
+
+ {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