Add admin setup page
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
# Wireguard UI
|
# Wireguard UI
|
||||||
|
|
||||||
An admin dashboard for Wireguard.
|
An admin dashboard for Wireguard.
|
||||||
|
|
||||||
|
Installation script located at `https://git.tben.me/Moduletrace/wireguard-ui/raw/branch/main/src/scripts/install-wg-ui.sh`
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ export const SiteData = {
|
|||||||
SiteDescription: `Wireguard UI is the ultimate dashboard for managing Wireguard.`,
|
SiteDescription: `Wireguard UI is the ultimate dashboard for managing Wireguard.`,
|
||||||
SiteSlug: `wgui`,
|
SiteSlug: `wgui`,
|
||||||
SiteURL: `https://git.tben.me/Moduletrace/wireguard-ui`,
|
SiteURL: `https://git.tben.me/Moduletrace/wireguard-ui`,
|
||||||
|
RepoURL: `https://git.tben.me/Moduletrace/wireguard-ui.git`,
|
||||||
ServerPort: 10752,
|
ServerPort: 10752,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
import grabDirNames from "@/src/utils/grab-dir-names";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
|
||||||
|
export type SystemSetupToolStatus = {
|
||||||
|
name: string;
|
||||||
|
command: string;
|
||||||
|
version?: string | null;
|
||||||
|
installed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SystemSetupLibDirStatus = {
|
||||||
|
label: string;
|
||||||
|
path: string;
|
||||||
|
exists: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SystemSetupStatus = {
|
||||||
|
is_root: boolean;
|
||||||
|
is_dev: boolean;
|
||||||
|
environment: string;
|
||||||
|
distro: string;
|
||||||
|
distro_pretty_name: string;
|
||||||
|
init_system: "systemd" | "openrc" | "unknown";
|
||||||
|
install_dir: string;
|
||||||
|
app_installed: boolean;
|
||||||
|
app_version: string | null;
|
||||||
|
repo_url: string | null;
|
||||||
|
repo_branch: string | null;
|
||||||
|
service_name: string;
|
||||||
|
service_active: boolean | null;
|
||||||
|
wg_module_loaded: boolean;
|
||||||
|
wg_quick_helper_installed: boolean;
|
||||||
|
tools: SystemSetupToolStatus[];
|
||||||
|
lib_dirs: SystemSetupLibDirStatus[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ShellResult = {
|
||||||
|
success: boolean;
|
||||||
|
output: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
ROOT_DIR,
|
||||||
|
WGUI_LIB_DIR,
|
||||||
|
WGUI_LIB_KEYS_DIR,
|
||||||
|
WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||||
|
WGUI_LIB_SCRIPTS_DIR,
|
||||||
|
WGUI_WG_QUICK_MANAGE_SCRIPT,
|
||||||
|
} = grabDirNames();
|
||||||
|
|
||||||
|
const TOOL_CHECKS: {
|
||||||
|
name: string;
|
||||||
|
command: string;
|
||||||
|
version_command?: string;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
name: "wireguard",
|
||||||
|
command: "wg",
|
||||||
|
version_command: "wg --version",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wg-quick",
|
||||||
|
command: "wg-quick",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ip (iproute2)",
|
||||||
|
command: "ip",
|
||||||
|
version_command: "ip -V",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "curl",
|
||||||
|
command: "curl",
|
||||||
|
version_command: "curl --version",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "git",
|
||||||
|
command: "git",
|
||||||
|
version_command: "git --version",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bun",
|
||||||
|
command: "bun",
|
||||||
|
version_command: "bun --version",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tryShell = ({
|
||||||
|
command,
|
||||||
|
timeout_ms = 10000,
|
||||||
|
}: {
|
||||||
|
command: string;
|
||||||
|
timeout_ms?: number;
|
||||||
|
}): ShellResult => {
|
||||||
|
try {
|
||||||
|
const result = execSync(command, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
stdio: "pipe",
|
||||||
|
timeout: timeout_ms,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
output: String(result).trim(),
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
const stdout = error?.stdout ? String(error.stdout).trim() : "";
|
||||||
|
const stderr = error?.stderr ? String(error.stderr).trim() : "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: [stdout, stderr].filter(Boolean).join("\n").trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const runCommand = (command: string, timeout_ms = 10000): ShellResult =>
|
||||||
|
tryShell({ command, timeout_ms });
|
||||||
|
|
||||||
|
const grabDistro = (): { id: string; pretty_name: string } => {
|
||||||
|
try {
|
||||||
|
const os_release = fs.readFileSync("/etc/os-release", "utf-8");
|
||||||
|
|
||||||
|
const id_match = os_release.match(/^ID=(.+)$/m);
|
||||||
|
const pretty_match = os_release.match(/^PRETTY_NAME=(.+)$/m);
|
||||||
|
|
||||||
|
const strip = (value: string) =>
|
||||||
|
value.replace(/^["']|["']$/g, "").trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id_match?.[1] ? strip(id_match[1]) : "unknown",
|
||||||
|
pretty_name: pretty_match?.[1] ? strip(pretty_match[1]) : "",
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
id: "unknown",
|
||||||
|
pretty_name: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const grabInitSystem = (): "systemd" | "openrc" | "unknown" => {
|
||||||
|
if (runCommand(`command -v systemctl`, 5000).success) {
|
||||||
|
return "systemd";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
runCommand(`command -v rc-service`, 5000).success ||
|
||||||
|
fs.existsSync("/etc/alpine-release")
|
||||||
|
) {
|
||||||
|
return "openrc";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
};
|
||||||
|
|
||||||
|
const grabGitValue = ({
|
||||||
|
install_dir,
|
||||||
|
command,
|
||||||
|
env_value,
|
||||||
|
}: {
|
||||||
|
install_dir: string;
|
||||||
|
command: string;
|
||||||
|
env_value?: string;
|
||||||
|
}): string | null => {
|
||||||
|
if (env_value) {
|
||||||
|
return env_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = runCommand(`git -C "${install_dir}" ${command}`, 10000);
|
||||||
|
|
||||||
|
return res.success && res.output ? res.output : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function grabSystemSetupStatus(): SystemSetupStatus {
|
||||||
|
const IS_DEV = (process.env.NODE_ENV || "development") == "development";
|
||||||
|
const IS_ROOT =
|
||||||
|
typeof process.getuid === "function" && process.getuid() === 0;
|
||||||
|
|
||||||
|
const INSTALL_DIR = IS_DEV ? ROOT_DIR : path.join(WGUI_LIB_DIR, "webapp");
|
||||||
|
|
||||||
|
const APP_INSTALLED = fs.existsSync(path.join(INSTALL_DIR, ".git"));
|
||||||
|
|
||||||
|
const tools: SystemSetupToolStatus[] = TOOL_CHECKS.map((tool) => {
|
||||||
|
const which_res = runCommand(`command -v ${tool.command}`, 5000);
|
||||||
|
const installed = which_res.success;
|
||||||
|
|
||||||
|
let version: string | null = null;
|
||||||
|
|
||||||
|
if (installed && tool.version_command) {
|
||||||
|
const version_res = runCommand(tool.version_command, 5000);
|
||||||
|
|
||||||
|
if (version_res.success) {
|
||||||
|
version = version_res.output.split("\n")[0] || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: tool.name,
|
||||||
|
command: tool.command,
|
||||||
|
installed,
|
||||||
|
version,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id: distro, pretty_name: distro_pretty_name } = grabDistro();
|
||||||
|
const init_system = grabInitSystem();
|
||||||
|
const service_name = process.env.SERVICE_NAME || "wgui";
|
||||||
|
|
||||||
|
let service_active: boolean | null = null;
|
||||||
|
|
||||||
|
if (!IS_DEV && init_system == "systemd") {
|
||||||
|
const service_res = runCommand(
|
||||||
|
`systemctl is-active ${service_name} 2>/dev/null`,
|
||||||
|
10000,
|
||||||
|
);
|
||||||
|
|
||||||
|
service_active = service_res.success && service_res.output == "active";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
is_root: IS_ROOT,
|
||||||
|
is_dev: IS_DEV,
|
||||||
|
environment: IS_DEV ? "development" : "production",
|
||||||
|
distro,
|
||||||
|
distro_pretty_name,
|
||||||
|
init_system,
|
||||||
|
install_dir: INSTALL_DIR,
|
||||||
|
app_installed: APP_INSTALLED,
|
||||||
|
app_version: APP_INSTALLED
|
||||||
|
? grabGitValue({
|
||||||
|
install_dir: INSTALL_DIR,
|
||||||
|
command: `rev-parse --short HEAD 2>/dev/null`,
|
||||||
|
})
|
||||||
|
: null,
|
||||||
|
repo_url: grabGitValue({
|
||||||
|
install_dir: INSTALL_DIR,
|
||||||
|
command: `config --get remote.origin.url 2>/dev/null`,
|
||||||
|
env_value: process.env.REPO_URL,
|
||||||
|
}),
|
||||||
|
repo_branch:
|
||||||
|
grabGitValue({
|
||||||
|
install_dir: INSTALL_DIR,
|
||||||
|
command: `branch --show-current 2>/dev/null`,
|
||||||
|
env_value: process.env.BRANCH,
|
||||||
|
}) || "main",
|
||||||
|
service_name,
|
||||||
|
service_active,
|
||||||
|
wg_module_loaded: fs.existsSync("/sys/module/wireguard"),
|
||||||
|
wg_quick_helper_installed: fs.existsSync(WGUI_WG_QUICK_MANAGE_SCRIPT),
|
||||||
|
tools,
|
||||||
|
lib_dirs: [
|
||||||
|
{
|
||||||
|
label: "Host configs",
|
||||||
|
path: WGUI_LIB_HOSTS_CONFIGS_DIR,
|
||||||
|
exists: fs.existsSync(WGUI_LIB_HOSTS_CONFIGS_DIR),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Scripts",
|
||||||
|
path: WGUI_LIB_SCRIPTS_DIR,
|
||||||
|
exists: fs.existsSync(WGUI_LIB_SCRIPTS_DIR),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import grabDirNames from "@/src/utils/grab-dir-names";
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
script: string;
|
||||||
|
timeout_ms?: number;
|
||||||
|
env?: Record<string, string | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RunSetupScriptResult = {
|
||||||
|
success: boolean;
|
||||||
|
output: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function runSetupScript({
|
||||||
|
script,
|
||||||
|
timeout_ms = 600000,
|
||||||
|
env,
|
||||||
|
}: Params): RunSetupScriptResult {
|
||||||
|
const { ROOT_DIR } = grabDirNames();
|
||||||
|
const script_path = path.join(ROOT_DIR, "src", "scripts", script);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = execSync(`bash "${script_path}" 2>&1`, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
stdio: "pipe",
|
||||||
|
timeout: timeout_ms,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
...env,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
output: String(result).trim(),
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
const stdout = error?.stdout ? String(error.stdout).trim() : "";
|
||||||
|
const stderr = error?.stderr ? String(error.stderr).trim() : "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: [stdout, stderr]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n")
|
||||||
|
.trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import LinkList from "@/src/components/twui/elements/LinkList";
|
|||||||
import Stack from "@/src/components/twui/layout/Stack";
|
import Stack from "@/src/components/twui/layout/Stack";
|
||||||
import { AppContext } from "@/src/pages/__root";
|
import { AppContext } from "@/src/pages/__root";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { LayoutDashboard, LogOut, Server, Settings, Users } from "lucide-react";
|
import { LayoutDashboard, LogOut, Server, Settings, Users, Wrench } from "lucide-react";
|
||||||
|
|
||||||
const sectionLabel = (label: string) => (
|
const sectionLabel = (label: string) => (
|
||||||
<div className="px-3 pt-6 pb-1.5 text-[11.5px] font-semibold uppercase tracking-[0.08em] text-foreground-light/40 dark:text-foreground-dark/40">
|
<div className="px-3 pt-6 pb-1.5 text-[11.5px] font-semibold uppercase tracking-[0.08em] text-foreground-light/40 dark:text-foreground-dark/40">
|
||||||
@@ -27,6 +27,9 @@ export default function AdminAsideLinks() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const is_super_admin =
|
||||||
|
user_types?.find((t) => t.user_type === "super_admin") !== undefined;
|
||||||
|
|
||||||
const links: TWUILink[] = [
|
const links: TWUILink[] = [
|
||||||
{ component: sectionLabel("Overview") },
|
{ component: sectionLabel("Overview") },
|
||||||
{
|
{
|
||||||
@@ -54,6 +57,22 @@ export default function AdminAsideLinks() {
|
|||||||
url: "/admin/clients",
|
url: "/admin/clients",
|
||||||
icon: <Users size={16} strokeWidth={2} className={iconClassName} />,
|
icon: <Users size={16} strokeWidth={2} className={iconClassName} />,
|
||||||
},
|
},
|
||||||
|
{ component: sectionLabel("System") },
|
||||||
|
...(is_super_admin
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: "Setup",
|
||||||
|
url: "/admin/setup",
|
||||||
|
icon: (
|
||||||
|
<Wrench
|
||||||
|
size={16}
|
||||||
|
strokeWidth={2}
|
||||||
|
className={iconClassName}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{ component: sectionLabel("Account") },
|
{ component: sectionLabel("Account") },
|
||||||
{
|
{
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
|
||||||
|
import type grabSystemSetupStatus from "@/src/functions/backend/setup/grab-system-setup-status";
|
||||||
|
import type { ApiReqParams } from "@/src/types";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export type SystemSetupStatus = ReturnType<typeof grabSystemSetupStatus>;
|
||||||
|
|
||||||
|
type SetupStatusResponse = {
|
||||||
|
success: boolean;
|
||||||
|
singleRes?: SystemSetupStatus | null;
|
||||||
|
msg?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setup_status?: SystemSetupStatus | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useSystemSetupStatus({ setup_status }: Props) {
|
||||||
|
const [current, setCurrent] = useState<SystemSetupStatus | null>(
|
||||||
|
setup_status || null,
|
||||||
|
);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const refresh = () =>
|
||||||
|
fetchApi<ApiReqParams, SetupStatusResponse>(`/api/admin/system-setup-status`, {
|
||||||
|
method: "GET",
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.success && res.singleRes) {
|
||||||
|
setCurrent(res.singleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshWithLoading = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
refresh().finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
setup_status: current,
|
||||||
|
refresh: refreshWithLoading,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import AdminCard from "@/src/components/general/admin-card";
|
||||||
|
import Button from "@/src/components/twui/layout/Button";
|
||||||
|
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 useStatus from "@/src/components/twui/hooks/useStatus";
|
||||||
|
import { TriangleAlert } from "lucide-react";
|
||||||
|
import { useState, type ComponentProps, type ReactNode } from "react";
|
||||||
|
import SetupConsole from "./setup-console";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
description?: string | ReactNode;
|
||||||
|
icon?: ReactNode;
|
||||||
|
button_title: string;
|
||||||
|
buttonProps?: Omit<ComponentProps<typeof Button>, "title">;
|
||||||
|
on_run: () =>
|
||||||
|
| Promise<{ success: boolean; msg?: string } | void>
|
||||||
|
| { success: boolean; msg?: string }
|
||||||
|
| void;
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SetupActionCard({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
icon,
|
||||||
|
button_title,
|
||||||
|
buttonProps,
|
||||||
|
on_run,
|
||||||
|
children,
|
||||||
|
}: Props) {
|
||||||
|
const { loading, setLoading, status, setStatus } = useStatus();
|
||||||
|
const [output, setOutput] = useState<string | null>(null);
|
||||||
|
const [is_error, setIsError] = useState(false);
|
||||||
|
|
||||||
|
const run = () => {
|
||||||
|
setLoading(true);
|
||||||
|
setStatus(undefined);
|
||||||
|
setOutput(null);
|
||||||
|
|
||||||
|
Promise.resolve()
|
||||||
|
.then(() => on_run())
|
||||||
|
.then((res) => {
|
||||||
|
if (res) {
|
||||||
|
setOutput(res.msg || null);
|
||||||
|
setIsError(!res.success);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
setStatus({
|
||||||
|
error: true,
|
||||||
|
msg: `Request failed — the wg-ui server may have restarted. ${
|
||||||
|
error?.message || ""
|
||||||
|
}`.trim(),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminCard className="w-full p-5 flex flex-col gap-4">
|
||||||
|
<Row className="gap-3 items-start">
|
||||||
|
{icon ? (
|
||||||
|
<Span className="p-2.5 rounded-lg bg-primary/10 dark:bg-primary-dark/10 text-primary dark:text-primary-dark shrink-0">
|
||||||
|
{icon}
|
||||||
|
</Span>
|
||||||
|
) : null}
|
||||||
|
<Stack className="gap-1 min-w-0">
|
||||||
|
<H3 className="text-[14px] font-semibold mb-0!">
|
||||||
|
{title}
|
||||||
|
</H3>
|
||||||
|
{description ? (
|
||||||
|
typeof description == "string" ? (
|
||||||
|
<P
|
||||||
|
noMargin
|
||||||
|
className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45"
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</P>
|
||||||
|
) : (
|
||||||
|
description
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
title={button_title}
|
||||||
|
className="w-full"
|
||||||
|
loading={loading}
|
||||||
|
{...buttonProps}
|
||||||
|
onClick={run}
|
||||||
|
>
|
||||||
|
{button_title}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{status?.error && status.msg ? (
|
||||||
|
<Row className="gap-2 items-center bg-error/5 rounded-lg px-3 py-2 text-error">
|
||||||
|
<TriangleAlert size={15} className="shrink-0" />
|
||||||
|
<Span className="text-[12.5px]">{status.msg}</Span>
|
||||||
|
</Row>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<SetupConsole output={output} error={is_error} />
|
||||||
|
</AdminCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import Card from "@/src/components/twui/elements/Card";
|
||||||
|
import Tag from "@/src/components/twui/elements/Tag";
|
||||||
|
import Span from "@/src/components/twui/layout/Span";
|
||||||
|
import Stack from "@/src/components/twui/layout/Stack";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
ok: boolean;
|
||||||
|
ok_label?: string;
|
||||||
|
fail_label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SetupCheckStatusItem({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
ok,
|
||||||
|
ok_label = "Installed",
|
||||||
|
fail_label = "Missing",
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
noHover
|
||||||
|
className="w-full p-2.5 flex flex-row items-center gap-2.5"
|
||||||
|
title={subtitle}
|
||||||
|
>
|
||||||
|
<Span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={twMerge(
|
||||||
|
"relative flex w-2.5 h-2.5 shrink-0 rounded-full",
|
||||||
|
ok ? "bg-success" : "bg-error",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Stack className="gap-1 min-w-0 flex-1">
|
||||||
|
<Span
|
||||||
|
className={twMerge(
|
||||||
|
"text-[13px] font-semibold leading-none text-foreground-light",
|
||||||
|
"dark:text-foreground-dark truncate w-full line-clamp-1",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Span>
|
||||||
|
{subtitle ? (
|
||||||
|
<Span
|
||||||
|
className={twMerge(
|
||||||
|
"font-mono text-[11.5px] text-foreground-light/45 dark:text-foreground-dark/45 truncate",
|
||||||
|
"line-clamp-1 w-full",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{subtitle}
|
||||||
|
</Span>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
<Tag
|
||||||
|
variant="outlined"
|
||||||
|
color={ok ? "success" : "error"}
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
{ok ? ok_label : fail_label}
|
||||||
|
</Tag>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import Span from "@/src/components/twui/layout/Span";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
output?: string | null;
|
||||||
|
error?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SetupConsole({ output, error }: Props) {
|
||||||
|
if (!output) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-live="polite"
|
||||||
|
className={twMerge(
|
||||||
|
"w-full max-h-64 overflow-y-auto overflow-x-hidden rounded-lg bg-slate-950 p-3",
|
||||||
|
"border border-solid",
|
||||||
|
error ? "border-error/40" : "border-slate-700/40",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<pre className="text-[11.5px] leading-5 font-mono whitespace-pre-wrap break-words text-slate-300">
|
||||||
|
{output}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import Span from "@/src/components/twui/layout/Span";
|
||||||
|
import Stack from "@/src/components/twui/layout/Stack";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export type SetupStatCellTone = "success" | "error" | "warning" | "muted";
|
||||||
|
|
||||||
|
const toneClassName: Record<SetupStatCellTone, string> = {
|
||||||
|
success: "text-success",
|
||||||
|
error: "text-error",
|
||||||
|
warning: "text-warning",
|
||||||
|
muted: "text-foreground-light/40 dark:text-foreground-dark/40",
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
tone?: SetupStatCellTone;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SetupStatCell({ label, value, tone }: Props) {
|
||||||
|
return (
|
||||||
|
<Stack className="gap-1 min-w-0">
|
||||||
|
<Span className="text-[11.5px] font-semibold uppercase tracking-[0.08em] text-foreground-light/40 dark:text-foreground-dark/40">
|
||||||
|
{label}
|
||||||
|
</Span>
|
||||||
|
<Span
|
||||||
|
title={value}
|
||||||
|
className={twMerge(
|
||||||
|
"tabular text-[15px] font-semibold truncate line-clamp-1 w-full",
|
||||||
|
tone
|
||||||
|
? toneClassName[tone]
|
||||||
|
: "text-foreground-light/85 dark:text-foreground-dark/85",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Span>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
|
||||||
|
import Input from "@/src/components/twui/form/Input";
|
||||||
|
import Stack from "@/src/components/twui/layout/Stack";
|
||||||
|
import Span from "@/src/components/twui/layout/Span";
|
||||||
|
import type useSystemSetupStatus from "../(hooks)/use-system-setup-status";
|
||||||
|
import SetupActionCard from "./setup-action-card";
|
||||||
|
import type { ApiReqParams } from "@/src/types";
|
||||||
|
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||||
|
import { CloudDownload } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { SiteData } from "@/src/data/site-data";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setup: ReturnType<typeof useSystemSetupStatus>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SetupUpdateWgUiAction({ setup }: Props) {
|
||||||
|
const setup_status = setup.setup_status;
|
||||||
|
|
||||||
|
const [repoUrl, setRepoUrl] = useState<string>(
|
||||||
|
setup_status?.repo_url || "",
|
||||||
|
);
|
||||||
|
const [branch, setBranch] = useState<string>(
|
||||||
|
setup_status?.repo_branch || "main",
|
||||||
|
);
|
||||||
|
|
||||||
|
const needs_repo_url = !setup_status?.is_dev && !setup_status?.repo_url;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SetupActionCard
|
||||||
|
title="Update wg-ui"
|
||||||
|
description={
|
||||||
|
needs_repo_url ? (
|
||||||
|
<Span className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45">
|
||||||
|
wg-ui isn't installed yet — provide the git repository
|
||||||
|
URL so the installer can clone it, then run the update
|
||||||
|
again to pull newer versions.
|
||||||
|
</Span>
|
||||||
|
) : setup_status?.is_dev ? (
|
||||||
|
<Span className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45">
|
||||||
|
Development mode — uses this local checkout. Reinstalls
|
||||||
|
dependencies and syncs the wg-quick helper script. The
|
||||||
|
dev server is not restarted.
|
||||||
|
</Span>
|
||||||
|
) : (
|
||||||
|
<Span className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45">
|
||||||
|
Pulls the latest code, installs dependencies, syncs
|
||||||
|
helper scripts and restarts the{" "}
|
||||||
|
{setup_status?.service_name || "wgui"} service. The
|
||||||
|
server will briefly go offline.
|
||||||
|
</Span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
icon={<CloudDownload size={19} />}
|
||||||
|
button_title={
|
||||||
|
setup_status?.app_installed ? "Update wg-ui" : "Install wg-ui"
|
||||||
|
}
|
||||||
|
on_run={async () => {
|
||||||
|
const res = await fetchApi<ApiReqParams, APIResponseObject>(
|
||||||
|
`/api/admin/update-wg-ui`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
repo_url: repoUrl,
|
||||||
|
branch,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
setup.refresh();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{needs_repo_url || !setup_status?.is_dev ? (
|
||||||
|
<Stack className="gap-2.5">
|
||||||
|
{needs_repo_url ? (
|
||||||
|
<Input<"repo_url">
|
||||||
|
label="Repository URL"
|
||||||
|
placeholder={SiteData["RepoURL"]}
|
||||||
|
value={repoUrl}
|
||||||
|
onChange={(e) => {
|
||||||
|
setRepoUrl(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{!setup_status?.is_dev ? (
|
||||||
|
<Input<"branch">
|
||||||
|
label="Branch"
|
||||||
|
placeholder="main"
|
||||||
|
value={branch}
|
||||||
|
onChange={(e) => {
|
||||||
|
setBranch(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
) : null}
|
||||||
|
</SetupActionCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
|
||||||
|
import Row from "@/src/components/twui/layout/Row";
|
||||||
|
import Span from "@/src/components/twui/layout/Span";
|
||||||
|
import type useSystemSetupStatus from "../(hooks)/use-system-setup-status";
|
||||||
|
import SetupActionCard from "../(partials)/setup-action-card";
|
||||||
|
import SetupUpdateWgUiAction from "../(partials)/setup-update-wg-ui-action";
|
||||||
|
import type { ApiReqParams } from "@/src/types";
|
||||||
|
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||||
|
import { RotateCcw, TriangleAlert, Wrench } from "lucide-react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setup: ReturnType<typeof useSystemSetupStatus>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WG_TOOL_COMMANDS = [`wg`, `wg-quick`, `ip`, `curl`];
|
||||||
|
|
||||||
|
export default function SetupActionsSection({ setup }: Props) {
|
||||||
|
const setup_status = setup.setup_status;
|
||||||
|
|
||||||
|
if (!setup_status) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const have_wg_tools = setup_status.tools
|
||||||
|
.filter((tool) => WG_TOOL_COMMANDS.includes(tool.command))
|
||||||
|
.every((tool) => tool.installed);
|
||||||
|
|
||||||
|
const show_restart =
|
||||||
|
!setup_status.is_dev && setup_status.init_system !== "unknown";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!setup_status.is_root ? (
|
||||||
|
<Row className="gap-2 items-center bg-warning/5 rounded-lg px-3 py-2.5 text-warning">
|
||||||
|
<TriangleAlert size={16} className="shrink-0" />
|
||||||
|
<Span className="text-[12.5px]">
|
||||||
|
The server is not running as root — installs and
|
||||||
|
tunnel management may fail. Run wg-ui as root for full
|
||||||
|
functionality.
|
||||||
|
</Span>
|
||||||
|
</Row>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<SetupActionCard
|
||||||
|
title="WireGuard tools"
|
||||||
|
description="Installs or updates the WireGuard tools (wg, wg-quick, iproute2) using your distro's package manager. Safe to re-run — the installer is idempotent."
|
||||||
|
icon={<Wrench size={19} />}
|
||||||
|
button_title={
|
||||||
|
have_wg_tools
|
||||||
|
? "Update WireGuard tools"
|
||||||
|
: "Install WireGuard tools"
|
||||||
|
}
|
||||||
|
on_run={async () => {
|
||||||
|
const res = await fetchApi<ApiReqParams, APIResponseObject>(
|
||||||
|
`/api/admin/setup-wireguard-tools`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
setup.refresh();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SetupUpdateWgUiAction setup={setup} />
|
||||||
|
|
||||||
|
{show_restart ? (
|
||||||
|
<SetupActionCard
|
||||||
|
title="Restart service"
|
||||||
|
description={`Restarts the ${setup_status.service_name} system service. Use this after manual changes — the server goes briefly offline.`}
|
||||||
|
icon={<RotateCcw size={19} />}
|
||||||
|
button_title="Restart service"
|
||||||
|
on_run={async () => {
|
||||||
|
const res = await fetchApi<
|
||||||
|
ApiReqParams,
|
||||||
|
APIResponseObject
|
||||||
|
>(`/api/admin/restart-wg-ui-service`, {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
|
||||||
|
setup.refresh();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import AdminCard from "@/src/components/general/admin-card";
|
||||||
|
import Button from "@/src/components/twui/layout/Button";
|
||||||
|
import Divider from "@/src/components/twui/layout/Divider";
|
||||||
|
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 EmptyContent from "@/src/components/twui/elements/EmptyContent";
|
||||||
|
import type useSystemSetupStatus from "../(hooks)/use-system-setup-status";
|
||||||
|
import SetupCheckStatusItem from "../(partials)/setup-check-status-item";
|
||||||
|
import SetupStatCell, {
|
||||||
|
type SetupStatCellTone,
|
||||||
|
} from "../(partials)/setup-stat-cell";
|
||||||
|
import { RefreshCw } from "lucide-react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setup: ReturnType<typeof useSystemSetupStatus>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SystemStatusSection({ setup }: Props) {
|
||||||
|
const setup_status = setup.setup_status;
|
||||||
|
|
||||||
|
if (!setup_status) {
|
||||||
|
return <EmptyContent title="No system status available" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const service_label =
|
||||||
|
setup_status.service_active === null
|
||||||
|
? "Not managed"
|
||||||
|
: setup_status.service_active
|
||||||
|
? "Active"
|
||||||
|
: "Inactive";
|
||||||
|
|
||||||
|
const service_tone: SetupStatCellTone =
|
||||||
|
setup_status.service_active === null
|
||||||
|
? "muted"
|
||||||
|
: setup_status.service_active
|
||||||
|
? "success"
|
||||||
|
: "error";
|
||||||
|
|
||||||
|
const overview_items: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
tone?: SetupStatCellTone;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
label: "Environment",
|
||||||
|
value: setup_status.environment,
|
||||||
|
tone: setup_status.is_dev ? "warning" : "success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Running as root",
|
||||||
|
value: setup_status.is_root ? "Yes" : "No",
|
||||||
|
tone: setup_status.is_root ? "success" : "error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Distro",
|
||||||
|
value: setup_status.distro_pretty_name || setup_status.distro,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Init system",
|
||||||
|
value: setup_status.init_system,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Install directory",
|
||||||
|
value: setup_status.install_dir,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "App version",
|
||||||
|
value: setup_status.app_version || "—",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Repository",
|
||||||
|
value: setup_status.repo_url || "—",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Service",
|
||||||
|
value: setup_status.service_name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Service status",
|
||||||
|
value: service_label,
|
||||||
|
tone: service_tone,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const runtime_items: {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
ok: boolean;
|
||||||
|
ok_label: string;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
key: "kernel-module",
|
||||||
|
title: "WireGuard kernel module",
|
||||||
|
subtitle: "/sys/module/wireguard",
|
||||||
|
ok: setup_status.wg_module_loaded,
|
||||||
|
ok_label: "Loaded",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "wg-quick-helper",
|
||||||
|
title: "wg-quick manage helper",
|
||||||
|
subtitle: `/var/lib/wgui/scripts/wg-quick-manage.sh`,
|
||||||
|
ok: setup_status.wg_quick_helper_installed,
|
||||||
|
ok_label: "Installed",
|
||||||
|
},
|
||||||
|
...setup_status.lib_dirs.map((dir) => ({
|
||||||
|
key: dir.path,
|
||||||
|
title: `Runtime directory · ${dir.label}`,
|
||||||
|
subtitle: dir.path,
|
||||||
|
ok: dir.exists,
|
||||||
|
ok_label: "Exists",
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminCard className="w-full p-5 flex flex-col gap-4">
|
||||||
|
<Row className="justify-between items-start gap-3">
|
||||||
|
<Stack className="gap-1">
|
||||||
|
<H3 className="text-[14px] font-semibold mb-0!">
|
||||||
|
System status
|
||||||
|
</H3>
|
||||||
|
<P
|
||||||
|
noMargin
|
||||||
|
className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45"
|
||||||
|
>
|
||||||
|
The web server's environment and required tools
|
||||||
|
</P>
|
||||||
|
</Stack>
|
||||||
|
<Button
|
||||||
|
title="Refresh system status"
|
||||||
|
size="small"
|
||||||
|
variant="ghost"
|
||||||
|
color="gray"
|
||||||
|
beforeIcon={<RefreshCw size={15} />}
|
||||||
|
loading={setup.loading}
|
||||||
|
onClick={setup.refresh}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Divider className="w-full" />
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-x-4 gap-y-5 min-w-0">
|
||||||
|
{overview_items.map((item) => (
|
||||||
|
<SetupStatCell
|
||||||
|
key={item.label}
|
||||||
|
label={item.label}
|
||||||
|
value={item.value}
|
||||||
|
tone={item.tone}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider className="w-full" />
|
||||||
|
|
||||||
|
<Span className="text-[11.5px] font-semibold uppercase tracking-[0.08em] text-foreground-light/40 dark:text-foreground-dark/40">
|
||||||
|
WireGuard tools
|
||||||
|
</Span>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
|
{setup_status.tools.map((tool) => (
|
||||||
|
<SetupCheckStatusItem
|
||||||
|
key={tool.command}
|
||||||
|
title={tool.name}
|
||||||
|
subtitle={
|
||||||
|
tool.version
|
||||||
|
? `${tool.command} · ${tool.version}`
|
||||||
|
: tool.command
|
||||||
|
}
|
||||||
|
ok={tool.installed}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Span className="text-[11.5px] font-semibold uppercase tracking-[0.08em] text-foreground-light/40 dark:text-foreground-dark/40">
|
||||||
|
Runtime
|
||||||
|
</Span>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
{runtime_items.map((item) => (
|
||||||
|
<SetupCheckStatusItem
|
||||||
|
key={item.key}
|
||||||
|
title={item.title}
|
||||||
|
subtitle={item.subtitle}
|
||||||
|
ok={item.ok}
|
||||||
|
ok_label={item.ok_label}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</AdminCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||||
|
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||||
|
import grabSystemSetupStatus from "@/src/functions/backend/setup/grab-system-setup-status";
|
||||||
|
import type { PagePropsType } from "@/src/types";
|
||||||
|
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||||
|
|
||||||
|
const server: BunextPageServerFn<PagePropsType> = async ({ req }) => {
|
||||||
|
const { user, user_types } = await userAuth({ req });
|
||||||
|
|
||||||
|
if (!user?.logged_in_status || !user.id) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/auth/login",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_super_admin = checkUserAccess({ user_types });
|
||||||
|
|
||||||
|
if (!is_super_admin.success) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/admin",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
setup_status: grabSystemSetupStatus(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default server;
|
||||||
@@ -3,8 +3,19 @@ import AdminHero from "@/src/components/general/admin-hero";
|
|||||||
import { SiteData } from "@/src/data/site-data";
|
import { SiteData } from "@/src/data/site-data";
|
||||||
import Divider from "@/src/components/twui/layout/Divider";
|
import Divider from "@/src/components/twui/layout/Divider";
|
||||||
import Stack from "@/src/components/twui/layout/Stack";
|
import Stack from "@/src/components/twui/layout/Stack";
|
||||||
|
import useSystemSetupStatus from "./(hooks)/use-system-setup-status";
|
||||||
|
import SystemStatusSection from "./(sections)/system-status-section";
|
||||||
|
import SetupActionsSection from "./(sections)/setup-actions-section";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { AppContext } from "@/src/pages/__root";
|
||||||
|
|
||||||
export default function AdminWireguardSetupPage() {
|
export default function AdminWireguardSetupPage() {
|
||||||
|
const { pageProps } = useContext(AppContext);
|
||||||
|
|
||||||
|
const setup = useSystemSetupStatus({
|
||||||
|
setup_status: pageProps?.setup_status,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AdminHero
|
<AdminHero
|
||||||
@@ -12,7 +23,10 @@ export default function AdminWireguardSetupPage() {
|
|||||||
description="Setup wireguard and wg-ui. Check if dependencies are installed. Update wg-ui and related packages. Etc."
|
description="Setup wireguard and wg-ui. Check if dependencies are installed. Update wg-ui and related packages. Etc."
|
||||||
/>
|
/>
|
||||||
<Divider className="mb-6" />
|
<Divider className="mb-6" />
|
||||||
<Stack className="w-full px-6 pb-8 gap-5 items-stretch"></Stack>
|
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
|
||||||
|
<SystemStatusSection setup={setup} />
|
||||||
|
<SetupActionsSection setup={setup} />
|
||||||
|
</Stack>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||||
|
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||||
|
import grabSystemSetupStatus from "@/src/functions/backend/setup/grab-system-setup-status";
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import type { APIResponseObject, BunextAPIRouteHandler } from "@moduletrace/bunext/types";
|
||||||
|
|
||||||
|
const restart_service = ({
|
||||||
|
command,
|
||||||
|
}: {
|
||||||
|
command: string;
|
||||||
|
}): APIResponseObject => {
|
||||||
|
try {
|
||||||
|
const output = execSync(command, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
stdio: "pipe",
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
msg: String(output).trim() || `Restarted service successfully`,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
const stdout = error?.stdout ? String(error.stdout).trim() : "";
|
||||||
|
const stderr = error?.stderr ? String(error.stderr).trim() : "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `${error.message}\n${[stdout, stderr]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n")
|
||||||
|
.trim()}`.trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||||
|
req,
|
||||||
|
}) => {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_super_admin = checkUserAccess({ user_types });
|
||||||
|
|
||||||
|
if (!is_super_admin.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Unauthorized`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const setup_status = grabSystemSetupStatus();
|
||||||
|
|
||||||
|
if (setup_status.is_dev) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Development mode — the wg-ui server is run manually. Restart it yourself (e.g. Ctrl+C then run the dev command again).`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (setup_status.init_system) {
|
||||||
|
case "systemd":
|
||||||
|
return restart_service({
|
||||||
|
command: `systemctl restart ${setup_status.service_name}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
case "openrc":
|
||||||
|
return restart_service({
|
||||||
|
command: `rc-service ${setup_status.service_name} restart`,
|
||||||
|
});
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `No supported init system detected — restart the wg-ui process manually.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||||
|
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||||
|
import runSetupScript from "@/src/functions/backend/setup/run-setup-script";
|
||||||
|
import type { APIResponseObject, BunextAPIRouteHandler } from "@moduletrace/bunext/types";
|
||||||
|
|
||||||
|
const SETUP_WIREGUARD_SCRIPT = `setup-wireguard.sh`;
|
||||||
|
const SETUP_WIREGUARD_TIMEOUT_MS = 15 * 60 * 1000;
|
||||||
|
|
||||||
|
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||||
|
req,
|
||||||
|
}) => {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_super_admin = checkUserAccess({ user_types });
|
||||||
|
|
||||||
|
if (!is_super_admin.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Unauthorized`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { success, output } = runSetupScript({
|
||||||
|
script: SETUP_WIREGUARD_SCRIPT,
|
||||||
|
timeout_ms: SETUP_WIREGUARD_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success,
|
||||||
|
msg: output,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||||
|
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||||
|
import grabSystemSetupStatus, {
|
||||||
|
type SystemSetupStatus,
|
||||||
|
} from "@/src/functions/backend/setup/grab-system-setup-status";
|
||||||
|
import type {
|
||||||
|
APIResponseObject,
|
||||||
|
BunextAPIRouteHandler,
|
||||||
|
} from "@moduletrace/bunext/types";
|
||||||
|
|
||||||
|
export const handler: BunextAPIRouteHandler<
|
||||||
|
APIResponseObject<SystemSetupStatus>
|
||||||
|
> = async ({ req }) => {
|
||||||
|
if (req.method !== "GET") {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user, user_types } = await userAuth({ req });
|
||||||
|
|
||||||
|
if (!user?.logged_in_status) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Unauthorized`,
|
||||||
|
logoutUser: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_super_admin = checkUserAccess({ user_types });
|
||||||
|
|
||||||
|
if (!is_super_admin.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Unauthorized`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
singleRes: grabSystemSetupStatus(),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||||
|
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||||
|
import grabSystemSetupStatus from "@/src/functions/backend/setup/grab-system-setup-status";
|
||||||
|
import runSetupScript from "@/src/functions/backend/setup/run-setup-script";
|
||||||
|
import type { ApiReqParams } from "@/src/types";
|
||||||
|
import type { APIResponseObject, BunextAPIRouteHandler } from "@moduletrace/bunext/types";
|
||||||
|
|
||||||
|
const INSTALL_WG_UI_SCRIPT = `install-wg-ui.sh`;
|
||||||
|
const INSTALL_WG_UI_TIMEOUT_MS = 20 * 60 * 1000;
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_super_admin = checkUserAccess({ user_types });
|
||||||
|
|
||||||
|
if (!is_super_admin.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `Unauthorized`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const setup_status = grabSystemSetupStatus();
|
||||||
|
|
||||||
|
const { success, output } = runSetupScript({
|
||||||
|
script: INSTALL_WG_UI_SCRIPT,
|
||||||
|
timeout_ms: INSTALL_WG_UI_TIMEOUT_MS,
|
||||||
|
env: {
|
||||||
|
NODE_ENV: setup_status.is_dev ? "development" : "production",
|
||||||
|
REPO_URL:
|
||||||
|
body.repo_url?.trim() || setup_status.repo_url || "",
|
||||||
|
BRANCH:
|
||||||
|
body.branch?.trim() || setup_status.repo_branch || "main",
|
||||||
|
SERVICE_NAME: setup_status.service_name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success,
|
||||||
|
msg: output,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -48,7 +48,7 @@ if [ "$DEV_MODE" = true ]; then
|
|||||||
log "NODE_ENV=development — using local repo at $INSTALL_DIR, skipping clone and system service installation (daemon is assumed to already be running)"
|
log "NODE_ENV=development — using local repo at $INSTALL_DIR, skipping clone and system service installation (daemon is assumed to already be running)"
|
||||||
elif [ -z "$REPO_URL" ]; then
|
elif [ -z "$REPO_URL" ]; then
|
||||||
fail "REPO_URL is not set — pass the git URL of the wg-ui repo, e.g.
|
fail "REPO_URL is not set — pass the git URL of the wg-ui repo, e.g.
|
||||||
REPO_URL=https://git.example.com/org/wireguard-ui.git $0"
|
REPO_URL=https://git.tben.me/Moduletrace/wireguard-ui.git $0"
|
||||||
else
|
else
|
||||||
INSTALL_DIR="${INSTALL_DIR:-$WGUI_LIB_DIR/webapp}"
|
INSTALL_DIR="${INSTALL_DIR:-$WGUI_LIB_DIR/webapp}"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import type {
|
|||||||
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";
|
||||||
|
import type grabSystemSetupStatus from "../functions/backend/setup/grab-system-setup-status";
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -71,6 +72,7 @@ export type PagePropsType = {
|
|||||||
main_host?: BUN_SQLITE_WGUI_HOSTS | null;
|
main_host?: BUN_SQLITE_WGUI_HOSTS | null;
|
||||||
next_available_client_ip?: string | null;
|
next_available_client_ip?: string | null;
|
||||||
is_local_ip?: boolean | null;
|
is_local_ip?: boolean | null;
|
||||||
|
setup_status?: ReturnType<typeof grabSystemSetupStatus> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AppContextObject = {
|
export type AppContextObject = {
|
||||||
@@ -267,6 +269,8 @@ export type ApiReqParams<
|
|||||||
ip_address?: string;
|
ip_address?: string;
|
||||||
wg_ip_address?: string | null;
|
wg_ip_address?: string | null;
|
||||||
is_main_host?: boolean | null;
|
is_main_host?: boolean | null;
|
||||||
|
repo_url?: string;
|
||||||
|
branch?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserAuthReturn = {
|
export type UserAuthReturn = {
|
||||||
|
|||||||
Reference in New Issue
Block a user