Add host setup functions and client components
This commit is contained in:
@@ -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",
|
||||
},
|
||||
];
|
||||
@@ -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<SetStateAction<boolean>>;
|
||||
onCreated?: () => void;
|
||||
};
|
||||
|
||||
type FormFieldProps = {
|
||||
@@ -15,94 +23,104 @@ type FormFieldProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
function FormField({ label, htmlFor, children }: FormFieldProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label
|
||||
htmlFor={htmlFor}
|
||||
className="text-[11.5px] font-semibold uppercase tracking-[0.08em] text-foreground-light/50 dark:text-foreground-dark/50"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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<string>();
|
||||
|
||||
async function handleSubmit(data: FormData) {
|
||||
setBusy(true);
|
||||
setError(undefined);
|
||||
|
||||
const res = await adminCrudHandler<BUN_SQLITE_WGUI_CLIENTS>({
|
||||
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 (
|
||||
<Modal
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
no_cancel_button
|
||||
className="p-6"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-foreground-light dark:text-foreground-dark">
|
||||
Add client
|
||||
</h3>
|
||||
<p className="text-xs text-foreground-light/50 dark:text-foreground-dark/50 mt-1">
|
||||
A WireGuard config will be generated on save
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close"
|
||||
onClick={() => setOpen(false)}
|
||||
className="p-1 cursor-pointer text-foreground-light/60 dark:text-foreground-dark/60 hover:text-foreground-light dark:hover:text-foreground-dark transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<form
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
setOpen(false);
|
||||
<Modal open={open} setOpen={setOpen} className="p-6">
|
||||
<Stack className="gap-4 mb-6">
|
||||
<H3 className="text-lg font-bold text-foreground-light dark:text-foreground-dark">
|
||||
Add client
|
||||
</H3>
|
||||
<Span className="" variant="faded">
|
||||
The client is added to your WireGuard network
|
||||
</Span>
|
||||
</Stack>
|
||||
|
||||
<Form
|
||||
submitHandler={(e, data) => {
|
||||
handleSubmit(data as FormData);
|
||||
}}
|
||||
>
|
||||
<FormField label="Client name" htmlFor="client_name">
|
||||
<Stack className="w-full items-stretch gap-6">
|
||||
<Input
|
||||
name="client_name"
|
||||
id="client_name"
|
||||
placeholder="e.g. MacBook Pro"
|
||||
label="Client name"
|
||||
autoFocus
|
||||
showLabel
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<FormField label="Tunnel IP" htmlFor="tunnel_ip">
|
||||
<Row className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<Input
|
||||
name="tunnel_ip"
|
||||
id="tunnel_ip"
|
||||
placeholder="10.0.0.10 · auto-assigned"
|
||||
placeholder="10.0.0.10"
|
||||
label="Tunnel IP"
|
||||
showLabel
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Allowed IPs" htmlFor="allowed_ips">
|
||||
<Input
|
||||
name="allowed_ips"
|
||||
id="allowed_ips"
|
||||
placeholder="10.0.0.10/32"
|
||||
label="Allowed IPs"
|
||||
showLabel
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
<FormField label="Notes" htmlFor="client_notes">
|
||||
<Input
|
||||
name="client_notes"
|
||||
id="client_notes"
|
||||
istextarea
|
||||
placeholder="Optional description for this client"
|
||||
/>
|
||||
</FormField>
|
||||
<div className="flex justify-end gap-2 mt-2">
|
||||
<AdminButton onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</AdminButton>
|
||||
<AdminButton variant="primary" type="submit">
|
||||
Add client
|
||||
</AdminButton>
|
||||
</div>
|
||||
</form>
|
||||
</Row>
|
||||
{error ? (
|
||||
<Span className="text-[12.5px] text-error dark:text-error">
|
||||
{error}
|
||||
</Span>
|
||||
) : null}
|
||||
<Row className="justify-end gap-2 mt-2">
|
||||
<AdminButton onClick={() => setOpen(false)} disabled={busy}>
|
||||
Cancel
|
||||
</AdminButton>
|
||||
<AdminButton
|
||||
variant="primary"
|
||||
type="submit"
|
||||
disabled={busy}
|
||||
>
|
||||
{busy ? "Adding…" : "Add client"}
|
||||
</AdminButton>
|
||||
</Row>
|
||||
</Stack>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<tr className="border-b border-slate-200/60 dark:border-white/5 last:border-b-0 hover:bg-foreground-light/[0.02] dark:hover:bg-foreground-dark/[0.02] transition-colors">
|
||||
<td className="px-4 py-[9px]">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<StatusDot status={client.status} />
|
||||
<span className="text-[13.5px] font-medium text-foreground-light dark:text-foreground-dark">
|
||||
{client.name}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-[9px] tabular text-[13px] text-foreground-light/60 dark:text-foreground-dark/60 whitespace-nowrap">
|
||||
{client.tunnelIp}
|
||||
</td>
|
||||
<td className="px-4 py-[9px] tabular text-[13px] text-foreground-light/45 dark:text-foreground-dark/45 whitespace-nowrap">
|
||||
{client.allowedIps}
|
||||
</td>
|
||||
<td className="px-4 py-[9px]">
|
||||
<span className="font-mono text-[12px] text-foreground-light/45 dark:text-foreground-dark/45 whitespace-nowrap">
|
||||
{client.publicKey.slice(0, 16)}…
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-[9px] text-right whitespace-nowrap">
|
||||
<span className="tabular text-[13px] font-semibold text-foreground-light dark:text-foreground-dark">
|
||||
↓ {client.received}
|
||||
</span>
|
||||
<span className="tabular text-[12px] text-foreground-light/40 dark:text-foreground-dark/40">
|
||||
{" "}
|
||||
↑ {client.sent}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-[9px] tabular text-[12px] text-foreground-light/40 dark:text-foreground-dark/40 text-right whitespace-nowrap">
|
||||
{client.lastHandshake}
|
||||
</td>
|
||||
<td className="px-4 py-[9px] tabular text-[12px] text-foreground-light/40 dark:text-foreground-dark/40 text-right whitespace-nowrap">
|
||||
{client.createdAt}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
@@ -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<BUN_SQLITE_WGUI_CLIENTS>({
|
||||
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 (
|
||||
<AdminCard className="w-full overflow-hidden">
|
||||
<div className="flex items-center justify-between gap-3 px-5 h-12 flex-wrap">
|
||||
<h2 className="text-[13.5px] font-medium text-foreground-light/60 dark:text-foreground-dark/60">
|
||||
<Row className="justify-between gap-3 px-5 h-12 flex-wrap">
|
||||
<H2 className="text-[13.5px] font-medium text-foreground-light/60 dark:text-foreground-dark/60 mb-0!">
|
||||
Clients
|
||||
<span className="tabular text-foreground-light/40 dark:text-foreground-dark/40 ml-2">
|
||||
{filtered.length}
|
||||
</span>
|
||||
</h2>
|
||||
<Span className="tabular text-foreground-light/40 dark:text-foreground-dark/40 ml-2">
|
||||
{String(filtered.length)}
|
||||
</Span>
|
||||
</H2>
|
||||
<Search
|
||||
no_search_button
|
||||
placeholder="Search clients…"
|
||||
@@ -50,8 +67,12 @@ export default function ClientsTableSection({ addOpen, setAddOpen }: Props) {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{filtered.length ? (
|
||||
</Row>
|
||||
{res === undefined ? (
|
||||
<Stack center className="w-full py-10">
|
||||
<Loading />
|
||||
</Stack>
|
||||
) : filtered.length ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[860px]">
|
||||
<thead>
|
||||
@@ -60,8 +81,6 @@ export default function ClientsTableSection({ addOpen, setAddOpen }: Props) {
|
||||
<th className={thClass}>Tunnel IP</th>
|
||||
<th className={thClass}>Allowed IPs</th>
|
||||
<th className={thClass}>Public key</th>
|
||||
<th className={thRightClass}>Traffic</th>
|
||||
<th className={thRightClass}>Last handshake</th>
|
||||
<th className={thRightClass}>Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -73,16 +92,22 @@ export default function ClientsTableSection({ addOpen, setAddOpen }: Props) {
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center gap-1 py-14 px-6 text-center">
|
||||
<span className="text-[13.5px] font-medium text-foreground-light/50 dark:text-foreground-dark/50">
|
||||
No clients found
|
||||
</span>
|
||||
<span className="text-[12.5px] text-foreground-light/35 dark:text-foreground-dark/35">
|
||||
Try adjusting your search terms
|
||||
</span>
|
||||
</div>
|
||||
<Stack center className="w-full justify-center py-14 px-6 text-center gap-1">
|
||||
<Span className="text-[13.5px] font-medium text-foreground-light/50 dark:text-foreground-dark/50">
|
||||
{query ? "No clients found" : "No clients yet"}
|
||||
</Span>
|
||||
<Span className="text-[12.5px] text-foreground-light/35 dark:text-foreground-dark/35">
|
||||
{query
|
||||
? "Try adjusting your search terms"
|
||||
: "Add a client to get started"}
|
||||
</Span>
|
||||
</Stack>
|
||||
)}
|
||||
<ClientFormModal open={addOpen} setOpen={setAddOpen} />
|
||||
<ClientFormModal
|
||||
open={addOpen}
|
||||
setOpen={setAddOpen}
|
||||
onCreated={() => setRes(undefined)}
|
||||
/>
|
||||
</AdminCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user