Add admin setup page
This commit is contained in:
@@ -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 Divider from "@/src/components/twui/layout/Divider";
|
||||
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() {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
|
||||
const setup = useSystemSetupStatus({
|
||||
setup_status: pageProps?.setup_status,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<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."
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -20,4 +34,4 @@ export default function AdminWireguardSetupPage() {
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `Admin Wireguard Setup | ${SiteData["SiteName"]}`,
|
||||
description: `Setup wireguard and wg-ui. Check if dependencies are installed. Update wg-ui and related packages. Etc.`,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user