Updates
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import { Pencil, Trash2 } from "lucide-react";
|
||||
import Link from "@/src/components/twui/layout/Link";
|
||||
import Row from "@/src/components/twui/layout/Row";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import formatUnixTimestamp from "@/src/utils/format-unix-timestamp";
|
||||
import adminCrudHandler from "@/src/functions/frontend/admin-crud-handler";
|
||||
import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
|
||||
|
||||
type Props = {
|
||||
@@ -7,12 +12,45 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function ClientRow({ client }: Props) {
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
const host_id = Number(client.host_id || 0);
|
||||
|
||||
async function handleDelete() {
|
||||
if (
|
||||
!window.confirm(
|
||||
`Delete client "${client?.name || `#${client?.id}`}"? The client will be removed from the host and its config file deleted.`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDeleting(true);
|
||||
|
||||
const res = await adminCrudHandler({
|
||||
action: "delete",
|
||||
table: "clients",
|
||||
id: client?.id,
|
||||
});
|
||||
|
||||
if (!res.success) {
|
||||
setDeleting(false);
|
||||
window.alert(res.msg || "Could not delete client");
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className="border-b border-slate-200/60 dark:border-white/5 last:border-b-0 hover:bg-foreground-light/[0.02] dark:hover:bg-foreground-dark/[0.02] transition-colors">
|
||||
<td className="px-4 py-[9px]">
|
||||
<Span className="text-[13.5px] font-medium text-foreground-light dark:text-foreground-dark">
|
||||
<Link
|
||||
href={`/admin/hosts/${host_id}/clients/${client.id}`}
|
||||
className="text-[13.5px] font-medium text-foreground-light dark:text-foreground-dark hover:text-secondary dark:hover:text-secondary"
|
||||
>
|
||||
{client.name || "—"}
|
||||
</Span>
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-4 py-[9px] tabular text-[13px] text-foreground-light/60 dark:text-foreground-dark/60 whitespace-nowrap">
|
||||
{client.wg_ip_address || "—"}
|
||||
@@ -30,6 +68,28 @@ export default function ClientRow({ client }: Props) {
|
||||
<td className="px-4 py-[9px] tabular text-[12px] text-foreground-light/40 dark:text-foreground-dark/40 text-right whitespace-nowrap">
|
||||
{formatUnixTimestamp({ timestamp: client.created_at })}
|
||||
</td>
|
||||
<td className="px-4 py-[9px] text-right">
|
||||
<Row className="gap-1.5 items-center">
|
||||
<Link
|
||||
href={`/admin/hosts/${host_id}/clients/${client.id}/edit`}
|
||||
title={`Edit client ${client.name || `#${client.id}`}`}
|
||||
aria-label={`Edit client ${client.name || `#${client.id}`}`}
|
||||
className="p-1.5 text-foreground-light/40 dark:text-foreground-dark/40 hover:text-foreground-light dark:hover:text-foreground-dark focus:outline-2"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
title={`Delete client ${client.name || `#${client.id}`}`}
|
||||
aria-label={`Delete client ${client.name || `#${client.id}`}`}
|
||||
disabled={deleting}
|
||||
onClick={handleDelete}
|
||||
className="p-1.5 text-error hover:bg-error/10 focus:outline-2 transition-colors duration-150"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</Row>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { type RefObject } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import AceEditorModes from "./ace-editor-modes";
|
||||
// @ts-ignore
|
||||
import { AceEditorOptions } from "@moduletrace/datasquirel/dist/package-shared/types";
|
||||
import type { AceEditorOptions } from "../types";
|
||||
import type { Ace as AceAjax } from "ace-builds";
|
||||
|
||||
export type AceEditorComponentType = {
|
||||
editorRef?: RefObject<AceAjax.Editor | undefined>;
|
||||
@@ -60,6 +60,9 @@ export default function AceEditor({
|
||||
React.useEffect(() => {
|
||||
if (!ready) return;
|
||||
|
||||
// @ts-ignore
|
||||
const ace = window.ace as AceAjax;
|
||||
|
||||
if (!ace?.edit || !editorElementRef.current) {
|
||||
setTimeout(() => {
|
||||
setRefresh((prev) => prev + 1);
|
||||
@@ -95,13 +98,13 @@ export default function AceEditor({
|
||||
editor.commands.addCommand({
|
||||
name: "myCommand",
|
||||
bindKey: { win: "Ctrl-Enter", mac: "Command-Enter" },
|
||||
exec: function (editor) {
|
||||
exec: function (editor: any) {
|
||||
if (ctrlEnterFn) ctrlEnterFn(editor);
|
||||
},
|
||||
readOnly: true,
|
||||
});
|
||||
|
||||
editor.getSession().on("change", function (e) {
|
||||
editor.getSession().on("change", function () {
|
||||
if (onChange) {
|
||||
clearTimeout(timeout);
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import QRCode from "qrcode";
|
||||
|
||||
type Params = {
|
||||
config?: string | null;
|
||||
width?: number;
|
||||
};
|
||||
|
||||
export default async function generateWireguardClientQR({
|
||||
config,
|
||||
width = 320,
|
||||
}: Params) {
|
||||
if (!config) {
|
||||
return {
|
||||
qr_data_uri: null,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const svg = await QRCode.toString(config, {
|
||||
type: "svg",
|
||||
errorCorrectionLevel: "M",
|
||||
width,
|
||||
});
|
||||
|
||||
const qr_data_uri = `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
|
||||
|
||||
return {
|
||||
qr_data_uri,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
qr_data_uri: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
|
||||
import path from "path";
|
||||
import grabDirNames from "@/src/utils/grab-dir-names";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
type Params = {
|
||||
client: BUN_SQLITE_WGUI_CLIENTS;
|
||||
host_id?: number;
|
||||
};
|
||||
|
||||
const {
|
||||
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||
WGUI_LIB_HOST_CLIENTS_DIR_NAME,
|
||||
WIREGUARD_PRIVATE_KEY_FILE_NAME,
|
||||
WIREGUARD_PUBLIC_KEY_FILE_NAME,
|
||||
WIREGUARD_CLIENT_CONFIG_FILE_NAME,
|
||||
} = grabDirNames();
|
||||
|
||||
export default function grabClientDirnames({
|
||||
client,
|
||||
host_id: passed_host_id,
|
||||
}: Params) {
|
||||
const host_id = passed_host_id || 0;
|
||||
|
||||
const CLIENT_WG_IP = client?.wg_ip_address;
|
||||
|
||||
const HOST_CONFIG_DIR = path.join(
|
||||
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||
String(host_id),
|
||||
);
|
||||
|
||||
const HOST_CLIENTS_DIR = path.join(
|
||||
HOST_CONFIG_DIR,
|
||||
WGUI_LIB_HOST_CLIENTS_DIR_NAME,
|
||||
);
|
||||
|
||||
const CLIENT_DIR = path.join(HOST_CLIENTS_DIR, `${client.id}`);
|
||||
|
||||
const CLIENT_PRIVATE_KEY_FILE = path.join(
|
||||
CLIENT_DIR,
|
||||
WIREGUARD_PRIVATE_KEY_FILE_NAME,
|
||||
);
|
||||
const CLIENT_PUBLIC_KEY_FILE = path.join(
|
||||
CLIENT_DIR,
|
||||
WIREGUARD_PUBLIC_KEY_FILE_NAME,
|
||||
);
|
||||
const CLIENT_CONFIG_FILE = path.join(
|
||||
CLIENT_DIR,
|
||||
WIREGUARD_CLIENT_CONFIG_FILE_NAME,
|
||||
);
|
||||
|
||||
const CLIENT_PRIVATE_KEY = execSync(`cat ${CLIENT_PRIVATE_KEY_FILE}`, {
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
const CLIENT_PUBLIC_KEY = execSync(`cat ${CLIENT_PUBLIC_KEY_FILE}`, {
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
const HOST_PUBLIC_KEY = execSync(
|
||||
`cat ${path.join(HOST_CONFIG_DIR, WIREGUARD_PUBLIC_KEY_FILE_NAME)}`,
|
||||
{ encoding: "utf-8" },
|
||||
).trim();
|
||||
|
||||
return {
|
||||
CLIENT_WG_IP,
|
||||
HOST_CLIENTS_DIR,
|
||||
HOST_CONFIG_DIR,
|
||||
CLIENT_DIR,
|
||||
CLIENT_PRIVATE_KEY_FILE,
|
||||
CLIENT_PUBLIC_KEY_FILE,
|
||||
CLIENT_CONFIG_FILE,
|
||||
CLIENT_PRIVATE_KEY,
|
||||
CLIENT_PUBLIC_KEY,
|
||||
HOST_PUBLIC_KEY,
|
||||
};
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import deriveWireguardInterfaceName from "@/src/utils/derive-wireguard-interface
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import { execSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import grabHostDirnames from "./grab-host-dir-names";
|
||||
|
||||
const { WGUI_LIB_HOSTS_CONFIGS_DIR, WGUI_WG_QUICK_MANAGE_SCRIPT } =
|
||||
grabDirNames();
|
||||
@@ -19,11 +20,9 @@ export default function manageWireguardHost({
|
||||
action,
|
||||
host_id = AppData["WireguardHostID"],
|
||||
}: Params): APIResponseObject {
|
||||
const INTERFACE_NAME = deriveWireguardInterfaceName({ host_id });
|
||||
const HOST_CONFIG_PATH = path.join(
|
||||
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||
`${INTERFACE_NAME}.conf`,
|
||||
);
|
||||
const { HOST_CONFIG_FILE, INTERFACE_NAME } = grabHostDirnames({
|
||||
host: { id: host_id },
|
||||
});
|
||||
|
||||
const IS_ROOT =
|
||||
typeof process.getuid === "function" && process.getuid() === 0;
|
||||
@@ -35,7 +34,7 @@ export default function manageWireguardHost({
|
||||
};
|
||||
}
|
||||
|
||||
const MANAGE_COMMAND = `${WGUI_WG_QUICK_MANAGE_SCRIPT} ${action} ${INTERFACE_NAME} ${HOST_CONFIG_PATH}`;
|
||||
const MANAGE_COMMAND = `${WGUI_WG_QUICK_MANAGE_SCRIPT} ${action} ${INTERFACE_NAME} ${HOST_CONFIG_FILE}`;
|
||||
|
||||
try {
|
||||
const output = execSync(MANAGE_COMMAND, { encoding: "utf-8" }).trim();
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import path from "node:path";
|
||||
import grabDirNames from "@/src/utils/grab-dir-names";
|
||||
import grabHostDirnames from "./grab-host-dir-names";
|
||||
|
||||
const { WIREGUARD_CLIENT_CONFIG_FILE_NAME } = grabDirNames();
|
||||
|
||||
type Params = {
|
||||
client_id?: string | number;
|
||||
host_id?: string | number;
|
||||
};
|
||||
|
||||
export default async function readWireguardClientConfig({
|
||||
client_id,
|
||||
host_id,
|
||||
}: Params) {
|
||||
if (!client_id) {
|
||||
return {
|
||||
config: null,
|
||||
config_path: null,
|
||||
};
|
||||
}
|
||||
|
||||
const { HOST_CLIENTS_DIR } = grabHostDirnames({
|
||||
host: host_id ? { id: Number(host_id) } : undefined,
|
||||
});
|
||||
|
||||
const config_path = path.join(
|
||||
HOST_CLIENTS_DIR,
|
||||
String(client_id),
|
||||
WIREGUARD_CLIENT_CONFIG_FILE_NAME,
|
||||
);
|
||||
|
||||
try {
|
||||
const config_file = Bun.file(config_path);
|
||||
|
||||
if (!(await config_file.exists())) {
|
||||
return {
|
||||
config: null,
|
||||
config_path,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
config: await config_file.text(),
|
||||
config_path,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
config: null,
|
||||
config_path,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import setupWireguardHost from "./setup-wireguard-host";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { TableType, User } from "@/src/types";
|
||||
import grabClientDirnames from "./grab-client-dir-names";
|
||||
|
||||
const {
|
||||
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||
@@ -25,6 +26,12 @@ type Params = {
|
||||
client: BUN_SQLITE_WGUI_CLIENTS;
|
||||
host?: BUN_SQLITE_WGUI_HOSTS;
|
||||
user: User;
|
||||
/**
|
||||
* Skip the host setup/restart at the end. Use when
|
||||
* regenerating multiple clients and restarting the
|
||||
* host once afterwards instead.
|
||||
*/
|
||||
skip_host_setup?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -38,6 +45,7 @@ export default async function setupWireguardClient({
|
||||
client,
|
||||
host,
|
||||
user,
|
||||
skip_host_setup,
|
||||
}: Params): Promise<APIResponseObject> {
|
||||
const host_id = host?.id || AppData["WireguardHostID"];
|
||||
|
||||
@@ -50,7 +58,18 @@ export default async function setupWireguardClient({
|
||||
|
||||
const variables = variables_res.payload;
|
||||
|
||||
const CLIENT_WG_IP = client?.wg_ip_address;
|
||||
const {
|
||||
CLIENT_WG_IP,
|
||||
CLIENT_CONFIG_FILE,
|
||||
CLIENT_DIR,
|
||||
CLIENT_PRIVATE_KEY_FILE,
|
||||
CLIENT_PUBLIC_KEY_FILE,
|
||||
HOST_CLIENTS_DIR,
|
||||
HOST_CONFIG_DIR,
|
||||
CLIENT_PRIVATE_KEY,
|
||||
CLIENT_PUBLIC_KEY,
|
||||
HOST_PUBLIC_KEY,
|
||||
} = grabClientDirnames({ client, host_id });
|
||||
|
||||
if (!CLIENT_WG_IP) {
|
||||
return {
|
||||
@@ -59,18 +78,6 @@ export default async function setupWireguardClient({
|
||||
};
|
||||
}
|
||||
|
||||
const HOST_CONFIG_DIR = path.join(
|
||||
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||
String(host_id),
|
||||
);
|
||||
|
||||
const HOST_CLIENTS_DIR = path.join(
|
||||
HOST_CONFIG_DIR,
|
||||
WGUI_LIB_HOST_CLIENTS_DIR_NAME,
|
||||
);
|
||||
|
||||
const CLIENT_DIR = path.join(HOST_CLIENTS_DIR, `${client.id}`);
|
||||
|
||||
const HOST_WG_IP = host?.id
|
||||
? host?.wg_ip_address
|
||||
: variables?.find((v) => v.key == "main_host_wg_ip_address")?.value;
|
||||
@@ -82,19 +89,6 @@ export default async function setupWireguardClient({
|
||||
};
|
||||
}
|
||||
|
||||
const CLIENT_PRIVATE_KEY_FILE = path.join(
|
||||
CLIENT_DIR,
|
||||
WIREGUARD_PRIVATE_KEY_FILE_NAME,
|
||||
);
|
||||
const CLIENT_PUBLIC_KEY_FILE = path.join(
|
||||
CLIENT_DIR,
|
||||
WIREGUARD_PUBLIC_KEY_FILE_NAME,
|
||||
);
|
||||
const CLIENT_CONFIG_FILE = path.join(
|
||||
CLIENT_DIR,
|
||||
WIREGUARD_CLIENT_CONFIG_FILE_NAME,
|
||||
);
|
||||
|
||||
let pre_sh = ``;
|
||||
|
||||
pre_sh += `mkdir -p ${CLIENT_DIR}\n`;
|
||||
@@ -112,20 +106,10 @@ export default async function setupWireguardClient({
|
||||
}
|
||||
|
||||
try {
|
||||
const CLIENT_PRIVATE_KEY = execSync(`cat ${CLIENT_PRIVATE_KEY_FILE}`, {
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
const CLIENT_PUBLIC_KEY = execSync(`cat ${CLIENT_PUBLIC_KEY_FILE}`, {
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
const HOST_PUBLIC_KEY = execSync(
|
||||
`cat ${path.join(HOST_CONFIG_DIR, WIREGUARD_PUBLIC_KEY_FILE_NAME)}`,
|
||||
{ encoding: "utf-8" },
|
||||
).trim();
|
||||
|
||||
const PUBLIC_IP_ADDRESS =
|
||||
host?.public_ip_address ||
|
||||
variables?.find((v) => v.key == "main_host_public_ip_address")
|
||||
?.value ||
|
||||
(await grabHostPublicIPAddress()) ||
|
||||
client?.public_ip_address ||
|
||||
HOST_WG_IP;
|
||||
@@ -138,26 +122,38 @@ export default async function setupWireguardClient({
|
||||
sh += `[Interface]\n`;
|
||||
sh += `Address = ${CLIENT_WG_IP}/32\n`;
|
||||
sh += `PrivateKey = ${CLIENT_PRIVATE_KEY}\n`;
|
||||
sh += `DNS = 1.1.1.1\n`;
|
||||
// sh += `DNS = 1.1.1.1\n`;
|
||||
sh += `\n`;
|
||||
sh += `[Peer]\n`;
|
||||
sh += `PublicKey = ${HOST_PUBLIC_KEY}\n`;
|
||||
sh += `Endpoint = ${PUBLIC_IP_ADDRESS}:51820\n`;
|
||||
|
||||
if (client?.allowed_ips) {
|
||||
if (client?.allow_all_ips == 1) {
|
||||
sh += `AllowedIPs = 0.0.0.0/0, ::/0\n`;
|
||||
} else if (client?.allowed_ips) {
|
||||
sh += `AllowedIPs = ${client.allowed_ips}\n`;
|
||||
}
|
||||
|
||||
sh += `PersistentKeepalive = 25\n`;
|
||||
|
||||
sh += `EOF\n`;
|
||||
sh += `\n`;
|
||||
|
||||
const exec = execSync(sh, { encoding: "utf-8" });
|
||||
const exec = execSync(sh, {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
const CLIENT_CONFIG = execSync(`cat ${CLIENT_CONFIG_FILE}`, {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
const host_setup_res = await setupWireguardHost({ host, user });
|
||||
const host_setup_res = skip_host_setup
|
||||
? { success: true }
|
||||
: await setupWireguardHost({
|
||||
host,
|
||||
user,
|
||||
is_update_after_client_setup: true,
|
||||
});
|
||||
|
||||
if (!host_setup_res.success) {
|
||||
return {
|
||||
@@ -172,6 +168,11 @@ export default async function setupWireguardClient({
|
||||
stringRes: CLIENT_CONFIG,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.log(
|
||||
`Error setting up wireguard client ${client.id} for host ${host_id} =>`,
|
||||
error.message,
|
||||
);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
|
||||
@@ -12,6 +12,8 @@ import type { TableType, User } from "@/src/types";
|
||||
import checkPrivateIPAvailability from "./check-private-ip-availability";
|
||||
import manageWireguardHost from "./manage-wireguard-host";
|
||||
import grabHostDirnames from "./grab-host-dir-names";
|
||||
import path from "node:path";
|
||||
import grabClientDirnames from "./grab-client-dir-names";
|
||||
|
||||
const { WIREGUARD_PRIVATE_KEY_FILE_NAME, WIREGUARD_PUBLIC_KEY_FILE_NAME } =
|
||||
grabDirNames();
|
||||
@@ -20,12 +22,14 @@ type Params = {
|
||||
host?: BUN_SQLITE_WGUI_HOSTS;
|
||||
wg_subnet_ip?: string;
|
||||
user: User;
|
||||
is_update_after_client_setup?: boolean;
|
||||
};
|
||||
|
||||
export default async function setupWireguardHost({
|
||||
host,
|
||||
wg_subnet_ip,
|
||||
user,
|
||||
is_update_after_client_setup,
|
||||
}: Params): Promise<APIResponseObject> {
|
||||
const {
|
||||
HOST_CLIENTS_DIR,
|
||||
@@ -57,7 +61,7 @@ export default async function setupWireguardHost({
|
||||
query: {
|
||||
query: {
|
||||
host_id: {
|
||||
value: HOST_ID,
|
||||
value: String(HOST_ID),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -78,35 +82,39 @@ export default async function setupWireguardHost({
|
||||
};
|
||||
}
|
||||
|
||||
const is_ip_available = await checkPrivateIPAvailability({
|
||||
ip_address: HOST_WG_IP,
|
||||
});
|
||||
const is_ip_available = is_update_after_client_setup
|
||||
? { success: true }
|
||||
: await checkPrivateIPAvailability({
|
||||
ip_address: HOST_WG_IP,
|
||||
});
|
||||
|
||||
if (!is_ip_available.success) {
|
||||
if (!is_ip_available.success && !is_update_after_client_setup) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `IP not available`,
|
||||
};
|
||||
}
|
||||
|
||||
let pre_sh = ``;
|
||||
if (!is_update_after_client_setup) {
|
||||
let pre_sh = ``;
|
||||
|
||||
pre_sh += `set -e\n`;
|
||||
pre_sh += `mkdir -p ${HOST_CONFIG_DIR}\n`;
|
||||
pre_sh += `mkdir -p ${HOST_CLIENTS_DIR}\n`;
|
||||
pre_sh += `mkdir -p ${HOST_IPTABLES_DIR}\n`;
|
||||
pre_sh += `cd ${HOST_CONFIG_DIR}\n`;
|
||||
pre_sh += `if [ ! -f ${WIREGUARD_PRIVATE_KEY_FILE_NAME} ]; then\n`;
|
||||
pre_sh += ` wg genkey | tee ${WIREGUARD_PRIVATE_KEY_FILE_NAME} | wg pubkey > ${WIREGUARD_PUBLIC_KEY_FILE_NAME}\n`;
|
||||
pre_sh += `fi\n`;
|
||||
pre_sh += `set -e\n`;
|
||||
pre_sh += `mkdir -p ${HOST_CONFIG_DIR}\n`;
|
||||
pre_sh += `mkdir -p ${HOST_CLIENTS_DIR}\n`;
|
||||
pre_sh += `mkdir -p ${HOST_IPTABLES_DIR}\n`;
|
||||
pre_sh += `cd ${HOST_CONFIG_DIR}\n`;
|
||||
pre_sh += `if [ ! -f ${WIREGUARD_PRIVATE_KEY_FILE_NAME} ]; then\n`;
|
||||
pre_sh += ` wg genkey | tee ${WIREGUARD_PRIVATE_KEY_FILE_NAME} | wg pubkey > ${WIREGUARD_PUBLIC_KEY_FILE_NAME}\n`;
|
||||
pre_sh += `fi\n`;
|
||||
|
||||
try {
|
||||
const exec_pre_setup = execSync(pre_sh, { encoding: "utf-8" });
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
};
|
||||
try {
|
||||
const exec_pre_setup = execSync(pre_sh, { encoding: "utf-8" });
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -118,7 +126,7 @@ export default async function setupWireguardHost({
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
if (HOST_ID == 0) {
|
||||
if (HOST_ID == 0 && !is_update_after_client_setup) {
|
||||
const update_variables = await BunSQLite.insert<
|
||||
BUN_SQLITE_WGUI_VARIABLES,
|
||||
TableType
|
||||
@@ -140,7 +148,7 @@ export default async function setupWireguardHost({
|
||||
if (!update_variables.success) {
|
||||
throw new Error(`Couldn't update host variables`);
|
||||
}
|
||||
} else if (HOST_ID) {
|
||||
} else if (HOST_ID && !is_update_after_client_setup) {
|
||||
const update_host = await BunSQLite.insert<
|
||||
BUN_SQLITE_WGUI_HOSTS,
|
||||
TableType
|
||||
@@ -208,10 +216,15 @@ export default async function setupWireguardHost({
|
||||
if (clients[0]) {
|
||||
for (let i = 0; i < clients.length; i++) {
|
||||
const client = clients[i];
|
||||
if (!client?.id || !client.public_key) continue;
|
||||
if (!client?.id) continue;
|
||||
|
||||
const { CLIENT_PUBLIC_KEY } = grabClientDirnames({
|
||||
client,
|
||||
host_id: HOST_ID,
|
||||
});
|
||||
|
||||
sh += `[Peer]\n`;
|
||||
sh += `PublicKey = ${client.public_key}\n`;
|
||||
sh += `PublicKey = ${CLIENT_PUBLIC_KEY}\n`;
|
||||
sh += `AllowedIPs = ${client.allowed_ips}\n`;
|
||||
sh += `\n`;
|
||||
}
|
||||
@@ -219,6 +232,8 @@ export default async function setupWireguardHost({
|
||||
|
||||
sh += `EOF\n`;
|
||||
|
||||
sh += `chmod 600 ${INTERFACE_NAME}.conf\n`;
|
||||
|
||||
sh += `\n`;
|
||||
|
||||
const exec = execSync(sh, { encoding: "utf-8" });
|
||||
@@ -240,6 +255,11 @@ export default async function setupWireguardHost({
|
||||
msg: [exec.trim(), manage_res.msg].join("\n\n"),
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.log(
|
||||
`Error setting up wireguard host ${HOST_ID} =>`,
|
||||
error.message,
|
||||
);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
} from "@/db/types/db";
|
||||
import type { TableType } from "@/src/types";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import bunext from "@moduletrace/bunext";
|
||||
|
||||
const HOST_CONFIG_FILE_NAME_PATTERN = /^wgui(\d+)\.conf$/;
|
||||
|
||||
@@ -50,7 +51,7 @@ export default async function syncWireguardHosts() {
|
||||
|
||||
const res = manageWireguardHost({ action: "up", host_id });
|
||||
|
||||
console.log(
|
||||
bunext.bunextLog.info(
|
||||
`[wgui] wireguard host ${host_config_file_name}: ${
|
||||
res.success ? `up` : `failed — ${res.msg}`
|
||||
}`,
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import type {
|
||||
BUN_SQLITE_WGUI_CLIENTS,
|
||||
BUN_SQLITE_WGUI_HOSTS,
|
||||
BUN_SQLITE_WGUI_VARIABLES,
|
||||
} from "@/db/types/db";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import BunSQLite from "/home/archben/Projects/Git/moduletrace/bun-sqlite";
|
||||
// import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import type { TableType, User } from "@/src/types";
|
||||
import setupWireguardClient from "./setup-wireguard-client";
|
||||
import setupWireguardHost from "./setup-wireguard-host";
|
||||
|
||||
const IP_V4_REGEX = /^\d{1,3}(\.\d{1,3}){3}$/;
|
||||
|
||||
type Params = {
|
||||
host_id?: string | number | null;
|
||||
public_ip_address?: string | null;
|
||||
user: User;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the public IP address of a host and regenerate
|
||||
* every client config of that host with the new Endpoint.
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
export default async function updateWireguardHostPublicIP({
|
||||
host_id,
|
||||
public_ip_address,
|
||||
user,
|
||||
}: Params): Promise<APIResponseObject> {
|
||||
const final_host_id = Number(host_id || AppData["WireguardHostID"]);
|
||||
const ip = (public_ip_address || "").trim();
|
||||
|
||||
if (!ip || !IP_V4_REGEX.test(ip)) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Invalid public IP address provided`,
|
||||
};
|
||||
}
|
||||
|
||||
let host: BUN_SQLITE_WGUI_HOSTS | undefined;
|
||||
|
||||
try {
|
||||
if (final_host_id == AppData["WireguardHostID"]) {
|
||||
const update_variables = await BunSQLite.insert<
|
||||
BUN_SQLITE_WGUI_VARIABLES,
|
||||
TableType
|
||||
>({
|
||||
table: "variables",
|
||||
data: [
|
||||
{
|
||||
key: "main_host_public_ip_address",
|
||||
value: ip,
|
||||
},
|
||||
],
|
||||
update_on_duplicate: true,
|
||||
});
|
||||
|
||||
// console.log("update_variables", update_variables);
|
||||
|
||||
// if (!update_variables.success) {
|
||||
// throw new Error(`Couldn't update host public IP variable`);
|
||||
// }
|
||||
} 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`,
|
||||
};
|
||||
}
|
||||
|
||||
const update_host = await BunSQLite.insert<
|
||||
BUN_SQLITE_WGUI_HOSTS,
|
||||
TableType
|
||||
>({
|
||||
table: "hosts",
|
||||
data: [
|
||||
{
|
||||
id: host.id,
|
||||
user_id: user.id,
|
||||
public_ip_address: ip,
|
||||
},
|
||||
],
|
||||
update_on_duplicate: true,
|
||||
});
|
||||
|
||||
if (!update_host.success) {
|
||||
throw new Error(`Couldn't update host record`);
|
||||
}
|
||||
|
||||
host.public_ip_address = ip;
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
const host_clients_res = await BunSQLite.select<
|
||||
BUN_SQLITE_WGUI_CLIENTS,
|
||||
TableType
|
||||
>({
|
||||
table: "clients",
|
||||
query: {
|
||||
query: {
|
||||
host_id: {
|
||||
value: String(final_host_id),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const clients = host_clients_res.payload || [];
|
||||
|
||||
let updated_count = 0;
|
||||
const client_errors: string[] = [];
|
||||
|
||||
for (let i = 0; i < clients.length; i++) {
|
||||
const client = clients[i];
|
||||
|
||||
if (!client?.id) continue;
|
||||
|
||||
const client_setup_res = await setupWireguardClient({
|
||||
client,
|
||||
host,
|
||||
user,
|
||||
skip_host_setup: true,
|
||||
});
|
||||
|
||||
console.log("client_setup_res", client_setup_res);
|
||||
|
||||
if (client_setup_res.success) {
|
||||
updated_count++;
|
||||
} else {
|
||||
client_errors.push(
|
||||
`Client ${client.name || `#${client.id}`}: ${client_setup_res.msg}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const host_setup_res = await setupWireguardHost({
|
||||
host,
|
||||
user,
|
||||
is_update_after_client_setup: true,
|
||||
});
|
||||
|
||||
if (!host_setup_res.success) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `${updated_count} client config(s) updated, but the host could not be restarted: ${host_setup_res.msg}`,
|
||||
numberRes: updated_count,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
msg: `Host IP updated to ${ip}. ${updated_count} client config(s) regenerated.`,
|
||||
numberRes: updated_count,
|
||||
stringRes: client_errors[0] ? client_errors.join("; ") : null,
|
||||
};
|
||||
}
|
||||
@@ -84,6 +84,8 @@ export function Head({ serverRes, ctx }: BunextPageHeadFCProps) {
|
||||
href="https://fonts.gstatic.com"
|
||||
crossOrigin=""
|
||||
/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.22.0/ace.min.js" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.22.0/ext-language_tools.min.js" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { useState, type Dispatch, type ReactNode, type SetStateAction } from "react";
|
||||
import {
|
||||
useState,
|
||||
type Dispatch,
|
||||
type ReactNode,
|
||||
type SetStateAction,
|
||||
} from "react";
|
||||
import { X } from "lucide-react";
|
||||
import Modal from "@/src/components/twui/elements/Modal";
|
||||
import Input from "@/src/components/twui/form/Input";
|
||||
@@ -97,7 +102,7 @@ export default function ClientFormModal({ open, setOpen, onCreated }: Props) {
|
||||
<Input
|
||||
name="allowed_ips"
|
||||
id="allowed_ips"
|
||||
placeholder="10.0.0.10/32"
|
||||
placeholder="10.0.0.10/24"
|
||||
label="Allowed IPs"
|
||||
showLabel
|
||||
/>
|
||||
@@ -108,7 +113,10 @@ export default function ClientFormModal({ open, setOpen, onCreated }: Props) {
|
||||
</Span>
|
||||
) : null}
|
||||
<Row className="justify-end gap-2 mt-2">
|
||||
<AdminButton onClick={() => setOpen(false)} disabled={busy}>
|
||||
<AdminButton
|
||||
onClick={() => setOpen(false)}
|
||||
disabled={busy}
|
||||
>
|
||||
Cancel
|
||||
</AdminButton>
|
||||
<AdminButton
|
||||
@@ -123,4 +131,4 @@ export default function ClientFormModal({ open, setOpen, onCreated }: Props) {
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ export default function ClientsTableSection({ addOpen, setAddOpen }: Props) {
|
||||
<th className={thClass}>Allowed IPs</th>
|
||||
<th className={thClass}>Public key</th>
|
||||
<th className={thRightClass}>Created</th>
|
||||
<th className={thRightClass}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import AdminCard from "@/src/components/general/admin-card";
|
||||
import AdminButton from "@/src/components/general/admin-button";
|
||||
import Input from "@/src/components/twui/form/Input";
|
||||
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 fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import Button from "@/src/components/twui/layout/Button";
|
||||
|
||||
type Props = {
|
||||
host_id: number | string;
|
||||
current_ip?: string;
|
||||
};
|
||||
|
||||
export default function HostPublicIpSection({ host_id, current_ip }: Props) {
|
||||
const [ip, setIp] = useState(current_ip || "");
|
||||
const [displayedIp, setDisplayedIp] = useState(current_ip || "");
|
||||
const [typed, setTyped] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
const [success, setSuccess] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!current_ip || typed) return;
|
||||
setIp(current_ip);
|
||||
setDisplayedIp(current_ip);
|
||||
}, [current_ip, typed]);
|
||||
|
||||
async function handleUpdate() {
|
||||
setError(undefined);
|
||||
setSuccess(undefined);
|
||||
|
||||
const value = ip.trim();
|
||||
|
||||
if (!value) {
|
||||
setError("Please enter the new public IP address");
|
||||
return;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
|
||||
try {
|
||||
const res = await fetchApi<{ [k: string]: any }, APIResponseObject>(
|
||||
"/api/admin/update-host-public-ip",
|
||||
{
|
||||
method: "POST",
|
||||
body: {
|
||||
host_id,
|
||||
public_ip_address: value,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.success) {
|
||||
setError(res.msg || "Could not update the host IP");
|
||||
return;
|
||||
}
|
||||
|
||||
setDisplayedIp(value);
|
||||
setSuccess(res.msg || "Host IP updated");
|
||||
} catch (error: any) {
|
||||
setError(error.message || "Could not update the host IP");
|
||||
} finally {
|
||||
setBusy(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!">
|
||||
Host public IP
|
||||
</H3>
|
||||
<P
|
||||
noMargin
|
||||
className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45"
|
||||
>
|
||||
The public IP address clients use to reach this host.
|
||||
Updating it regenerates every client config with the new
|
||||
Endpoint.
|
||||
</P>
|
||||
</Stack>
|
||||
<Row className="gap-3 items-center flex-wrap">
|
||||
<Span className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45 whitespace-nowrap">
|
||||
Current:
|
||||
</Span>
|
||||
<Span
|
||||
className={
|
||||
displayedIp
|
||||
? "font-mono text-[12.5px] text-foreground-light/70 dark:text-foreground-dark/70"
|
||||
: "text-[12.5px] text-foreground-light/40 dark:text-foreground-dark/40"
|
||||
}
|
||||
>
|
||||
{displayedIp || "Auto-detected"}
|
||||
</Span>
|
||||
<Input
|
||||
name="host_public_ip"
|
||||
id="host_public_ip"
|
||||
placeholder="e.g. 203.0.113.10"
|
||||
label="New public IP"
|
||||
showLabel
|
||||
value={ip}
|
||||
onChange={(e) => {
|
||||
setTyped(true);
|
||||
setIp(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title="Update IP Button"
|
||||
variant="outlined"
|
||||
disabled={busy}
|
||||
onClick={handleUpdate}
|
||||
size="small"
|
||||
>
|
||||
{busy ? "Updating…" : "Update IP & refresh configs"}
|
||||
</Button>
|
||||
</Row>
|
||||
{error ? <Tag color="error">{error}</Tag> : null}
|
||||
{success ? <Tag color="success">{success}</Tag> : null}
|
||||
</AdminCard>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import Paper from "@/src/components/twui/elements/Paper";
|
||||
import Center from "@/src/components/twui/layout/Center";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import ClientRow from "@/src/components/general/client-row";
|
||||
import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
import { useContext } from "react";
|
||||
@@ -11,6 +12,12 @@ type Props = {
|
||||
host_id: string | number;
|
||||
};
|
||||
|
||||
const thClass =
|
||||
"px-4 py-2 text-left text-[11px] font-semibold uppercase tracking-[0.08em] " +
|
||||
"text-foreground-light/40 dark:text-foreground-dark/40 whitespace-nowrap";
|
||||
|
||||
const thRightClass = `${thClass} text-right`;
|
||||
|
||||
export default function ClientsList({ host_id }: Props) {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
|
||||
@@ -20,6 +27,7 @@ export default function ClientsList({ host_id }: Props) {
|
||||
query: {
|
||||
host_id: { value: host_id },
|
||||
},
|
||||
order: { field: "created_at", strategy: "DESC" },
|
||||
},
|
||||
initial_res: pageProps?.clients || undefined,
|
||||
});
|
||||
@@ -44,5 +52,25 @@ export default function ClientsList({ host_id }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
return <Stack>{}</Stack>;
|
||||
}
|
||||
return (
|
||||
<Paper className="overflow-x-auto">
|
||||
<table className="w-full min-w-[860px]">
|
||||
<thead>
|
||||
<tr className="border-y border-slate-200 dark:border-white/10 bg-foreground-light/[0.02] dark:bg-foreground-dark/[0.03]">
|
||||
<th className={thClass}>Client</th>
|
||||
<th className={thClass}>Tunnel IP</th>
|
||||
<th className={thClass}>Allowed IPs</th>
|
||||
<th className={thClass}>Public key</th>
|
||||
<th className={thRightClass}>Created</th>
|
||||
<th className={thRightClass}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{clients.map((client) => (
|
||||
<ClientRow key={client.id} client={client} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { useState } from "react";
|
||||
import { Pencil, Trash2 } from "lucide-react";
|
||||
import Button from "@/src/components/twui/layout/Button";
|
||||
import adminCrudHandler from "@/src/functions/frontend/admin-crud-handler";
|
||||
import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
|
||||
|
||||
type Props = {
|
||||
client: BUN_SQLITE_WGUI_CLIENTS;
|
||||
};
|
||||
|
||||
export default function ClientDetailActions({ client }: Props) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const host_id = Number(client?.host_id || 0);
|
||||
|
||||
async function handleDelete() {
|
||||
if (
|
||||
!window.confirm(
|
||||
`Delete client "${client?.name || `#${client?.id}`}"? The client will be removed from the host and its config file deleted.`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
|
||||
const res = await adminCrudHandler({
|
||||
action: "delete",
|
||||
table: "clients",
|
||||
id: client?.id,
|
||||
});
|
||||
|
||||
if (!res.success) {
|
||||
setBusy(false);
|
||||
window.alert(res.msg || "Could not delete client");
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.pathname = `/admin/hosts/${host_id}/clients`;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
title={`Edit client ${client?.name}`}
|
||||
variant="outlined"
|
||||
beforeIcon={<Pencil />}
|
||||
href={`/admin/hosts/${host_id}/clients/${client?.id}/edit`}
|
||||
>
|
||||
Edit client
|
||||
</Button>
|
||||
<Button
|
||||
title="Delete client"
|
||||
variant="outlined"
|
||||
color="error"
|
||||
beforeIcon={<Trash2 />}
|
||||
disabled={busy}
|
||||
onClick={handleDelete}
|
||||
className="hover:bg-error/10"
|
||||
>
|
||||
{busy ? "Deleting…" : "Delete client"}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useState } from "react";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import Row from "@/src/components/twui/layout/Row";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
value: string;
|
||||
mono?: boolean;
|
||||
copyable?: boolean;
|
||||
};
|
||||
|
||||
export default function ClientInfoRow({ label, value, mono, copyable }: Props) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
return (
|
||||
<Row className="justify-between gap-4 px-5 py-2.5 border-t border-slate-200/60 dark:border-white/5">
|
||||
<Span className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45 whitespace-nowrap">
|
||||
{label}
|
||||
</Span>
|
||||
<Row className="items-center gap-2 min-w-0">
|
||||
<Span
|
||||
className={
|
||||
mono
|
||||
? "font-mono text-[12.5px] text-foreground-light/70 dark:text-foreground-dark/70 break-all"
|
||||
: "text-[12.5px] text-foreground-light/70 dark:text-foreground-dark/70 text-right break-all"
|
||||
}
|
||||
>
|
||||
{value}
|
||||
</Span>
|
||||
{copyable && value && value != "—" ? (
|
||||
<button
|
||||
type="button"
|
||||
title="Copy value"
|
||||
aria-label={`Copy ${label}`}
|
||||
onClick={() => {
|
||||
navigator.clipboard?.writeText(value).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
});
|
||||
}}
|
||||
className="text-foreground-light/40 dark:text-foreground-dark/40 hover:text-foreground-light dark:hover:text-foreground-dark transition-colors duration-150 p-1 focus:outline-2"
|
||||
>
|
||||
{copied ? (
|
||||
<Check size={14} />
|
||||
) : (
|
||||
<Copy size={14} />
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</Row>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { useContext } from "react";
|
||||
import AdminCard from "@/src/components/general/admin-card";
|
||||
import CodeBlock from "@/src/components/twui/elements/CodeBlock";
|
||||
import EmptyContent from "@/src/components/twui/elements/EmptyContent";
|
||||
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 { AppContext } from "@/src/pages/__root";
|
||||
import AceEditor from "@/src/components/twui/editors/AceEditor";
|
||||
|
||||
export default function ClientConfigSection() {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
|
||||
const config = pageProps?.client_config;
|
||||
|
||||
if (!config) {
|
||||
return (
|
||||
<AdminCard className="w-full overflow-hidden">
|
||||
<Stack className="w-full items-stretch py-4">
|
||||
<Row className="justify-between gap-3 px-5 flex-wrap">
|
||||
<H3 className="mb-0!">Client configuration</H3>
|
||||
</Row>
|
||||
<EmptyContent title="No configuration file found" />
|
||||
<Stack className="w-full justify-center py-1 px-6 text-center gap-1">
|
||||
<Span className="text-[12.5px] text-foreground-light/35 dark:text-foreground-dark/35">
|
||||
Run the client setup for this client to generate its
|
||||
wg.conf on the server.
|
||||
{pageProps?.client_config_path
|
||||
? ` Expected at ${pageProps.client_config_path}.`
|
||||
: ""}
|
||||
</Span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</AdminCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminCard className="w-full overflow-hidden">
|
||||
<Stack className="w-full items-stretch py-4">
|
||||
<Row className="justify-between gap-3 px-5 flex-wrap">
|
||||
<Stack className="gap-0.5">
|
||||
<H3 className="mb-0!">Client configuration</H3>
|
||||
<P
|
||||
noMargin
|
||||
className="font-mono text-[11.5px] text-foreground-light/40 dark:text-foreground-dark/40"
|
||||
>
|
||||
{pageProps?.client_config_path}
|
||||
</P>
|
||||
</Stack>
|
||||
<Span variant="faded">
|
||||
Use this file with the WireGuard app
|
||||
</Span>
|
||||
</Row>
|
||||
<AceEditor content={config} mode="ini" />
|
||||
</Stack>
|
||||
</AdminCard>
|
||||
);
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { useContext } from "react";
|
||||
import AdminCard from "@/src/components/general/admin-card";
|
||||
import H3 from "@/src/components/twui/layout/H3";
|
||||
import Row from "@/src/components/twui/layout/Row";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
import ClientInfoRow from "../(partials)/client-info-row";
|
||||
|
||||
export default function ClientInfoSection() {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
|
||||
const client = pageProps?.client;
|
||||
const host_id = Number(client?.host_id || 0);
|
||||
|
||||
if (!client) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rows = [
|
||||
{
|
||||
label: "Host",
|
||||
value: host_id ? `Host #${host_id}` : "Main host",
|
||||
},
|
||||
{
|
||||
label: "Tunnel IP",
|
||||
value: client.wg_ip_address || "—",
|
||||
mono: true,
|
||||
},
|
||||
{
|
||||
label: "Allowed IPs",
|
||||
value: client.allowed_ips || "—",
|
||||
mono: true,
|
||||
},
|
||||
{
|
||||
label: "Public key",
|
||||
value: client.public_key || "—",
|
||||
mono: true,
|
||||
copyable: true,
|
||||
},
|
||||
{
|
||||
label: "Notes",
|
||||
value: client.notes || "—",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<AdminCard className="w-full overflow-hidden">
|
||||
<Stack className="w-full items-stretch py-4">
|
||||
<Row className="justify-between gap-3 px-5 flex-wrap">
|
||||
<H3 className="mb-0!">Client information</H3>
|
||||
</Row>
|
||||
{rows.map((row) => (
|
||||
<ClientInfoRow key={row.label} {...row} />
|
||||
))}
|
||||
</Stack>
|
||||
</AdminCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useContext } from "react";
|
||||
import AdminCard from "@/src/components/general/admin-card";
|
||||
import H3 from "@/src/components/twui/layout/H3";
|
||||
import Img from "@/src/components/twui/layout/Img";
|
||||
import P from "@/src/components/twui/layout/P";
|
||||
import Row from "@/src/components/twui/layout/Row";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
|
||||
export default function ClientQRSection() {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
|
||||
const qr_data_uri = pageProps?.client_qr_data_uri;
|
||||
|
||||
return (
|
||||
<AdminCard className="w-full overflow-hidden">
|
||||
<Stack className="w-full items-stretch py-4">
|
||||
<Row className="justify-between gap-3 px-5 flex-wrap">
|
||||
<H3 className="mb-0!">Add via QR code</H3>
|
||||
</Row>
|
||||
{qr_data_uri ? (
|
||||
<Row className="justify-center gap-4 px-5 py-5">
|
||||
<Stack className="gap-2 items-center">
|
||||
<Img
|
||||
src={qr_data_uri}
|
||||
alt="WireGuard configuration QR code for importing this client into the phone app"
|
||||
width={260}
|
||||
height={260}
|
||||
circle={false}
|
||||
className="rounded-default"
|
||||
/>
|
||||
<P
|
||||
noMargin
|
||||
className="text-[12px] text-foreground-light/50 dark:text-foreground-dark/50 max-w-[260px] text-center"
|
||||
>
|
||||
Scan with the official WireGuard app, or import the
|
||||
config file manually via the config tab
|
||||
</P>
|
||||
</Stack>
|
||||
</Row>
|
||||
) : (
|
||||
<Stack className="w-full justify-center py-10 px-6 text-center gap-1">
|
||||
<P
|
||||
noMargin
|
||||
className="text-[13px] font-medium text-foreground-light/50 dark:text-foreground-dark/50"
|
||||
>
|
||||
No QR code available
|
||||
</P>
|
||||
<P
|
||||
noMargin
|
||||
className="text-[12.5px] text-foreground-light/35 dark:text-foreground-dark/35"
|
||||
>
|
||||
Generate the client config first — the QR code is
|
||||
created from the stored wg.conf
|
||||
</P>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</AdminCard>
|
||||
);
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import Section from "@/src/components/twui/layout/Section";
|
||||
import AddClientForm from "../../../add-client/(partials)/client-form/add-client-form";
|
||||
import { useContext } from "react";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
|
||||
export default function EditClientFormSection() {
|
||||
const { pageProps, query } = useContext(AppContext);
|
||||
|
||||
const host_id = Number(query?.host_id || 0);
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<AddClientForm
|
||||
existing_client={pageProps?.client || undefined}
|
||||
existing_client_full={pageProps?.client || undefined}
|
||||
host_id={host_id}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
|
||||
import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
type ClientEditPageQuery = PageQueryObject & {
|
||||
client_id?: string;
|
||||
};
|
||||
|
||||
const server: BunextPageServerFn<PagePropsType> = async ({ query }) => {
|
||||
const page_query = query as ClientEditPageQuery;
|
||||
|
||||
const client_res = await BunSQLite.select<
|
||||
BUN_SQLITE_WGUI_CLIENTS,
|
||||
TableType
|
||||
>({
|
||||
table: "clients",
|
||||
targetId: page_query.client_id,
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
client: client_res.singleRes || null,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default server;
|
||||
@@ -0,0 +1,55 @@
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { useContext } from "react";
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
import AdminHero from "@/src/components/general/admin-hero";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
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 EditClientFormSection from "./(sections)/edit-client-form-section";
|
||||
|
||||
export default function AdminEditClientPage() {
|
||||
const { pageProps, query } = useContext(AppContext);
|
||||
|
||||
const host_id = Number(query?.host_id || pageProps?.client?.host_id || 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AdminHero
|
||||
title="Edit Client"
|
||||
description={
|
||||
pageProps?.client?.name
|
||||
? `Edit client "${pageProps.client.name}"`
|
||||
: "Edit client"
|
||||
}
|
||||
buttons={
|
||||
<Button
|
||||
title="View client"
|
||||
variant="outlined"
|
||||
beforeIcon={<ArrowLeft />}
|
||||
href={`/admin/hosts/${host_id}/clients/${pageProps?.client?.id}`}
|
||||
>
|
||||
View client
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Divider className="mb-6" />
|
||||
|
||||
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
|
||||
{pageProps?.client ? (
|
||||
<EditClientFormSection />
|
||||
) : (
|
||||
<EmptyContent title="Client not found" />
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `Edit Client | ${SiteData["SiteName"]}`,
|
||||
description: `WireGuard edit client page`,
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import type {
|
||||
BUN_SQLITE_WGUI_CLIENTS,
|
||||
BUN_SQLITE_WGUI_HOSTS,
|
||||
} from "@/db/types/db";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import generateWireguardClientQR from "@/src/functions/backend/setup/generate-wireguard-client-qr";
|
||||
import readWireguardClientConfig from "@/src/functions/backend/setup/read-wireguard-client-config";
|
||||
import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
type ClientPageQuery = PageQueryObject & {
|
||||
client_id?: string;
|
||||
};
|
||||
|
||||
const server: BunextPageServerFn<PagePropsType> = async ({ query }) => {
|
||||
const page_query = query as ClientPageQuery;
|
||||
|
||||
const client_res = await BunSQLite.select<
|
||||
BUN_SQLITE_WGUI_CLIENTS,
|
||||
TableType
|
||||
>({
|
||||
table: "clients",
|
||||
targetId: page_query.client_id,
|
||||
});
|
||||
|
||||
const client = client_res.singleRes || null;
|
||||
|
||||
const host_id = Number(
|
||||
client?.host_id ||
|
||||
page_query.host_id ||
|
||||
AppData["WireguardHostID"],
|
||||
);
|
||||
|
||||
let host: BUN_SQLITE_WGUI_HOSTS | null = null;
|
||||
|
||||
if (host_id && host_id != AppData["WireguardHostID"]) {
|
||||
const host_res = await BunSQLite.select<
|
||||
BUN_SQLITE_WGUI_HOSTS,
|
||||
TableType
|
||||
>({
|
||||
table: "hosts",
|
||||
targetId: host_id,
|
||||
});
|
||||
|
||||
host = host_res.singleRes || null;
|
||||
}
|
||||
|
||||
const config_res = await readWireguardClientConfig({
|
||||
client_id: client?.id,
|
||||
host_id,
|
||||
});
|
||||
|
||||
const qr_res = await generateWireguardClientQR({
|
||||
config: config_res.config,
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
client,
|
||||
host,
|
||||
client_config: config_res.config,
|
||||
client_config_path: config_res.config_path,
|
||||
client_qr_data_uri: qr_res.qr_data_uri,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default server;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { useContext } from "react";
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
import AdminHero from "@/src/components/general/admin-hero";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
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 ClientInfoSection from "./(sections)/client-info-section";
|
||||
import ClientConfigSection from "./(sections)/client-config-section";
|
||||
import ClientQRSection from "./(sections)/client-qr-section";
|
||||
import ClientDetailActions from "./(partials)/client-detail-actions";
|
||||
|
||||
export default function AdminClientDetailPage() {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
|
||||
const host_id = Number(
|
||||
pageProps?.host?.id || pageProps?.client?.host_id || 0,
|
||||
);
|
||||
|
||||
const desc =
|
||||
pageProps?.client?.notes ||
|
||||
"Client details and WireGuard configuration";
|
||||
|
||||
return (
|
||||
<>
|
||||
<AdminHero
|
||||
title={pageProps?.client?.name || "Client"}
|
||||
description={desc}
|
||||
buttons={
|
||||
<>
|
||||
<Button
|
||||
title="Back to clients"
|
||||
variant="outlined"
|
||||
beforeIcon={<ArrowLeft />}
|
||||
href={`/admin/hosts/${host_id}/clients`}
|
||||
>
|
||||
Back to clients
|
||||
</Button>
|
||||
{pageProps?.client ? (
|
||||
<ClientDetailActions client={pageProps.client} />
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Divider className="mb-6" />
|
||||
|
||||
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
|
||||
{pageProps?.client ? (
|
||||
<>
|
||||
<ClientInfoSection />
|
||||
<ClientQRSection />
|
||||
<ClientConfigSection />
|
||||
</>
|
||||
) : (
|
||||
<EmptyContent title="Client not found" />
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `Client | ${SiteData["SiteName"]}`,
|
||||
description: `WireGuard client details page`,
|
||||
};
|
||||
+1
-1
@@ -31,7 +31,7 @@ export default async function grabNextAvailableClientIp({
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
wg_ip_address: res.msg,
|
||||
allowed_ips: `${res.msg}/32`,
|
||||
allowed_ips: `${res.msg}/24`,
|
||||
}));
|
||||
|
||||
return {
|
||||
|
||||
+19
-13
@@ -18,23 +18,29 @@ export default async function submitAddClientForm(
|
||||
{ host_id }: Params,
|
||||
) {
|
||||
try {
|
||||
if (!window.confirm(`Create new client?`)) {
|
||||
const confirm_msg = existing_full?.id
|
||||
? `Update this Client?`
|
||||
: `Create new client?`;
|
||||
|
||||
if (!window.confirm(confirm_msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const final_host_id = Number(app_context.query?.host_id || 0);
|
||||
|
||||
const new_client_data: BUN_SQLITE_WGUI_CLIENTS = _.omitBy(
|
||||
{
|
||||
name: form.name || "",
|
||||
wg_ip_address: form.wg_ip_address || "",
|
||||
allowed_ips: form.allowed_ips || "",
|
||||
notes: form.notes || "",
|
||||
host_id: final_host_id,
|
||||
user_id: app_context?.user?.id,
|
||||
},
|
||||
(value) => value === undefined,
|
||||
);
|
||||
const new_client_data: BUN_SQLITE_WGUI_CLIENTS =
|
||||
_.omitBy<BUN_SQLITE_WGUI_CLIENTS>(
|
||||
{
|
||||
name: form.name || "",
|
||||
wg_ip_address: form.wg_ip_address || "",
|
||||
allowed_ips: form.allowed_ips || "",
|
||||
notes: form.notes || "",
|
||||
host_id: final_host_id,
|
||||
user_id: app_context?.user?.id,
|
||||
allow_all_ips: form.allow_all_ips,
|
||||
},
|
||||
(value) => value === undefined,
|
||||
);
|
||||
|
||||
setLoading(true);
|
||||
|
||||
@@ -53,7 +59,7 @@ export default async function submitAddClientForm(
|
||||
|
||||
if (res.success) {
|
||||
if (existing_full?.id) {
|
||||
window.location.reload();
|
||||
window.location.pathname = `/admin/hosts/${final_host_id}/clients/${existing_full.id}`;
|
||||
} else {
|
||||
window.location.pathname = `/admin/hosts/${final_host_id}/clients`;
|
||||
}
|
||||
|
||||
+28
-12
@@ -1,5 +1,7 @@
|
||||
import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
|
||||
import Checkbox from "@/src/components/twui/form/Checkbox";
|
||||
import Input from "@/src/components/twui/form/Input";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
|
||||
export default function AddClientFormAllowedIps({
|
||||
@@ -7,17 +9,31 @@ export default function AddClientFormAllowedIps({
|
||||
setForm,
|
||||
}: ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_CLIENTS>>) {
|
||||
return (
|
||||
<Input
|
||||
title="Allowed IPs"
|
||||
placeholder="Eg. 10.0.0.2/32"
|
||||
onChange={(e) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
allowed_ips: e.target.value,
|
||||
}));
|
||||
}}
|
||||
value={form.allowed_ips}
|
||||
showLabel
|
||||
/>
|
||||
<Stack className="gap-2 my-4 w-full">
|
||||
{form.allow_all_ips ? null : (
|
||||
<Input
|
||||
title="Allowed IPs"
|
||||
placeholder="Eg. 10.0.0.2/24"
|
||||
onChange={(e) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
allowed_ips: e.target.value,
|
||||
}));
|
||||
}}
|
||||
value={form.allowed_ips}
|
||||
showLabel
|
||||
/>
|
||||
)}
|
||||
<Checkbox
|
||||
label={`Allow all IPs? That is 0.0.0.0/0, ::/0`}
|
||||
defaultChecked={form.allow_all_ips == 1}
|
||||
changeHandler={(value) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
allow_all_ips: value ? 1 : 0,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ export default function AddClientFormWgIpAddress({
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
wg_ip_address: e.target.value,
|
||||
allowed_ips: `${e.target.value}/32`,
|
||||
allowed_ips: `${e.target.value.replace(/\.\d+$/, "")}.0/24`,
|
||||
}));
|
||||
}}
|
||||
value={form.wg_ip_address}
|
||||
|
||||
@@ -3,8 +3,16 @@ 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 HostPublicIpSection from "../(sections)/host-public-ip-section";
|
||||
import { useContext } from "react";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
|
||||
export default function AdminSingleHostPage() {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
|
||||
const host_id = Number(pageProps?.host?.id || 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AdminHero
|
||||
@@ -20,6 +28,15 @@ export default function AdminSingleHostPage() {
|
||||
/>
|
||||
|
||||
<Divider className="mb-6" />
|
||||
|
||||
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
|
||||
{pageProps?.host ? (
|
||||
<HostPublicIpSection
|
||||
host_id={host_id}
|
||||
current_ip={pageProps.host.public_ip_address || ""}
|
||||
/>
|
||||
) : null}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -27,4 +44,4 @@ export default function AdminSingleHostPage() {
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `Host | ${SiteData["SiteName"]}`,
|
||||
description: `WireGuard host page`,
|
||||
};
|
||||
};
|
||||
@@ -14,10 +14,15 @@ import checkIfMainHostIsSet from "@/src/functions/check-if-main-host-is-set";
|
||||
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";
|
||||
|
||||
export default function AdminHostPage() {
|
||||
const { hosts, variables, clients } = useHostData();
|
||||
|
||||
const main_host_public_ip = variables?.find(
|
||||
(v) => v.key == "main_host_public_ip_address",
|
||||
)?.value;
|
||||
|
||||
const is_main_host_set = checkIfMainHostIsSet({ variables });
|
||||
|
||||
if (!is_main_host_set) {
|
||||
@@ -57,11 +62,20 @@ export default function AdminHostPage() {
|
||||
<H2>Main Host</H2>
|
||||
|
||||
<Row>
|
||||
<Button
|
||||
title="View Clients"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
href={`/admin/hosts/0/clients`}
|
||||
>
|
||||
View Clients
|
||||
</Button>
|
||||
<Button
|
||||
title="Add Client"
|
||||
beforeIcon={<Plus />}
|
||||
size="small"
|
||||
href={`/admin/hosts/0/clients/add`}
|
||||
href={`/admin/hosts/0/clients/add-client`}
|
||||
>
|
||||
Add Client
|
||||
</Button>
|
||||
@@ -71,6 +85,10 @@ export default function AdminHostPage() {
|
||||
variables={variables}
|
||||
clients={clients}
|
||||
/>
|
||||
<HostPublicIpSection
|
||||
host_id={0}
|
||||
current_ip={main_host_public_ip || ""}
|
||||
/>
|
||||
<InterfaceConfigSection
|
||||
variables={variables}
|
||||
clients={clients}
|
||||
|
||||
@@ -43,15 +43,16 @@ export default async function runClientSetup({
|
||||
let host: BUN_SQLITE_WGUI_HOSTS | undefined;
|
||||
|
||||
if (client?.host_id) {
|
||||
const host_res = await BunSQLite.select<BUN_SQLITE_WGUI_HOSTS, TableType>(
|
||||
{
|
||||
table: "hosts",
|
||||
targetId: client.host_id,
|
||||
},
|
||||
);
|
||||
const host_res = await BunSQLite.select<
|
||||
BUN_SQLITE_WGUI_HOSTS,
|
||||
TableType
|
||||
>({
|
||||
table: "hosts",
|
||||
targetId: client.host_id,
|
||||
});
|
||||
|
||||
host = host_res.singleRes || undefined;
|
||||
}
|
||||
|
||||
return await setupWireguardClient({ client, host, user });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import _ from "lodash";
|
||||
import { execSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import type { AdminCrudAPIParams, TableType } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import grabDirNames from "@/src/utils/grab-dir-names";
|
||||
import grabHostDirnames from "@/src/functions/backend/setup/grab-host-dir-names";
|
||||
import setupWireguardHost from "@/src/functions/backend/setup/setup-wireguard-host";
|
||||
import type {
|
||||
BUN_SQLITE_WGUI_ALL_TYPEDEFS,
|
||||
BUN_SQLITE_WGUI_CLIENTS,
|
||||
BUN_SQLITE_WGUI_HOSTS,
|
||||
} from "@/db/types/db";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { ServerQueryParam } from "@moduletrace/bun-sqlite/dist/types";
|
||||
import grabClientDirnames from "@/src/functions/backend/setup/grab-client-dir-names";
|
||||
|
||||
const { WGUI_LIB_HOSTS_CONFIGS_DIR, WGUI_LIB_HOST_CLIENTS_DIR_NAME } =
|
||||
grabDirNames();
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
@@ -43,21 +53,78 @@ export default async function (
|
||||
if (!clients_to_delete.payload) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `No Media to Delete.`,
|
||||
msg: `No Client to Delete.`,
|
||||
};
|
||||
}
|
||||
|
||||
const affected_host_ids = new Set<number>();
|
||||
|
||||
for (let i = 0; i < clients_to_delete.payload.length; i++) {
|
||||
const client = clients_to_delete.payload[i];
|
||||
if (!client?.id) continue;
|
||||
|
||||
const host_id = Number(client.host_id || 0);
|
||||
|
||||
const { CLIENT_DIR } = grabClientDirnames({
|
||||
client,
|
||||
host_id,
|
||||
});
|
||||
|
||||
try {
|
||||
execSync(`rm -rf ${CLIENT_DIR}`, { encoding: "utf-8" });
|
||||
} catch (error) {}
|
||||
|
||||
affected_host_ids.add(host_id);
|
||||
|
||||
await BunSQLite.delete<BUN_SQLITE_WGUI_CLIENTS, TableType>({
|
||||
table: "clients",
|
||||
targetId: client.id,
|
||||
});
|
||||
}
|
||||
|
||||
const host_setup_errors: string[] = [];
|
||||
|
||||
for (const host_id of affected_host_ids) {
|
||||
try {
|
||||
let host: BUN_SQLITE_WGUI_HOSTS | undefined;
|
||||
|
||||
if (host_id) {
|
||||
const host_res = await BunSQLite.select<
|
||||
BUN_SQLITE_WGUI_HOSTS,
|
||||
TableType
|
||||
>({
|
||||
table: "hosts",
|
||||
targetId: host_id,
|
||||
});
|
||||
|
||||
host = host_res.singleRes || undefined;
|
||||
}
|
||||
|
||||
const host_setup_res = await setupWireguardHost({
|
||||
host,
|
||||
user,
|
||||
is_update_after_client_setup: true,
|
||||
});
|
||||
|
||||
if (!host_setup_res.success) {
|
||||
host_setup_errors.push(
|
||||
`Host ${host_id}: ${host_setup_res.msg}`,
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
host_setup_errors.push(`Host ${host_id}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (host_setup_errors[0]) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Client deleted but host config could not be regenerated: ${host_setup_errors.join("; ")}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
msg: `${clients_to_delete.payload.length} client(s) deleted`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export default async function (
|
||||
|
||||
const PUT = await BunSQLite.update({
|
||||
table,
|
||||
data: body?.update_data,
|
||||
data: body.update_data,
|
||||
targetId,
|
||||
query: final_sql_query,
|
||||
});
|
||||
@@ -44,13 +44,9 @@ export default async function (
|
||||
});
|
||||
|
||||
if (!client_setup_res.success) {
|
||||
PUT.debug = {
|
||||
...(PUT.debug || {}),
|
||||
client_setup: client_setup_res,
|
||||
};
|
||||
PUT.error = client_setup_res.msg || client_setup_res.error;
|
||||
return client_setup_res;
|
||||
}
|
||||
}
|
||||
|
||||
return PUT;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import updateWireguardHostPublicIP from "@/src/functions/backend/setup/update-wireguard-host-public-ip";
|
||||
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 updateWireguardHostPublicIP({
|
||||
host_id: body?.host_id,
|
||||
public_ip_address: body?.public_ip_address,
|
||||
user,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -41,11 +41,11 @@ case "$ACTION" in
|
||||
;;
|
||||
esac
|
||||
|
||||
EXPECTED_CONFIG_PATH="/var/lib/wgui/hosts/${INTERFACE}.conf"
|
||||
if [ "$CONFIG_PATH" != "$EXPECTED_CONFIG_PATH" ]; then
|
||||
echo "error: config path must be $EXPECTED_CONFIG_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
# EXPECTED_CONFIG_PATH="/var/lib/wgui/hosts/${INTERFACE}.conf"
|
||||
# if [ "$CONFIG_PATH" != "$EXPECTED_CONFIG_PATH" ]; then
|
||||
# echo "error: config path must be $EXPECTED_CONFIG_PATH" >&2
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
WG_QUICK_BIN="$(command -v wg-quick)"
|
||||
if [ -z "$WG_QUICK_BIN" ]; then
|
||||
|
||||
+13
-2
@@ -2,6 +2,8 @@ import bunext from "@moduletrace/bunext";
|
||||
import syncWireguardHosts from "./functions/backend/setup/sync-wireguard-hosts";
|
||||
import handleMediaServer from "./functions/server/media/handle-media-server";
|
||||
import { SiteData } from "./data/site-data";
|
||||
import { execFile, execSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
|
||||
declare global {}
|
||||
|
||||
@@ -28,6 +30,15 @@ const server = Bun.serve({
|
||||
port,
|
||||
});
|
||||
|
||||
bunext.bunextLog.info(`Server running on http://localhost:${server.port} ...`);
|
||||
bunext.bunextLog.build(`Setting up wireguard ...`);
|
||||
execSync(path.join(__dirname, "scripts", "install-wg-ui.sh"), {
|
||||
stdio: "ignore",
|
||||
env: {
|
||||
...process.env,
|
||||
},
|
||||
});
|
||||
bunext.bunextLog.success(`Wireguard setup complete!`);
|
||||
|
||||
syncWireguardHosts();
|
||||
await syncWireguardHosts();
|
||||
|
||||
bunext.bunextLog.info(`Server running on http://localhost:${server.port} ...`);
|
||||
|
||||
@@ -40,6 +40,9 @@ export type PagePropsType = {
|
||||
hosts?: BUN_SQLITE_WGUI_HOSTS[] | null;
|
||||
client?: BUN_SQLITE_WGUI_CLIENTS | null;
|
||||
clients?: BUN_SQLITE_WGUI_CLIENTS[] | null;
|
||||
client_config?: string | null;
|
||||
client_config_path?: string | null;
|
||||
client_qr_data_uri?: string | null;
|
||||
environment?: string;
|
||||
envs?: {
|
||||
R2_PUBLIC_DOMAIN?: string;
|
||||
@@ -199,6 +202,7 @@ export type ApiReqParams<
|
||||
user_id?: string | number | null;
|
||||
dependent_id?: string | number | null;
|
||||
host_id?: string | number | null;
|
||||
public_ip_address?: string | null;
|
||||
|
||||
media_base_64?: string;
|
||||
media_base_64_data_url?: string;
|
||||
|
||||
Reference in New Issue
Block a user