diff --git a/src/components/general/grab-next-subnet-button.tsx b/src/components/general/grab-next-subnet-button.tsx new file mode 100644 index 0000000..c91b7d9 --- /dev/null +++ b/src/components/general/grab-next-subnet-button.tsx @@ -0,0 +1,43 @@ +import { useState } from "react"; +import { Wand2 } from "lucide-react"; +import Button from "../twui/layout/Button"; +import fetchApi from "../twui/utils/fetch/fetchApi"; +import type { ApiReqParams } from "@/src/types"; + +type Props = { + onGrab?: (ip: string) => void; +}; + +export default function GrabNextSubnetButton({ onGrab }: Props) { + const [grabbing, setGrabbing] = useState(false); + + function grabNextSubnet() { + setGrabbing(true); + + fetchApi( + `/api/admin/grab-next-available-private-ip`, + { method: "POST" }, + ) + .then((res) => { + if (res?.success && res.msg) { + onGrab?.(res.msg); + } + }) + .finally(() => { + setGrabbing(false); + }); + } + + return ( + + ); +} \ No newline at end of file diff --git a/src/components/general/host-wg-ip-field.tsx b/src/components/general/host-wg-ip-field.tsx new file mode 100644 index 0000000..991c4d3 --- /dev/null +++ b/src/components/general/host-wg-ip-field.tsx @@ -0,0 +1,158 @@ +import { useEffect, useState, type RefObject } from "react"; +import { CircleCheck, Loader2, Network, TriangleAlert } from "lucide-react"; +import Input from "../twui/form/Input"; +import Row from "../twui/layout/Row"; +import Span from "../twui/layout/Span"; +import Tag from "../twui/elements/Tag"; +import Stack from "../twui/layout/Stack"; +import fetchApi from "../twui/utils/fetch/fetchApi"; +import type { ApiReqParams } from "@/src/types"; + +export type HostWgIpFieldStatus = + | "checking" + | "available" + | "not_available" + | "invalid" + | "current"; + +type Props = { + value: string; + onChange?: (value: string) => void; + onStatus?: (status: HostWgIpFieldStatus) => void; + inputRef?: RefObject; + current_value?: string; + autoFocus?: boolean; +}; + +const subnet_ip_pattern = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.1$/; + +type AvailabilityType = { + success: boolean; +}; + +export default function HostWgIpField({ + value, + onChange, + onStatus, + inputRef, + current_value, + autoFocus, +}: Props) { + const [availability, setAvailability] = useState(); + + useEffect(() => { + setAvailability(undefined); + + if (!subnet_ip_pattern.test(value)) return; + if (current_value && value == current_value) return; + + let cancelled = false; + + fetchApi( + `/api/admin/check-private-ip-address-availability`, + { + method: "POST", + body: { ip_address: value }, + }, + ).then((res) => { + if (cancelled) return; + setAvailability(res); + }); + + return () => { + cancelled = true; + }; + }, [value, current_value]); + + const is_current = Boolean(current_value && value == current_value); + + const ip_status: HostWgIpFieldStatus = is_current + ? "current" + : subnet_ip_pattern.test(value) + ? availability + ? availability.success + ? "available" + : "not_available" + : "checking" + : "invalid"; + + useEffect(() => { + onStatus?.(ip_status); + }, [ip_status, onStatus]); + + return ( + + { + onChange?.(v); + }} + prefix={} + suffix={ + + WireGuard IP Address + + } + componentRef={inputRef} + autoFocus={autoFocus} + /> + + {ip_status === "invalid" ? ( + + + + Invalid IP + + + ) : ip_status === "current" ? ( + + + + Current value + + + ) : ip_status === "not_available" ? ( + + + + Not available + + + ) : ip_status === "available" ? ( + + + + Available + + + ) : ( + + + + Checking availability… + + + )} + + ); +} diff --git a/src/components/general/setup-main-host-button.tsx b/src/components/general/setup-main-host-button.tsx index 72b4e64..700fe67 100755 --- a/src/components/general/setup-main-host-button.tsx +++ b/src/components/general/setup-main-host-button.tsx @@ -1,164 +1,41 @@ -import { useEffect, useRef, useState, type ComponentProps } from "react"; +import { 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 { CircleCheck, - Loader2, - Network, TriangleAlert, - Wand2, } from "lucide-react"; import Span from "../twui/layout/Span"; import Tag from "../twui/elements/Tag"; import { AppData } from "@/src/data/app-data"; import type { APIResponseObject } from "@moduletrace/bunext/types"; +import HostWgIpField, { + type HostWgIpFieldStatus, +} from "./host-wg-ip-field"; +import GrabNextSubnetButton from "./grab-next-subnet-button"; type Props = { button_props?: Omit, "title">; }; -type AvailabilityType = { - success: boolean; -}; - -type GrabSubnetType = { - success: boolean; - msg?: string; -}; - -const subnet_ip_pattern = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.1$/; - -type IPStatusType = "checking" | "available" | "not_available" | "invalid"; - export default function SetupMainHostButton({ button_props }: Props) { const { loading, setLoading, status, setStatus } = useStatus(); const [wgIP, setWgIP] = useState(AppData["DefaultPrivateIP"]); - const [availability, setAvailability] = useState(); - const [grabbing, setGrabbing] = useState(false); - - const input_ref = useRef(null); - - useEffect(() => { - setAvailability(undefined); - - if (!subnet_ip_pattern.test(wgIP)) return; - - let cancelled = false; - - fetchApi( - `/api/admin/check-private-ip-address-availability`, - { - method: "POST", - body: { ip_address: wgIP }, - }, - ).then((res) => { - if (cancelled) return; - setAvailability(res); - }); - - return () => { - cancelled = true; - }; - }, [wgIP]); - - const ip_status: IPStatusType = subnet_ip_pattern.test(wgIP) - ? availability - ? availability.success - ? "available" - : "not_available" - : "checking" - : "invalid"; - - function grabNextSubnet() { - setGrabbing(true); - - fetchApi( - `/api/admin/grab-next-available-private-ip`, - { method: "POST" }, - ) - .then((res) => { - if (res?.success && res.msg) { - setWgIP(res.msg); - if (input_ref.current) { - input_ref.current.value = res.msg; - } - } - }) - .finally(() => { - setGrabbing(false); - }); - } + const [ipStatus, setIpStatus] = useState("checking"); return ( - - { - setWgIP(v); - }} - prefix={} - suffix={ - - Selected Wireguard IP Address - - } - componentRef={input_ref} - autoFocus - /> - - {ip_status === "invalid" ? ( - - - - Invalid IP - - - ) : ip_status === "not_available" ? ( - - - - Not available - - - ) : ip_status === "available" ? ( - - - - Available - - - ) : ( - - - - Checking availability… - - - )} - + {status?.error && status.msg ? ( - {status.msg} + {status.msg} ) : null} @@ -180,27 +57,18 @@ export default function SetupMainHostButton({ button_props }: Props) { > - {status.msg} + {status.msg} ) : null} - + + + + + + + {client_count || 0}{" "} + {client_count == 1 ? "client" : "clients"} + + {host.public_ip_address ? ( + + {host.public_ip_address} + + ) : null} + + + ); +} \ No newline at end of file diff --git a/src/pages/admin/hosts/(sections)/host-public-ip-section.tsx b/src/pages/admin/hosts/(sections)/host-public-ip-section.tsx index 035e4b5..f136baf 100644 --- a/src/pages/admin/hosts/(sections)/host-public-ip-section.tsx +++ b/src/pages/admin/hosts/(sections)/host-public-ip-section.tsx @@ -104,7 +104,7 @@ export default function HostPublicIpSection({ host_id, current_ip }: Props) { placeholder="e.g. 203.0.113.10" label="New public IP" showLabel - value={ip} + defaultValue={ip} onChange={(e) => { setTyped(true); setIp(e.target.value); diff --git a/src/pages/admin/hosts/(sections)/main-host-status-section.tsx b/src/pages/admin/hosts/(sections)/main-host-status-section.tsx index f81c795..a636216 100644 --- a/src/pages/admin/hosts/(sections)/main-host-status-section.tsx +++ b/src/pages/admin/hosts/(sections)/main-host-status-section.tsx @@ -9,14 +9,16 @@ import type { BUN_SQLITE_WGUI_CLIENTS, BUN_SQLITE_WGUI_VARIABLES, } from "@/db/types/db"; +import type { BUN_SQLITE_WGUI_HOSTS_JOIN } from "@/src/types/sql-joins"; type Props = { + host?: BUN_SQLITE_WGUI_HOSTS_JOIN; variables?: BUN_SQLITE_WGUI_VARIABLES[]; clients?: BUN_SQLITE_WGUI_CLIENTS[]; }; -export default function MainHostStatusSection({ variables, clients }: Props) { - const config = deriveHostConfig({ variables, clients }); +export default function MainHostStatusSection({ host, variables, clients }: Props) { + const config = deriveHostConfig({ host, variables, clients }); const stats = [ { diff --git a/src/pages/admin/hosts/[host_id]/clients/(partials)/client-rules/client-rule-ports-field.tsx b/src/pages/admin/hosts/[host_id]/clients/(partials)/client-rules/client-rule-ports-field.tsx index deb184a..3b0d086 100644 --- a/src/pages/admin/hosts/[host_id]/clients/(partials)/client-rules/client-rule-ports-field.tsx +++ b/src/pages/admin/hosts/[host_id]/clients/(partials)/client-rules/client-rule-ports-field.tsx @@ -16,11 +16,11 @@ export default function ClientRulePortsField({ draft, setDraft }: Props) { title="Ports" placeholder="80,443 or 8000:8080" showLabel - value={draft.ports} - onChange={(e) => { + defaultValue={draft.ports} + changeHandler={(value) => { setDraft({ ...draft, - ports: e.target.value, + ports: value, }); }} info="Leave empty for any port." diff --git a/src/pages/admin/hosts/[host_id]/clients/add-client/(partials)/client-form/add-client-form-wg-ip-address.tsx b/src/pages/admin/hosts/[host_id]/clients/add-client/(partials)/client-form/add-client-form-wg-ip-address.tsx index e01aabf..b105126 100644 --- a/src/pages/admin/hosts/[host_id]/clients/add-client/(partials)/client-form/add-client-form-wg-ip-address.tsx +++ b/src/pages/admin/hosts/[host_id]/clients/add-client/(partials)/client-form/add-client-form-wg-ip-address.tsx @@ -44,14 +44,14 @@ export default function AddClientFormWgIpAddress({ id="add_client_wg_ip_address" title="WireGuard IP Address" placeholder="Eg. 10.0.0.2" - onChange={(e) => { + changeHandler={(v) => { setForm((prev) => ({ ...prev, - wg_ip_address: e.target.value, - allowed_ips: `${e.target.value.replace(/\.\d+$/, "")}.0/24`, + wg_ip_address: v, + allowed_ips: `${v.replace(/\.\d+$/, "")}.0/24`, })); }} - value={form.wg_ip_address} + defaultValue={form.wg_ip_address} showLabel /> diff --git a/src/pages/admin/hosts/[host_id]/edit/(sections)/edit-host-form-section.tsx b/src/pages/admin/hosts/[host_id]/edit/(sections)/edit-host-form-section.tsx new file mode 100644 index 0000000..1a13601 --- /dev/null +++ b/src/pages/admin/hosts/[host_id]/edit/(sections)/edit-host-form-section.tsx @@ -0,0 +1,142 @@ +import { useContext, useState } from "react"; +import { CircleCheck, TriangleAlert } from "lucide-react"; +import AdminCard from "@/src/components/general/admin-card"; +import HostWgIpField, { + type HostWgIpFieldStatus, +} from "@/src/components/general/host-wg-ip-field"; +import useStatus from "@/src/components/twui/hooks/useStatus"; +import fetchApi from "@/src/components/twui/utils/fetch/fetchApi"; +import H3 from "@/src/components/twui/layout/H3"; +import P from "@/src/components/twui/layout/P"; +import Row from "@/src/components/twui/layout/Row"; +import Span from "@/src/components/twui/layout/Span"; +import Stack from "@/src/components/twui/layout/Stack"; +import Tag from "@/src/components/twui/elements/Tag"; +import Button from "@/src/components/twui/layout/Button"; +import { AppContext } from "@/src/pages/__root"; +import HostPublicIpSection from "../../../(sections)/host-public-ip-section"; +import type { ApiReqParams } from "@/src/types"; +import type { APIResponseObject } from "@moduletrace/bunext/types"; + +export default function EditHostFormSection() { + const { pageProps, query } = useContext(AppContext); + + const host_id = Number(query?.host_id || pageProps?.host?.id || 0); + const host = pageProps?.host; + + const { loading, setLoading, status, setStatus } = useStatus(); + + const [wgIP, setWgIP] = useState(host?.wg_ip_address || ""); + const [ipStatus, setIpStatus] = useState("checking"); + + function handleSave() { + if (!window.confirm("Update this host configuration?")) return; + + setStatus(undefined); + setLoading(true); + + fetchApi( + `/api/admin/update-host-wg-ip`, + { + method: "POST", + body: { + host_id, + wg_ip_address: wgIP, + }, + }, + ) + .then((res) => { + if (res.success) { + window.location.pathname = `/admin/hosts/${host_id}`; + return; + } + + setStatus({ + error: true, + success: false, + msg: res.msg || "Could not update the host", + }); + }) + .catch((error: any) => { + setStatus({ + error: true, + success: false, + msg: error.message || "Could not update the host", + }); + }) + .finally(() => { + setLoading(false); + }); + } + + return ( + <> + + +

+ WireGuard subnet +

+

+ The private subnet the host listens on (e.g. + 10.1.0.1/24). Updating it rewrites the host config and + restarts the tunnel. +

+
+ + + + {status?.error && status.msg ? ( + + + + {status.msg} + + + ) : null} + {status?.success && status.msg ? ( + + + + {status.msg} + + + ) : null} + + + + +
+ + + + ); +} \ No newline at end of file diff --git a/src/pages/admin/hosts/[host_id]/edit/index.server.ts b/src/pages/admin/hosts/[host_id]/edit/index.server.ts new file mode 100644 index 0000000..125a91c --- /dev/null +++ b/src/pages/admin/hosts/[host_id]/edit/index.server.ts @@ -0,0 +1,58 @@ +import type { + BUN_SQLITE_WGUI_HOSTS, + BUN_SQLITE_WGUI_VARIABLES, +} from "@/db/types/db"; +import { AppData } from "@/src/data/app-data"; +import type { PagePropsType, PageQueryObject, TableType } from "@/src/types"; +import BunSQLite from "@moduletrace/bun-sqlite"; +import type { BunextPageServerFn } from "@moduletrace/bunext/types"; + +const server: BunextPageServerFn = async ({ query }) => { + const page_query = query as PageQueryObject; + + const host_id = Number( + page_query.host_id || AppData["WireguardHostID"], + ); + + const host_res = await BunSQLite.select< + BUN_SQLITE_WGUI_HOSTS, + TableType + >({ + table: "hosts", + targetId: host_id, + }); + + let host = host_res.singleRes || null; + + if (!host && host_id == AppData["WireguardHostID"]) { + const variables_res = await BunSQLite.select< + BUN_SQLITE_WGUI_VARIABLES, + TableType + >({ + table: "variables", + }); + + const variables = variables_res.payload || []; + + host = { + id: AppData["WireguardHostID"], + wg_ip_address: + variables.find((v) => v.key == "main_host_wg_ip_address") + ?.value || "", + public_ip_address: + variables.find((v) => v.key == "main_host_public_ip_address") + ?.value || "", + public_key: + variables.find((v) => v.key == "main_host_wg_public_key") + ?.value || "", + }; + } + + return { + props: { + host, + }, + }; +}; + +export default server; \ No newline at end of file diff --git a/src/pages/admin/hosts/[host_id]/edit/index.tsx b/src/pages/admin/hosts/[host_id]/edit/index.tsx new file mode 100644 index 0000000..585f6eb --- /dev/null +++ b/src/pages/admin/hosts/[host_id]/edit/index.tsx @@ -0,0 +1,55 @@ +import type { BunextPageModuleMeta } from "@moduletrace/bunext/types"; +import AdminHero from "@/src/components/general/admin-hero"; +import { SiteData } from "@/src/data/site-data"; +import Button from "@/src/components/twui/layout/Button"; +import Divider from "@/src/components/twui/layout/Divider"; +import Stack from "@/src/components/twui/layout/Stack"; +import EmptyContent from "@/src/components/twui/elements/EmptyContent"; +import { ArrowLeft } from "lucide-react"; +import { useContext } from "react"; +import { AppContext } from "@/src/pages/__root"; +import EditHostFormSection from "./(sections)/edit-host-form-section"; + +export default function AdminEditHostPage() { + const { pageProps, query } = useContext(AppContext); + + const host_id = Number(query?.host_id || pageProps?.host?.id || 0); + + return ( + <> + } + href={`/admin/hosts/${host_id}`} + > + View host + + } + /> + + + + + {pageProps?.host ? ( + + ) : ( + + )} + + + ); +} + +export const meta: BunextPageModuleMeta = { + title: `Edit Host | ${SiteData["SiteName"]}`, + description: `WireGuard edit host page`, +}; \ No newline at end of file diff --git a/src/pages/admin/hosts/[host_id]/index.server.ts b/src/pages/admin/hosts/[host_id]/index.server.ts index f230433..e5daf95 100644 --- a/src/pages/admin/hosts/[host_id]/index.server.ts +++ b/src/pages/admin/hosts/[host_id]/index.server.ts @@ -1,7 +1,9 @@ import type { BUN_SQLITE_WGUI_CLIENTS, BUN_SQLITE_WGUI_HOSTS, + BUN_SQLITE_WGUI_VARIABLES, } from "@/db/types/db"; +import grabHostDirnames from "@/src/functions/backend/setup/grab-host-dir-names"; import type { PagePropsType, PageQueryObject, TableType } from "@/src/types"; import BunSQLite from "@moduletrace/bun-sqlite"; import type { BunextPageServerFn } from "@moduletrace/bunext/types"; @@ -18,6 +20,15 @@ const server: BunextPageServerFn = async ({ TableType >({ table: "hosts", targetId: page_query.host_id }); + const host_record = host_record_res.singleRes || null; + + const host = host_record + ? { + ...host_record, + dir_names: grabHostDirnames({ host: host_record }), + } + : null; + const host_clients = await BunSQLite.select< BUN_SQLITE_WGUI_CLIENTS, TableType @@ -32,12 +43,18 @@ const server: BunextPageServerFn = async ({ }, }); + const variables_res = await BunSQLite.select< + BUN_SQLITE_WGUI_VARIABLES, + TableType + >({ table: "variables" }); + return { props: { - host: host_record_res.singleRes || null, + host, clients: host_clients.payload || null, + variables: variables_res.payload || null, }, }; }; -export default server; +export default server; \ No newline at end of file diff --git a/src/pages/admin/hosts/[host_id]/index.tsx b/src/pages/admin/hosts/[host_id]/index.tsx index 555eae6..2b1fc6b 100644 --- a/src/pages/admin/hosts/[host_id]/index.tsx +++ b/src/pages/admin/hosts/[host_id]/index.tsx @@ -1,26 +1,72 @@ import type { BunextPageModuleMeta } from "@moduletrace/bunext/types"; +import { useContext } from "react"; import AdminHero from "@/src/components/general/admin-hero"; import { SiteData } from "@/src/data/site-data"; import Button from "@/src/components/twui/layout/Button"; import Divider from "@/src/components/twui/layout/Divider"; +import EmptyContent from "@/src/components/twui/elements/EmptyContent"; import Stack from "@/src/components/twui/layout/Stack"; -import HostPublicIpSection from "../(sections)/host-public-ip-section"; -import { useContext } from "react"; import { AppContext } from "@/src/pages/__root"; +import MainHostStatusSection from "../(sections)/main-host-status-section"; +import HostPublicIpSection from "../(sections)/host-public-ip-section"; +import InterfaceConfigSection from "../(sections)/interface-config-section"; export default function AdminSingleHostPage() { - const { pageProps } = useContext(AppContext); + const { pageProps, query } = useContext(AppContext); - const host_id = Number(pageProps?.host?.id || 0); + const host_id = Number(query?.host_id || pageProps?.host?.id || 0); + + const host = pageProps?.host || undefined; + const variables = pageProps?.variables || []; + const clients = pageProps?.clients || []; + + const is_main_host = !host || host_id == 0; + + if (!pageProps?.host && host_id != 0) { + return ( + <> + + + + + + + ); + } return ( <> - + + @@ -30,12 +76,26 @@ export default function AdminSingleHostPage() { - {pageProps?.host ? ( - - ) : null} + + v.key == "main_host_public_ip_address", + )?.value || + "" + } + /> + ); diff --git a/src/pages/admin/hosts/add/(sections)/add-host-form-section.tsx b/src/pages/admin/hosts/add/(sections)/add-host-form-section.tsx new file mode 100644 index 0000000..a90e60e --- /dev/null +++ b/src/pages/admin/hosts/add/(sections)/add-host-form-section.tsx @@ -0,0 +1,126 @@ +import { useState } from "react"; +import { CircleCheck, TriangleAlert } from "lucide-react"; +import AdminCard from "@/src/components/general/admin-card"; +import GrabNextSubnetButton from "@/src/components/general/grab-next-subnet-button"; +import HostWgIpField, { + type HostWgIpFieldStatus, +} from "@/src/components/general/host-wg-ip-field"; +import useStatus from "@/src/components/twui/hooks/useStatus"; +import fetchApi from "@/src/components/twui/utils/fetch/fetchApi"; +import H3 from "@/src/components/twui/layout/H3"; +import P from "@/src/components/twui/layout/P"; +import Row from "@/src/components/twui/layout/Row"; +import Span from "@/src/components/twui/layout/Span"; +import Stack from "@/src/components/twui/layout/Stack"; +import Tag from "@/src/components/twui/elements/Tag"; +import Button from "@/src/components/twui/layout/Button"; +import { AppData } from "@/src/data/app-data"; +import type { ApiReqParams } from "@/src/types"; +import type { APIResponseObject } from "@moduletrace/bunext/types"; + +export default function AddHostFormSection() { + const { loading, setLoading, status, setStatus } = useStatus(); + + const [wgIP, setWgIP] = useState(AppData["DefaultPrivateIP"]); + const [ipStatus, setIpStatus] = useState("checking"); + + function handleCreate() { + if (!window.confirm("Create this new host?")) return; + + setStatus(undefined); + setLoading(true); + + fetchApi( + `/api/admin/add-host`, + { + method: "POST", + body: { wg_ip_address: wgIP }, + }, + ) + .then((res) => { + if (res.success && res.numberRes) { + window.location.pathname = `/admin/hosts/${res.numberRes}`; + return; + } + + setStatus({ + error: true, + success: false, + msg: res.msg || "Could not create the host", + }); + }) + .catch((error: any) => { + setStatus({ + error: true, + success: false, + msg: error.message || "Could not create the host", + }); + }) + .finally(() => { + setLoading(false); + }); + } + + return ( + + +

+ WireGuard subnet +

+

+ Each host runs on its own private subnet (e.g. + 10.2.0.1/24). The tunnel is brought up and restarted as + part of creation. +

+
+ + + + {status?.error && status.msg ? ( + + + + {status.msg} + + + ) : null} + {status?.success && status.msg ? ( + + + + {status.msg} + + + ) : null} + + + + + + +
+ ); +} \ No newline at end of file diff --git a/src/pages/admin/hosts/add/index.tsx b/src/pages/admin/hosts/add/index.tsx new file mode 100644 index 0000000..0691fd5 --- /dev/null +++ b/src/pages/admin/hosts/add/index.tsx @@ -0,0 +1,40 @@ +import type { BunextPageModuleMeta } from "@moduletrace/bunext/types"; +import AdminHero from "@/src/components/general/admin-hero"; +import { SiteData } from "@/src/data/site-data"; +import Button from "@/src/components/twui/layout/Button"; +import Divider from "@/src/components/twui/layout/Divider"; +import Stack from "@/src/components/twui/layout/Stack"; +import { ArrowLeft } from "lucide-react"; +import AddHostFormSection from "./(sections)/add-host-form-section"; + +export default function AdminAddHostPage() { + return ( + <> + } + href="/admin/hosts" + > + Back to Hosts + + } + /> + + + + + + + + ); +} + +export const meta: BunextPageModuleMeta = { + title: `Add New Host | ${SiteData["SiteName"]}`, + description: `WireGuard add host page`, +}; \ No newline at end of file diff --git a/src/pages/admin/hosts/index.tsx b/src/pages/admin/hosts/index.tsx index 55d4bcb..90b28b2 100644 --- a/src/pages/admin/hosts/index.tsx +++ b/src/pages/admin/hosts/index.tsx @@ -15,10 +15,13 @@ import SetupMainHostButton from "@/src/components/general/setup-main-host-button import Row from "@/src/components/twui/layout/Row"; import { Plus } from "lucide-react"; import HostPublicIpSection from "./(sections)/host-public-ip-section"; +import OtherHostCard from "./(partials)/other-host-card"; export default function AdminHostPage() { const { hosts, variables, clients } = useHostData(); + const other_hosts = (hosts || []).filter((host) => (host.id || 0) != 0); + const main_host_public_ip = variables?.find( (v) => v.key == "main_host_public_ip_address", )?.value; @@ -48,7 +51,11 @@ export default function AdminHostPage() { description="WireGuard server configuration and interface details" buttons={ <> - @@ -97,8 +104,22 @@ export default function AdminHostPage() {

Other Hosts

- {hosts?.[0] ? ( - <> + {other_hosts[0] ? ( + + {other_hosts.map((host) => ( + + (client.host_id || 0) == + (host.id || 0), + ).length || 0 + } + /> + ))} + ) : ( )} diff --git a/src/pages/api/admin/add-host.ts b/src/pages/api/admin/add-host.ts new file mode 100644 index 0000000..d5e10ff --- /dev/null +++ b/src/pages/api/admin/add-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 createWireguardHost from "@/src/functions/backend/setup/create-wireguard-host"; +import type { ApiReqParams } from "@/src/types"; +import type { + APIResponseObject, + BunextAPIRouteHandler, +} from "@moduletrace/bunext/types"; + +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 createWireguardHost({ + wg_ip_address: body?.wg_ip_address, + user, + }); + } catch (error: any) { + return { + success: false, + msg: error.message, + }; + } +}; \ No newline at end of file diff --git a/src/pages/api/admin/update-host-wg-ip.ts b/src/pages/api/admin/update-host-wg-ip.ts new file mode 100644 index 0000000..b9448cd --- /dev/null +++ b/src/pages/api/admin/update-host-wg-ip.ts @@ -0,0 +1,53 @@ +import checkUserAccess from "@/src/functions/backend/auth/check-user-access"; +import userAuth from "@/src/functions/backend/auth/user-auth"; +import updateWireguardHost from "@/src/functions/backend/setup/update-wireguard-host"; +import type { ApiReqParams } from "@/src/types"; +import type { + APIResponseObject, + BunextAPIRouteHandler, +} from "@moduletrace/bunext/types"; + +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 updateWireguardHost({ + host_id: body?.host_id, + wg_ip_address: body?.wg_ip_address, + user, + }); + } catch (error: any) { + return { + success: false, + msg: error.message, + }; + } +}; \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 7f0c484..8921bfc 100755 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,6 +6,7 @@ import type { BUN_SQLITE_WGUI_MEDIA, BUN_SQLITE_WGUI_USER_TYPES, BUN_SQLITE_WGUI_USERS, + BUN_SQLITE_WGUI_VARIABLES, BunSQLiteTables, } from "@/db/types/db"; import type { SBFSusidiaries } from "../dict/subsidiaries-dict"; @@ -16,7 +17,10 @@ import type { ClientRuleTypes, } from "../dict/client-rules-dict"; import type { BunextPageModuleServerReturnURLObject } from "@moduletrace/bunext/types"; -import type { BUN_SQLITE_WGUI_USERS_JOIN } from "./sql-joins"; +import type { + BUN_SQLITE_WGUI_USERS_JOIN, + BUN_SQLITE_WGUI_HOSTS_JOIN, +} from "./sql-joins"; import type { ImageInputToBase64FunctionReturn } from "../components/twui/utils/form/imageInputToBase64"; import type { ServerQueryParam } from "@moduletrace/bun-sqlite/dist/types"; import type grabHostDirnames from "../functions/backend/setup/grab-host-dir-names"; @@ -41,8 +45,9 @@ export type PagePropsType = { user_types?: BUN_SQLITE_WGUI_USER_TYPES[] | null; person?: BUN_SQLITE_WGUI_USERS_JOIN | null; persons?: BUN_SQLITE_WGUI_USERS_JOIN[] | null; - host?: BUN_SQLITE_WGUI_HOSTS | null; + host?: BUN_SQLITE_WGUI_HOSTS_JOIN | null; hosts?: BUN_SQLITE_WGUI_HOSTS[] | null; + variables?: BUN_SQLITE_WGUI_VARIABLES[] | null; client?: BUN_SQLITE_WGUI_CLIENTS | null; clients?: BUN_SQLITE_WGUI_CLIENTS[] | null; client_rules?: BUN_SQLITE_WGUI_CLIENT_RULES[] | null; @@ -243,6 +248,7 @@ export type ApiReqParams< media?: BUN_SQLITE_WGUI_MEDIA; ip_address?: string; + wg_ip_address?: string | null; }; export type UserAuthReturn = { @@ -327,3 +333,10 @@ export type MediaDataType = | ReadableStream | Request | Response; + +export type AddClientFormData = { + client_name?: string; + tunnel_ip?: string; + allowed_ips?: string; + host_id: number; +};