Updates
This commit is contained in:
@@ -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<ApiReqParams, { success: boolean; msg?: string }>(
|
||||||
|
`/api/admin/grab-next-available-private-ip`,
|
||||||
|
{ method: "POST" },
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
if (res?.success && res.msg) {
|
||||||
|
onGrab?.(res.msg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setGrabbing(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
title="Grab Next Available Subnet"
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
beforeIcon={<Wand2 size={16} />}
|
||||||
|
loading={grabbing}
|
||||||
|
onClick={grabNextSubnet}
|
||||||
|
>
|
||||||
|
Grab Next Subnet
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<HTMLInputElement>;
|
||||||
|
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<AvailabilityType>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAvailability(undefined);
|
||||||
|
|
||||||
|
if (!subnet_ip_pattern.test(value)) return;
|
||||||
|
if (current_value && value == current_value) return;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
fetchApi<ApiReqParams, AvailabilityType>(
|
||||||
|
`/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 (
|
||||||
|
<Stack className="w-full gap-2">
|
||||||
|
<Input
|
||||||
|
defaultValue={value}
|
||||||
|
changeHandler={(v) => {
|
||||||
|
onChange?.(v);
|
||||||
|
}}
|
||||||
|
prefix={<Network size={17} opacity={0.5} />}
|
||||||
|
suffix={
|
||||||
|
<Span className="text-sm opacity-50 whitespace-nowrap">
|
||||||
|
WireGuard IP Address
|
||||||
|
</Span>
|
||||||
|
}
|
||||||
|
componentRef={inputRef}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{ip_status === "invalid" ? (
|
||||||
|
<Tag
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
className="w-full py-1.5 outline-error/50 bg-error/5!"
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
<TriangleAlert size={15} />
|
||||||
|
<Span>Invalid IP</Span>
|
||||||
|
</Row>
|
||||||
|
</Tag>
|
||||||
|
) : ip_status === "current" ? (
|
||||||
|
<Tag
|
||||||
|
variant="outlined"
|
||||||
|
color="gray"
|
||||||
|
className="w-full py-1.5 outline-gray/50 bg-gray/5!"
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
<CircleCheck size={15} />
|
||||||
|
<Span>Current value</Span>
|
||||||
|
</Row>
|
||||||
|
</Tag>
|
||||||
|
) : ip_status === "not_available" ? (
|
||||||
|
<Tag
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
className="w-full py-1.5 outline-error/50 bg-error/5!"
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
<TriangleAlert size={15} />
|
||||||
|
<Span>Not available</Span>
|
||||||
|
</Row>
|
||||||
|
</Tag>
|
||||||
|
) : ip_status === "available" ? (
|
||||||
|
<Tag
|
||||||
|
variant="outlined"
|
||||||
|
color="success"
|
||||||
|
className="w-full py-1.5 outline-success/50 bg-success/5!"
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
<CircleCheck size={15} />
|
||||||
|
<Span>Available</Span>
|
||||||
|
</Row>
|
||||||
|
</Tag>
|
||||||
|
) : (
|
||||||
|
<Tag
|
||||||
|
variant="outlined"
|
||||||
|
color="gray"
|
||||||
|
className="w-full py-1.5 outline-gray/50 bg-gray/5!"
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
<Loader2 size={15} className="animate-spin" />
|
||||||
|
<Span>Checking availability…</Span>
|
||||||
|
</Row>
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 Button from "../twui/layout/Button";
|
||||||
import useStatus from "../twui/hooks/useStatus";
|
import useStatus from "../twui/hooks/useStatus";
|
||||||
import fetchApi from "../twui/utils/fetch/fetchApi";
|
import fetchApi from "../twui/utils/fetch/fetchApi";
|
||||||
import type { ApiReqParams } from "@/src/types";
|
import type { ApiReqParams } from "@/src/types";
|
||||||
import Row from "../twui/layout/Row";
|
import Row from "../twui/layout/Row";
|
||||||
import Input from "../twui/form/Input";
|
|
||||||
import Stack from "../twui/layout/Stack";
|
import Stack from "../twui/layout/Stack";
|
||||||
import {
|
import {
|
||||||
CircleCheck,
|
CircleCheck,
|
||||||
Loader2,
|
|
||||||
Network,
|
|
||||||
TriangleAlert,
|
TriangleAlert,
|
||||||
Wand2,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Span from "../twui/layout/Span";
|
import Span from "../twui/layout/Span";
|
||||||
import Tag from "../twui/elements/Tag";
|
import Tag from "../twui/elements/Tag";
|
||||||
import { AppData } from "@/src/data/app-data";
|
import { AppData } from "@/src/data/app-data";
|
||||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
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 = {
|
type Props = {
|
||||||
button_props?: Omit<ComponentProps<typeof Button>, "title">;
|
button_props?: Omit<ComponentProps<typeof Button>, "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) {
|
export default function SetupMainHostButton({ button_props }: Props) {
|
||||||
const { loading, setLoading, status, setStatus } = useStatus();
|
const { loading, setLoading, status, setStatus } = useStatus();
|
||||||
|
|
||||||
const [wgIP, setWgIP] = useState<string>(AppData["DefaultPrivateIP"]);
|
const [wgIP, setWgIP] = useState<string>(AppData["DefaultPrivateIP"]);
|
||||||
const [availability, setAvailability] = useState<AvailabilityType>();
|
const [ipStatus, setIpStatus] = useState<HostWgIpFieldStatus>("checking");
|
||||||
const [grabbing, setGrabbing] = useState(false);
|
|
||||||
|
|
||||||
const input_ref = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setAvailability(undefined);
|
|
||||||
|
|
||||||
if (!subnet_ip_pattern.test(wgIP)) return;
|
|
||||||
|
|
||||||
let cancelled = false;
|
|
||||||
|
|
||||||
fetchApi<ApiReqParams, AvailabilityType>(
|
|
||||||
`/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<ApiReqParams, GrabSubnetType>(
|
|
||||||
`/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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className="w-full items-stretch gap-3">
|
<Stack className="w-full items-stretch gap-3">
|
||||||
<Stack className="w-full gap-2">
|
<HostWgIpField
|
||||||
<Input
|
value={wgIP}
|
||||||
defaultValue={wgIP}
|
onChange={setWgIP}
|
||||||
changeHandler={(v) => {
|
onStatus={setIpStatus}
|
||||||
setWgIP(v);
|
autoFocus
|
||||||
}}
|
/>
|
||||||
prefix={<Network size={17} opacity={0.5} />}
|
|
||||||
suffix={
|
|
||||||
<Span className="text-sm opacity-50 whitespace-nowrap">
|
|
||||||
Selected Wireguard IP Address
|
|
||||||
</Span>
|
|
||||||
}
|
|
||||||
componentRef={input_ref}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
|
|
||||||
{ip_status === "invalid" ? (
|
|
||||||
<Tag
|
|
||||||
variant="outlined"
|
|
||||||
color="error"
|
|
||||||
className="w-full py-1.5 outline-error/50 bg-error/5!"
|
|
||||||
>
|
|
||||||
<Row>
|
|
||||||
<TriangleAlert size={15} />
|
|
||||||
<span>Invalid IP</span>
|
|
||||||
</Row>
|
|
||||||
</Tag>
|
|
||||||
) : ip_status === "not_available" ? (
|
|
||||||
<Tag
|
|
||||||
variant="outlined"
|
|
||||||
color="error"
|
|
||||||
className="w-full py-1.5 outline-error/50 bg-error/5!"
|
|
||||||
>
|
|
||||||
<Row>
|
|
||||||
<TriangleAlert size={15} />
|
|
||||||
<span>Not available</span>
|
|
||||||
</Row>
|
|
||||||
</Tag>
|
|
||||||
) : ip_status === "available" ? (
|
|
||||||
<Tag
|
|
||||||
variant="outlined"
|
|
||||||
color="success"
|
|
||||||
className="w-full py-1.5 outline-success/50 bg-success/5!"
|
|
||||||
>
|
|
||||||
<Row>
|
|
||||||
<CircleCheck size={15} />
|
|
||||||
<span>Available</span>
|
|
||||||
</Row>
|
|
||||||
</Tag>
|
|
||||||
) : (
|
|
||||||
<Tag
|
|
||||||
variant="outlined"
|
|
||||||
color="gray"
|
|
||||||
className="w-full py-1.5 outline-gray/50 bg-gray/5!"
|
|
||||||
>
|
|
||||||
<Row>
|
|
||||||
<Loader2 size={15} className="animate-spin" />
|
|
||||||
<span>Checking availability…</span>
|
|
||||||
</Row>
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{status?.error && status.msg ? (
|
{status?.error && status.msg ? (
|
||||||
<Tag
|
<Tag
|
||||||
@@ -168,7 +45,7 @@ export default function SetupMainHostButton({ button_props }: Props) {
|
|||||||
>
|
>
|
||||||
<Row>
|
<Row>
|
||||||
<TriangleAlert size={15} />
|
<TriangleAlert size={15} />
|
||||||
<span>{status.msg}</span>
|
<Span>{status.msg}</Span>
|
||||||
</Row>
|
</Row>
|
||||||
</Tag>
|
</Tag>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -180,27 +57,18 @@ export default function SetupMainHostButton({ button_props }: Props) {
|
|||||||
>
|
>
|
||||||
<Row>
|
<Row>
|
||||||
<CircleCheck size={15} />
|
<CircleCheck size={15} />
|
||||||
<span>{status.msg}</span>
|
<Span>{status.msg}</Span>
|
||||||
</Row>
|
</Row>
|
||||||
</Tag>
|
</Tag>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Row className="w-full items-center gap-2">
|
<Row className="w-full items-center gap-2">
|
||||||
<Button
|
<GrabNextSubnetButton onGrab={setWgIP} />
|
||||||
title="Grab Next Available Subnet"
|
|
||||||
variant="outlined"
|
|
||||||
color="primary"
|
|
||||||
beforeIcon={<Wand2 size={16} />}
|
|
||||||
loading={grabbing}
|
|
||||||
onClick={grabNextSubnet}
|
|
||||||
>
|
|
||||||
Grab Next Subnet
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
title="Setup Main Host"
|
title="Setup Main Host"
|
||||||
{...button_props}
|
{...button_props}
|
||||||
disabled={ip_status !== "available"}
|
disabled={ipStatus !== "available"}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
|
||||||
|
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||||
|
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||||
|
import type { TableType, User } from "@/src/types";
|
||||||
|
import setupWireguardHost from "./setup-wireguard-host";
|
||||||
|
import grabHostPublicIPAddress from "./grab-host-public-ip-address";
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
wg_ip_address?: string | null;
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new WireGuard host record and run the
|
||||||
|
* full host setup (keys, config, iptables, tunnel).
|
||||||
|
*
|
||||||
|
* The host record is rolled back if the setup fails.
|
||||||
|
* @param param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default async function createWireguardHost({
|
||||||
|
wg_ip_address,
|
||||||
|
user,
|
||||||
|
}: Params): Promise<APIResponseObject> {
|
||||||
|
const wg_ip = (wg_ip_address || "").trim();
|
||||||
|
|
||||||
|
if (!wg_ip) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `No WireGuard IP address provided`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const public_ip_address = await grabHostPublicIPAddress();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const insert_host = await BunSQLite.insert<
|
||||||
|
BUN_SQLITE_WGUI_HOSTS,
|
||||||
|
TableType
|
||||||
|
>({
|
||||||
|
table: "hosts",
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
user_id: user.id,
|
||||||
|
wg_ip_address: wg_ip,
|
||||||
|
public_ip_address: public_ip_address || undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!insert_host.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Could not create the host record: ${insert_host.msg}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const host_id = Number(insert_host.postInsertReturn?.insertId);
|
||||||
|
|
||||||
|
const host: BUN_SQLITE_WGUI_HOSTS = {
|
||||||
|
id: host_id,
|
||||||
|
user_id: user.id,
|
||||||
|
wg_ip_address: wg_ip,
|
||||||
|
public_ip_address: public_ip_address || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const setup_res = await setupWireguardHost({
|
||||||
|
host,
|
||||||
|
wg_subnet_ip: wg_ip,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!setup_res.success) {
|
||||||
|
await BunSQLite.delete<BUN_SQLITE_WGUI_HOSTS, TableType>({
|
||||||
|
table: "hosts",
|
||||||
|
targetId: host_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: setup_res.msg,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
msg: `New host created`,
|
||||||
|
numberRes: host_id,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import type {
|
||||||
|
BUN_SQLITE_WGUI_HOSTS,
|
||||||
|
BUN_SQLITE_WGUI_VARIABLES,
|
||||||
|
} from "@/db/types/db";
|
||||||
|
import { AppData } from "@/src/data/app-data";
|
||||||
|
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||||
|
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||||
|
import type { TableType, User } from "@/src/types";
|
||||||
|
import checkPrivateIPAvailability from "./check-private-ip-availability";
|
||||||
|
import setupWireguardHost from "./setup-wireguard-host";
|
||||||
|
|
||||||
|
const IP_V4_REGEX = /^\d{1,3}(\.\d{1,3}){3}$/;
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
host_id?: string | number | null;
|
||||||
|
wg_ip_address?: string | null;
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the private WireGuard subnet IP of a host
|
||||||
|
* and regenerate the host config + restart the tunnel.
|
||||||
|
*
|
||||||
|
* For the main host (id 0) the value is stored in the
|
||||||
|
* variables table, otherwise the hosts record is updated.
|
||||||
|
* @param param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default async function updateWireguardHost({
|
||||||
|
host_id,
|
||||||
|
wg_ip_address,
|
||||||
|
user,
|
||||||
|
}: Params): Promise<APIResponseObject> {
|
||||||
|
const final_host_id = Number(host_id || AppData["WireguardHostID"]);
|
||||||
|
const ip = (wg_ip_address || "").trim();
|
||||||
|
|
||||||
|
if (!ip || !IP_V4_REGEX.test(ip)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Invalid WireGuard IP address provided`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let host: BUN_SQLITE_WGUI_HOSTS | undefined;
|
||||||
|
let current_wg_ip: string | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (final_host_id == AppData["WireguardHostID"]) {
|
||||||
|
const variables_res = await BunSQLite.select<
|
||||||
|
BUN_SQLITE_WGUI_VARIABLES,
|
||||||
|
TableType
|
||||||
|
>({
|
||||||
|
table: "variables",
|
||||||
|
});
|
||||||
|
|
||||||
|
current_wg_ip = variables_res.payload?.find(
|
||||||
|
(v) => v.key == "main_host_wg_ip_address",
|
||||||
|
)?.value;
|
||||||
|
} else {
|
||||||
|
const host_res = await BunSQLite.select<
|
||||||
|
BUN_SQLITE_WGUI_HOSTS,
|
||||||
|
TableType
|
||||||
|
>({
|
||||||
|
table: "hosts",
|
||||||
|
targetId: final_host_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
host = host_res.singleRes || undefined;
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Host record not found`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
current_wg_ip = host.wg_ip_address || undefined;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ip != current_wg_ip) {
|
||||||
|
const availability_res = await checkPrivateIPAvailability({
|
||||||
|
ip_address: ip,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!availability_res.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: availability_res.msg,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (final_host_id == AppData["WireguardHostID"]) {
|
||||||
|
const update_variables = await BunSQLite.insert<
|
||||||
|
BUN_SQLITE_WGUI_VARIABLES,
|
||||||
|
TableType
|
||||||
|
>({
|
||||||
|
table: "variables",
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
key: "main_host_wg_ip_address",
|
||||||
|
value: ip,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
update_on_duplicate: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!update_variables.success) {
|
||||||
|
throw new Error(`Couldn't update host variables`);
|
||||||
|
}
|
||||||
|
|
||||||
|
host = {
|
||||||
|
id: AppData["WireguardHostID"],
|
||||||
|
user_id: user.id,
|
||||||
|
wg_ip_address: ip,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const update_host = await BunSQLite.insert<
|
||||||
|
BUN_SQLITE_WGUI_HOSTS,
|
||||||
|
TableType
|
||||||
|
>({
|
||||||
|
table: "hosts",
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: host?.id,
|
||||||
|
user_id: user.id,
|
||||||
|
wg_ip_address: ip,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
update_on_duplicate: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!update_host.success) {
|
||||||
|
throw new Error(`Couldn't update host record`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host) {
|
||||||
|
host.wg_ip_address = ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let setup_res: APIResponseObject;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setup_res = await setupWireguardHost({
|
||||||
|
host,
|
||||||
|
user,
|
||||||
|
is_update_after_client_setup: true,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!setup_res.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: setup_res.msg,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
msg: `Host IP updated to ${ip}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
|
||||||
|
import adminCrudHandler from "@/src/functions/frontend/admin-crud-handler";
|
||||||
|
import type { AddClientFormData } from "@/src/types";
|
||||||
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
setBusy: Dispatch<SetStateAction<boolean>>;
|
||||||
|
busy: boolean;
|
||||||
|
setError: Dispatch<SetStateAction<string | undefined>>;
|
||||||
|
error?: string;
|
||||||
|
data: AddClientFormData;
|
||||||
|
open: boolean;
|
||||||
|
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
onCreated?: () => void;
|
||||||
|
host_id: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function submitClientForm({
|
||||||
|
busy,
|
||||||
|
setBusy,
|
||||||
|
setError,
|
||||||
|
error,
|
||||||
|
data,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
onCreated,
|
||||||
|
host_id,
|
||||||
|
}: Params) {
|
||||||
|
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 || "",
|
||||||
|
host_id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
setBusy(false);
|
||||||
|
|
||||||
|
if (!res.success) {
|
||||||
|
setError(res.msg || "Could not add client");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
onCreated?.();
|
||||||
|
}
|
||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { useState, type Dispatch, type SetStateAction } from "react";
|
||||||
useState,
|
|
||||||
type Dispatch,
|
|
||||||
type ReactNode,
|
|
||||||
type SetStateAction,
|
|
||||||
} from "react";
|
|
||||||
import { X } from "lucide-react";
|
|
||||||
import Modal from "@/src/components/twui/elements/Modal";
|
import Modal from "@/src/components/twui/elements/Modal";
|
||||||
import Input from "@/src/components/twui/form/Input";
|
import Input from "@/src/components/twui/form/Input";
|
||||||
import AdminButton from "@/src/components/general/admin-button";
|
import AdminButton from "@/src/components/general/admin-button";
|
||||||
@@ -13,8 +7,12 @@ import Stack from "@/src/components/twui/layout/Stack";
|
|||||||
import Span from "@/src/components/twui/layout/Span";
|
import Span from "@/src/components/twui/layout/Span";
|
||||||
import H3 from "@/src/components/twui/layout/H3";
|
import H3 from "@/src/components/twui/layout/H3";
|
||||||
import Row from "@/src/components/twui/layout/Row";
|
import Row from "@/src/components/twui/layout/Row";
|
||||||
import adminCrudHandler from "@/src/functions/frontend/admin-crud-handler";
|
import type { AddClientFormData } from "@/src/types";
|
||||||
import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
|
import submitClientForm from "../(functions)/submit-client-form";
|
||||||
|
import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get";
|
||||||
|
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
|
||||||
|
import Select from "@/src/components/twui/form/Select";
|
||||||
|
import { Globe } from "lucide-react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -22,49 +20,27 @@ type Props = {
|
|||||||
onCreated?: () => void;
|
onCreated?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormFieldProps = {
|
|
||||||
label: string;
|
|
||||||
htmlFor: string;
|
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
type FormData = {
|
|
||||||
client_name?: string;
|
|
||||||
tunnel_ip?: string;
|
|
||||||
allowed_ips?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ClientFormModal({ open, setOpen, onCreated }: Props) {
|
export default function ClientFormModal({ open, setOpen, onCreated }: Props) {
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState<string>();
|
const [error, setError] = useState<string>();
|
||||||
|
|
||||||
async function handleSubmit(data: FormData) {
|
const [host_id, set_host_id] = useState(0);
|
||||||
setBusy(true);
|
|
||||||
setError(undefined);
|
|
||||||
|
|
||||||
const res = await adminCrudHandler<BUN_SQLITE_WGUI_CLIENTS>({
|
const { res: additional_hosts } = useAdminCrudGet<BUN_SQLITE_WGUI_HOSTS>({
|
||||||
action: "insert",
|
table: "hosts",
|
||||||
table: "clients",
|
});
|
||||||
insert_data: [
|
|
||||||
{
|
|
||||||
name: data.client_name || "",
|
|
||||||
wg_ip_address: data.tunnel_ip || "",
|
|
||||||
allowed_ips: data.allowed_ips || "",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
setBusy(false);
|
if (!additional_hosts) {
|
||||||
|
return null;
|
||||||
if (!res.success) {
|
|
||||||
setError(res.msg || "Could not add client");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setOpen(false);
|
|
||||||
onCreated?.();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hosts: BUN_SQLITE_WGUI_HOSTS[] = [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
...additional_hosts,
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={open} setOpen={setOpen} className="p-6">
|
<Modal open={open} setOpen={setOpen} className="p-6">
|
||||||
<Stack className="gap-4 mb-6">
|
<Stack className="gap-4 mb-6">
|
||||||
@@ -78,10 +54,35 @@ export default function ClientFormModal({ open, setOpen, onCreated }: Props) {
|
|||||||
|
|
||||||
<Form
|
<Form
|
||||||
submitHandler={(e, data) => {
|
submitHandler={(e, data) => {
|
||||||
handleSubmit(data as FormData);
|
submitClientForm({
|
||||||
|
data: data as AddClientFormData,
|
||||||
|
busy,
|
||||||
|
open,
|
||||||
|
setBusy,
|
||||||
|
setError,
|
||||||
|
setOpen,
|
||||||
|
error,
|
||||||
|
onCreated,
|
||||||
|
host_id,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack className="w-full items-stretch gap-6">
|
<Stack className="w-full items-stretch gap-6">
|
||||||
|
<Select
|
||||||
|
options={hosts.map((h) => ({
|
||||||
|
value: (h.id || 0).toString(),
|
||||||
|
title: h.id == 0 ? `Main Host` : `Host ${h.id}`,
|
||||||
|
}))}
|
||||||
|
changeHandler={(v) => {
|
||||||
|
const selected_host = hosts.find(
|
||||||
|
(h) => h.id == Number(v),
|
||||||
|
);
|
||||||
|
if (typeof selected_host?.id == "number") {
|
||||||
|
set_host_id(selected_host.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
name="client_name"
|
name="client_name"
|
||||||
id="client_name"
|
id="client_name"
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { Server } from "lucide-react";
|
||||||
|
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
|
||||||
|
import AdminCard from "@/src/components/general/admin-card";
|
||||||
|
import Button from "@/src/components/twui/layout/Button";
|
||||||
|
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";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
host: BUN_SQLITE_WGUI_HOSTS;
|
||||||
|
client_count?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function OtherHostCard({ host, client_count }: Props) {
|
||||||
|
const host_id = host.id || 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminCard className="w-full p-5 flex flex-col gap-4">
|
||||||
|
<Row className="justify-between items-center gap-3 flex-wrap">
|
||||||
|
<Stack className="gap-0.5">
|
||||||
|
<Row className="gap-2 items-center">
|
||||||
|
<Server size={15} className="opacity-50" />
|
||||||
|
<Span className="text-[14px] font-semibold leading-none">
|
||||||
|
Host #{host_id}
|
||||||
|
</Span>
|
||||||
|
</Row>
|
||||||
|
<Span className="font-mono text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45">
|
||||||
|
{host.wg_ip_address || "Not configured"}
|
||||||
|
</Span>
|
||||||
|
</Stack>
|
||||||
|
<Row className="gap-2">
|
||||||
|
<Button
|
||||||
|
title="View Host"
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
href={`/admin/hosts/${host_id}`}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
title="Edit Host"
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
href={`/admin/hosts/${host_id}/edit`}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
<Row className="gap-2 items-center flex-wrap">
|
||||||
|
<Tag variant="outlined" color="gray">
|
||||||
|
{client_count || 0}{" "}
|
||||||
|
{client_count == 1 ? "client" : "clients"}
|
||||||
|
</Tag>
|
||||||
|
{host.public_ip_address ? (
|
||||||
|
<Tag
|
||||||
|
variant="outlined"
|
||||||
|
color="success"
|
||||||
|
className="outline-success/50 bg-success/5!"
|
||||||
|
>
|
||||||
|
{host.public_ip_address}
|
||||||
|
</Tag>
|
||||||
|
) : null}
|
||||||
|
</Row>
|
||||||
|
</AdminCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -104,7 +104,7 @@ export default function HostPublicIpSection({ host_id, current_ip }: Props) {
|
|||||||
placeholder="e.g. 203.0.113.10"
|
placeholder="e.g. 203.0.113.10"
|
||||||
label="New public IP"
|
label="New public IP"
|
||||||
showLabel
|
showLabel
|
||||||
value={ip}
|
defaultValue={ip}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setTyped(true);
|
setTyped(true);
|
||||||
setIp(e.target.value);
|
setIp(e.target.value);
|
||||||
|
|||||||
@@ -9,14 +9,16 @@ import type {
|
|||||||
BUN_SQLITE_WGUI_CLIENTS,
|
BUN_SQLITE_WGUI_CLIENTS,
|
||||||
BUN_SQLITE_WGUI_VARIABLES,
|
BUN_SQLITE_WGUI_VARIABLES,
|
||||||
} from "@/db/types/db";
|
} from "@/db/types/db";
|
||||||
|
import type { BUN_SQLITE_WGUI_HOSTS_JOIN } from "@/src/types/sql-joins";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
host?: BUN_SQLITE_WGUI_HOSTS_JOIN;
|
||||||
variables?: BUN_SQLITE_WGUI_VARIABLES[];
|
variables?: BUN_SQLITE_WGUI_VARIABLES[];
|
||||||
clients?: BUN_SQLITE_WGUI_CLIENTS[];
|
clients?: BUN_SQLITE_WGUI_CLIENTS[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MainHostStatusSection({ variables, clients }: Props) {
|
export default function MainHostStatusSection({ host, variables, clients }: Props) {
|
||||||
const config = deriveHostConfig({ variables, clients });
|
const config = deriveHostConfig({ host, variables, clients });
|
||||||
|
|
||||||
const stats = [
|
const stats = [
|
||||||
{
|
{
|
||||||
|
|||||||
+3
-3
@@ -16,11 +16,11 @@ export default function ClientRulePortsField({ draft, setDraft }: Props) {
|
|||||||
title="Ports"
|
title="Ports"
|
||||||
placeholder="80,443 or 8000:8080"
|
placeholder="80,443 or 8000:8080"
|
||||||
showLabel
|
showLabel
|
||||||
value={draft.ports}
|
defaultValue={draft.ports}
|
||||||
onChange={(e) => {
|
changeHandler={(value) => {
|
||||||
setDraft({
|
setDraft({
|
||||||
...draft,
|
...draft,
|
||||||
ports: e.target.value,
|
ports: value,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
info="Leave empty for any port."
|
info="Leave empty for any port."
|
||||||
|
|||||||
+4
-4
@@ -44,14 +44,14 @@ export default function AddClientFormWgIpAddress({
|
|||||||
id="add_client_wg_ip_address"
|
id="add_client_wg_ip_address"
|
||||||
title="WireGuard IP Address"
|
title="WireGuard IP Address"
|
||||||
placeholder="Eg. 10.0.0.2"
|
placeholder="Eg. 10.0.0.2"
|
||||||
onChange={(e) => {
|
changeHandler={(v) => {
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
wg_ip_address: e.target.value,
|
wg_ip_address: v,
|
||||||
allowed_ips: `${e.target.value.replace(/\.\d+$/, "")}.0/24`,
|
allowed_ips: `${v.replace(/\.\d+$/, "")}.0/24`,
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
value={form.wg_ip_address}
|
defaultValue={form.wg_ip_address}
|
||||||
showLabel
|
showLabel
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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<string>(host?.wg_ip_address || "");
|
||||||
|
const [ipStatus, setIpStatus] = useState<HostWgIpFieldStatus>("checking");
|
||||||
|
|
||||||
|
function handleSave() {
|
||||||
|
if (!window.confirm("Update this host configuration?")) return;
|
||||||
|
|
||||||
|
setStatus(undefined);
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
fetchApi<ApiReqParams, APIResponseObject>(
|
||||||
|
`/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 (
|
||||||
|
<>
|
||||||
|
<AdminCard className="w-full p-5 flex flex-col gap-4">
|
||||||
|
<Stack className="gap-1">
|
||||||
|
<H3 className="text-[14px] font-semibold mb-0!">
|
||||||
|
WireGuard subnet
|
||||||
|
</H3>
|
||||||
|
<P
|
||||||
|
noMargin
|
||||||
|
className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45"
|
||||||
|
>
|
||||||
|
The private subnet the host listens on (e.g.
|
||||||
|
10.1.0.1/24). Updating it rewrites the host config and
|
||||||
|
restarts the tunnel.
|
||||||
|
</P>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<HostWgIpField
|
||||||
|
value={wgIP}
|
||||||
|
onChange={setWgIP}
|
||||||
|
onStatus={setIpStatus}
|
||||||
|
current_value={host?.wg_ip_address || ""}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
|
||||||
|
{status?.error && status.msg ? (
|
||||||
|
<Tag
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
className="w-full py-1.5 outline-error/50 bg-error/5!"
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
<TriangleAlert size={15} />
|
||||||
|
<Span>{status.msg}</Span>
|
||||||
|
</Row>
|
||||||
|
</Tag>
|
||||||
|
) : null}
|
||||||
|
{status?.success && status.msg ? (
|
||||||
|
<Tag
|
||||||
|
variant="outlined"
|
||||||
|
color="success"
|
||||||
|
className="w-full py-1.5 outline-success/50 bg-success/5!"
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
<CircleCheck size={15} />
|
||||||
|
<Span>{status.msg}</Span>
|
||||||
|
</Row>
|
||||||
|
</Tag>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Row className="w-full items-center gap-2">
|
||||||
|
<Button
|
||||||
|
title="Save Changes"
|
||||||
|
disabled={
|
||||||
|
ipStatus !== "available" && ipStatus !== "current"
|
||||||
|
}
|
||||||
|
loading={loading}
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</AdminCard>
|
||||||
|
|
||||||
|
<HostPublicIpSection
|
||||||
|
host_id={host_id}
|
||||||
|
current_ip={host?.public_ip_address || ""}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<PagePropsType> = 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;
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<AdminHero
|
||||||
|
title="Edit Host"
|
||||||
|
description={
|
||||||
|
pageProps?.host?.wg_ip_address
|
||||||
|
? `Edit host ${host_id} configuration`
|
||||||
|
: "Edit host configuration"
|
||||||
|
}
|
||||||
|
buttons={
|
||||||
|
<Button
|
||||||
|
title="View host"
|
||||||
|
variant="outlined"
|
||||||
|
beforeIcon={<ArrowLeft />}
|
||||||
|
href={`/admin/hosts/${host_id}`}
|
||||||
|
>
|
||||||
|
View host
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Divider className="mb-6" />
|
||||||
|
|
||||||
|
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
|
||||||
|
{pageProps?.host ? (
|
||||||
|
<EditHostFormSection />
|
||||||
|
) : (
|
||||||
|
<EmptyContent title="Host not found" />
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const meta: BunextPageModuleMeta = {
|
||||||
|
title: `Edit Host | ${SiteData["SiteName"]}`,
|
||||||
|
description: `WireGuard edit host page`,
|
||||||
|
};
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import type {
|
import type {
|
||||||
BUN_SQLITE_WGUI_CLIENTS,
|
BUN_SQLITE_WGUI_CLIENTS,
|
||||||
BUN_SQLITE_WGUI_HOSTS,
|
BUN_SQLITE_WGUI_HOSTS,
|
||||||
|
BUN_SQLITE_WGUI_VARIABLES,
|
||||||
} from "@/db/types/db";
|
} from "@/db/types/db";
|
||||||
|
import grabHostDirnames from "@/src/functions/backend/setup/grab-host-dir-names";
|
||||||
import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
|
import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
|
||||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||||
@@ -18,6 +20,15 @@ const server: BunextPageServerFn<PagePropsType> = async ({
|
|||||||
TableType
|
TableType
|
||||||
>({ table: "hosts", targetId: page_query.host_id });
|
>({ 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<
|
const host_clients = await BunSQLite.select<
|
||||||
BUN_SQLITE_WGUI_CLIENTS,
|
BUN_SQLITE_WGUI_CLIENTS,
|
||||||
TableType
|
TableType
|
||||||
@@ -32,10 +43,16 @@ const server: BunextPageServerFn<PagePropsType> = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const variables_res = await BunSQLite.select<
|
||||||
|
BUN_SQLITE_WGUI_VARIABLES,
|
||||||
|
TableType
|
||||||
|
>({ table: "variables" });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
host: host_record_res.singleRes || null,
|
host,
|
||||||
clients: host_clients.payload || null,
|
clients: host_clients.payload || null,
|
||||||
|
variables: variables_res.payload || null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +1,72 @@
|
|||||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||||
|
import { useContext } from "react";
|
||||||
import AdminHero from "@/src/components/general/admin-hero";
|
import AdminHero from "@/src/components/general/admin-hero";
|
||||||
import { SiteData } from "@/src/data/site-data";
|
import { SiteData } from "@/src/data/site-data";
|
||||||
import Button from "@/src/components/twui/layout/Button";
|
import Button from "@/src/components/twui/layout/Button";
|
||||||
import Divider from "@/src/components/twui/layout/Divider";
|
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 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 { 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() {
|
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 (
|
||||||
|
<>
|
||||||
|
<AdminHero
|
||||||
|
title="Host"
|
||||||
|
description="WireGuard server configuration and interface details"
|
||||||
|
/>
|
||||||
|
<Divider className="mb-6" />
|
||||||
|
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
|
||||||
|
<EmptyContent title="Host not found" />
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AdminHero
|
<AdminHero
|
||||||
title="Host"
|
title={is_main_host ? "Host" : `Host #${host_id}`}
|
||||||
description="WireGuard server configuration and interface details"
|
description="WireGuard server configuration and interface details"
|
||||||
buttons={
|
buttons={
|
||||||
<>
|
<>
|
||||||
<Button title="Add New Host" variant="outlined">
|
<Button
|
||||||
|
title="View Clients"
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
href={`/admin/hosts/${host_id}/clients`}
|
||||||
|
>
|
||||||
|
View Clients
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
title="Edit Host"
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
href={`/admin/hosts/${host_id}/edit`}
|
||||||
|
>
|
||||||
|
Edit Host
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
title="Add New Host"
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
href="/admin/hosts/add"
|
||||||
|
>
|
||||||
Add New Host
|
Add New Host
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
@@ -30,12 +76,26 @@ export default function AdminSingleHostPage() {
|
|||||||
<Divider className="mb-6" />
|
<Divider className="mb-6" />
|
||||||
|
|
||||||
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
|
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
|
||||||
{pageProps?.host ? (
|
<MainHostStatusSection
|
||||||
<HostPublicIpSection
|
host={host}
|
||||||
host_id={host_id}
|
variables={variables}
|
||||||
current_ip={pageProps.host.public_ip_address || ""}
|
clients={clients}
|
||||||
/>
|
/>
|
||||||
) : null}
|
<HostPublicIpSection
|
||||||
|
host_id={host_id}
|
||||||
|
current_ip={
|
||||||
|
host?.public_ip_address ||
|
||||||
|
variables.find(
|
||||||
|
(v) => v.key == "main_host_public_ip_address",
|
||||||
|
)?.value ||
|
||||||
|
""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InterfaceConfigSection
|
||||||
|
host={host}
|
||||||
|
variables={variables}
|
||||||
|
clients={clients}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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<string>(AppData["DefaultPrivateIP"]);
|
||||||
|
const [ipStatus, setIpStatus] = useState<HostWgIpFieldStatus>("checking");
|
||||||
|
|
||||||
|
function handleCreate() {
|
||||||
|
if (!window.confirm("Create this new host?")) return;
|
||||||
|
|
||||||
|
setStatus(undefined);
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
fetchApi<ApiReqParams, APIResponseObject>(
|
||||||
|
`/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 (
|
||||||
|
<AdminCard className="w-full p-5 flex flex-col gap-4">
|
||||||
|
<Stack className="gap-1">
|
||||||
|
<H3 className="text-[14px] font-semibold mb-0!">
|
||||||
|
WireGuard subnet
|
||||||
|
</H3>
|
||||||
|
<P
|
||||||
|
noMargin
|
||||||
|
className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45"
|
||||||
|
>
|
||||||
|
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.
|
||||||
|
</P>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<HostWgIpField
|
||||||
|
value={wgIP}
|
||||||
|
onChange={setWgIP}
|
||||||
|
onStatus={setIpStatus}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
|
||||||
|
{status?.error && status.msg ? (
|
||||||
|
<Tag
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
className="w-full py-1.5 outline-error/50 bg-error/5!"
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
<TriangleAlert size={15} />
|
||||||
|
<Span>{status.msg}</Span>
|
||||||
|
</Row>
|
||||||
|
</Tag>
|
||||||
|
) : null}
|
||||||
|
{status?.success && status.msg ? (
|
||||||
|
<Tag
|
||||||
|
variant="outlined"
|
||||||
|
color="success"
|
||||||
|
className="w-full py-1.5 outline-success/50 bg-success/5!"
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
<CircleCheck size={15} />
|
||||||
|
<Span>{status.msg}</Span>
|
||||||
|
</Row>
|
||||||
|
</Tag>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Row className="w-full items-center gap-2">
|
||||||
|
<GrabNextSubnetButton onGrab={setWgIP} />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
title="Create Host"
|
||||||
|
disabled={ipStatus !== "available"}
|
||||||
|
loading={loading}
|
||||||
|
onClick={handleCreate}
|
||||||
|
>
|
||||||
|
Create Host
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</AdminCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<AdminHero
|
||||||
|
title="Add New Host"
|
||||||
|
description="Create a new WireGuard host with a dedicated private subnet"
|
||||||
|
buttons={
|
||||||
|
<Button
|
||||||
|
title="Back to Hosts"
|
||||||
|
variant="outlined"
|
||||||
|
beforeIcon={<ArrowLeft />}
|
||||||
|
href="/admin/hosts"
|
||||||
|
>
|
||||||
|
Back to Hosts
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Divider className="mb-6" />
|
||||||
|
|
||||||
|
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
|
||||||
|
<AddHostFormSection />
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const meta: BunextPageModuleMeta = {
|
||||||
|
title: `Add New Host | ${SiteData["SiteName"]}`,
|
||||||
|
description: `WireGuard add host page`,
|
||||||
|
};
|
||||||
@@ -15,10 +15,13 @@ import SetupMainHostButton from "@/src/components/general/setup-main-host-button
|
|||||||
import Row from "@/src/components/twui/layout/Row";
|
import Row from "@/src/components/twui/layout/Row";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import HostPublicIpSection from "./(sections)/host-public-ip-section";
|
import HostPublicIpSection from "./(sections)/host-public-ip-section";
|
||||||
|
import OtherHostCard from "./(partials)/other-host-card";
|
||||||
|
|
||||||
export default function AdminHostPage() {
|
export default function AdminHostPage() {
|
||||||
const { hosts, variables, clients } = useHostData();
|
const { hosts, variables, clients } = useHostData();
|
||||||
|
|
||||||
|
const other_hosts = (hosts || []).filter((host) => (host.id || 0) != 0);
|
||||||
|
|
||||||
const main_host_public_ip = variables?.find(
|
const main_host_public_ip = variables?.find(
|
||||||
(v) => v.key == "main_host_public_ip_address",
|
(v) => v.key == "main_host_public_ip_address",
|
||||||
)?.value;
|
)?.value;
|
||||||
@@ -48,7 +51,11 @@ export default function AdminHostPage() {
|
|||||||
description="WireGuard server configuration and interface details"
|
description="WireGuard server configuration and interface details"
|
||||||
buttons={
|
buttons={
|
||||||
<>
|
<>
|
||||||
<Button title="Add New Host" variant="outlined">
|
<Button
|
||||||
|
title="Add New Host"
|
||||||
|
variant="outlined"
|
||||||
|
href="/admin/hosts/add"
|
||||||
|
>
|
||||||
Add New Host
|
Add New Host
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
@@ -97,8 +104,22 @@ export default function AdminHostPage() {
|
|||||||
<Divider className="mb-6" />
|
<Divider className="mb-6" />
|
||||||
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
|
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
|
||||||
<H2>Other Hosts</H2>
|
<H2>Other Hosts</H2>
|
||||||
{hosts?.[0] ? (
|
{other_hosts[0] ? (
|
||||||
<></>
|
<Stack className="w-full gap-4 items-stretch">
|
||||||
|
{other_hosts.map((host) => (
|
||||||
|
<OtherHostCard
|
||||||
|
key={host.id}
|
||||||
|
host={host}
|
||||||
|
client_count={
|
||||||
|
clients?.filter(
|
||||||
|
(client) =>
|
||||||
|
(client.host_id || 0) ==
|
||||||
|
(host.id || 0),
|
||||||
|
).length || 0
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<EmptyContent title="No other hosts found" />
|
<EmptyContent title="No other hosts found" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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<APIResponseObject> = async (
|
||||||
|
params,
|
||||||
|
) => {
|
||||||
|
const req = params.req;
|
||||||
|
const body = params.body as ApiReqParams;
|
||||||
|
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user, user_types } = await userAuth({ req });
|
||||||
|
|
||||||
|
if (!user?.logged_in_status) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Unauthorized`,
|
||||||
|
logoutUser: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const is_super_admin = checkUserAccess({ user_types });
|
||||||
|
|
||||||
|
if (!is_super_admin.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Unauthorized`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return await createWireguardHost({
|
||||||
|
wg_ip_address: body?.wg_ip_address,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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<APIResponseObject> = async (
|
||||||
|
params,
|
||||||
|
) => {
|
||||||
|
const req = params.req;
|
||||||
|
const body = params.body as ApiReqParams;
|
||||||
|
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user, user_types } = await userAuth({ req });
|
||||||
|
|
||||||
|
if (!user?.logged_in_status) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Unauthorized`,
|
||||||
|
logoutUser: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const is_super_admin = checkUserAccess({ user_types });
|
||||||
|
|
||||||
|
if (!is_super_admin.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Unauthorized`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
+15
-2
@@ -6,6 +6,7 @@ import type {
|
|||||||
BUN_SQLITE_WGUI_MEDIA,
|
BUN_SQLITE_WGUI_MEDIA,
|
||||||
BUN_SQLITE_WGUI_USER_TYPES,
|
BUN_SQLITE_WGUI_USER_TYPES,
|
||||||
BUN_SQLITE_WGUI_USERS,
|
BUN_SQLITE_WGUI_USERS,
|
||||||
|
BUN_SQLITE_WGUI_VARIABLES,
|
||||||
BunSQLiteTables,
|
BunSQLiteTables,
|
||||||
} from "@/db/types/db";
|
} from "@/db/types/db";
|
||||||
import type { SBFSusidiaries } from "../dict/subsidiaries-dict";
|
import type { SBFSusidiaries } from "../dict/subsidiaries-dict";
|
||||||
@@ -16,7 +17,10 @@ import type {
|
|||||||
ClientRuleTypes,
|
ClientRuleTypes,
|
||||||
} from "../dict/client-rules-dict";
|
} from "../dict/client-rules-dict";
|
||||||
import type { BunextPageModuleServerReturnURLObject } from "@moduletrace/bunext/types";
|
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 { ImageInputToBase64FunctionReturn } from "../components/twui/utils/form/imageInputToBase64";
|
||||||
import type { ServerQueryParam } from "@moduletrace/bun-sqlite/dist/types";
|
import type { ServerQueryParam } from "@moduletrace/bun-sqlite/dist/types";
|
||||||
import type grabHostDirnames from "../functions/backend/setup/grab-host-dir-names";
|
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;
|
user_types?: BUN_SQLITE_WGUI_USER_TYPES[] | null;
|
||||||
person?: BUN_SQLITE_WGUI_USERS_JOIN | null;
|
person?: BUN_SQLITE_WGUI_USERS_JOIN | null;
|
||||||
persons?: 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;
|
hosts?: BUN_SQLITE_WGUI_HOSTS[] | null;
|
||||||
|
variables?: BUN_SQLITE_WGUI_VARIABLES[] | null;
|
||||||
client?: BUN_SQLITE_WGUI_CLIENTS | null;
|
client?: BUN_SQLITE_WGUI_CLIENTS | null;
|
||||||
clients?: BUN_SQLITE_WGUI_CLIENTS[] | null;
|
clients?: BUN_SQLITE_WGUI_CLIENTS[] | null;
|
||||||
client_rules?: BUN_SQLITE_WGUI_CLIENT_RULES[] | null;
|
client_rules?: BUN_SQLITE_WGUI_CLIENT_RULES[] | null;
|
||||||
@@ -243,6 +248,7 @@ export type ApiReqParams<
|
|||||||
media?: BUN_SQLITE_WGUI_MEDIA;
|
media?: BUN_SQLITE_WGUI_MEDIA;
|
||||||
|
|
||||||
ip_address?: string;
|
ip_address?: string;
|
||||||
|
wg_ip_address?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserAuthReturn = {
|
export type UserAuthReturn = {
|
||||||
@@ -327,3 +333,10 @@ export type MediaDataType =
|
|||||||
| ReadableStream
|
| ReadableStream
|
||||||
| Request
|
| Request
|
||||||
| Response;
|
| Response;
|
||||||
|
|
||||||
|
export type AddClientFormData = {
|
||||||
|
client_name?: string;
|
||||||
|
tunnel_ip?: string;
|
||||||
|
allowed_ips?: string;
|
||||||
|
host_id: number;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user