Add TLS feature with Caddy
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { Activity, Shield, Wrench } from "lucide-react";
|
||||
import { Activity, LockKeyhole, Shield, Wrench } from "lucide-react";
|
||||
import type { SubNavItemType } from "@/src/components/general/sub-nav/sub-nav-item";
|
||||
|
||||
export const SetupNav: SubNavItemType[] = [
|
||||
{ title: "System status", href: "/admin/setup/status", Icon: Activity },
|
||||
{ title: "Maintenance", href: "/admin/setup/maintenance", Icon: Wrench },
|
||||
{ title: "Firewall", href: "/admin/setup/firewall", Icon: Shield },
|
||||
{ title: "TLS ingress", href: "/admin/setup/tls", Icon: LockKeyhole },
|
||||
];
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
|
||||
import type grabTlsStatus from "@/src/functions/backend/acme/grab-tls-status";
|
||||
import type { ApiReqParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export type TlsStatus = Awaited<ReturnType<typeof grabTlsStatus>>;
|
||||
|
||||
type TlsStatusResponse = APIResponseObject & {
|
||||
singleRes?: TlsStatus | null;
|
||||
};
|
||||
|
||||
export default function useTlsStatus() {
|
||||
const [tls_status, setTlsStatus] = useState<TlsStatus | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = () =>
|
||||
fetchApi<ApiReqParams, TlsStatusResponse>(`/api/admin/tls/status`, {
|
||||
method: "GET",
|
||||
}).then((res) => {
|
||||
if (res.success && res.singleRes) {
|
||||
setTlsStatus(res.singleRes);
|
||||
setError(null);
|
||||
} else {
|
||||
setError(res.msg || `Could not fetch TLS status`);
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
|
||||
const refreshWithLoading = () => {
|
||||
setLoading(true);
|
||||
|
||||
refresh()
|
||||
.catch((request_error: any) => {
|
||||
setError(
|
||||
request_error?.message || `Could not fetch TLS status`,
|
||||
);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const sync = () =>
|
||||
fetchApi<ApiReqParams, APIResponseObject>(`/api/admin/tls/sync`, {
|
||||
method: "POST",
|
||||
}).then(async (res) => {
|
||||
await refresh();
|
||||
return res;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
refreshWithLoading();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
tls_status,
|
||||
refresh: refreshWithLoading,
|
||||
sync,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ServerCog } from "lucide-react";
|
||||
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
|
||||
import type { ApiReqParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import SetupActionRow from "./setup-action-row";
|
||||
|
||||
type Props = {
|
||||
installed?: boolean | null;
|
||||
on_done?: () => void;
|
||||
};
|
||||
|
||||
export default function SetupCaddyAction({ installed, on_done }: Props) {
|
||||
return (
|
||||
<SetupActionRow
|
||||
title="Caddy TLS ingress"
|
||||
description="Installs or repairs the Caddy binary and the wgui-caddy service. Certificate issuance is handled by wg-ui with manual DNS-01. Safe to re-run."
|
||||
icon={<ServerCog size={15} />}
|
||||
button_title={installed ? "Repair Caddy" : "Install Caddy"}
|
||||
confirm_params={{
|
||||
title: installed ? "Repair the Caddy ingress?" : "Install Caddy?",
|
||||
message:
|
||||
"This installs Caddy and the wgui-caddy service. Existing routes and certificates are preserved.",
|
||||
confirm_label: installed ? "Repair Caddy" : "Install Caddy",
|
||||
}}
|
||||
on_run={async () => {
|
||||
const res = await fetchApi<ApiReqParams, APIResponseObject>(
|
||||
`/api/admin/install-caddy`,
|
||||
{ method: "POST" },
|
||||
);
|
||||
|
||||
on_done?.();
|
||||
return res;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import type { ApiReqParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import type useSystemSetupStatus from "../(hooks)/use-system-setup-status";
|
||||
import SetupActionRow from "../(partials)/setup-action-row";
|
||||
import SetupCaddyAction from "../(partials)/setup-caddy-action";
|
||||
import SetupUpdateWgUiAction from "../(partials)/setup-update-wg-ui-action";
|
||||
|
||||
type Props = {
|
||||
@@ -24,6 +25,11 @@ export default function SetupActionsSection({ setup }: Props) {
|
||||
.filter((tool) => WG_TOOL_COMMANDS.includes(tool.command))
|
||||
.every((tool) => tool.installed);
|
||||
|
||||
const have_caddy = Boolean(
|
||||
setup_status.tools.find((tool) => tool.command == "/opt/caddy/caddy")
|
||||
?.installed,
|
||||
);
|
||||
|
||||
const show_restart = !setup_status.is_dev && setup_status.init_system !== "unknown";
|
||||
|
||||
return (
|
||||
@@ -43,6 +49,11 @@ export default function SetupActionsSection({ setup }: Props) {
|
||||
}}
|
||||
/>
|
||||
|
||||
<SetupCaddyAction
|
||||
installed={have_caddy}
|
||||
on_done={() => setup.refresh()}
|
||||
/>
|
||||
|
||||
<SetupUpdateWgUiAction setup={setup} />
|
||||
|
||||
{show_restart ? (
|
||||
|
||||
@@ -12,7 +12,7 @@ type Props = PropsWithChildren & {
|
||||
is_root?: boolean;
|
||||
};
|
||||
|
||||
/** Header, sub-navigation and body shared by the three setup subpages. */
|
||||
/** Header, sub-navigation and body shared by the setup subpages. */
|
||||
export default function SetupPageFrame({ title, description, is_root, children }: Props) {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import Panel from "@/src/components/general/panel";
|
||||
import type useTlsStatus from "../../(hooks)/use-tls-status";
|
||||
import SetupCaddyAction from "../../(partials)/setup-caddy-action";
|
||||
|
||||
type Props = {
|
||||
tls: ReturnType<typeof useTlsStatus>;
|
||||
};
|
||||
|
||||
export default function TlsInstallSection({ tls }: Props) {
|
||||
return (
|
||||
<Panel flush>
|
||||
<SetupCaddyAction
|
||||
installed={tls.tls_status?.caddy.installed}
|
||||
on_done={tls.refresh}
|
||||
/>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import Button from "@/src/components/twui/layout/Button";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import P from "@/src/components/twui/layout/P";
|
||||
import Panel from "@/src/components/general/panel";
|
||||
import InlineAlert from "@/src/components/general/inline-alert";
|
||||
import MonoText from "@/src/components/general/mono-text";
|
||||
import type useTlsStatus from "../../(hooks)/use-tls-status";
|
||||
|
||||
type Props = {
|
||||
tls: ReturnType<typeof useTlsStatus>;
|
||||
};
|
||||
|
||||
export default function TlsSetupSection({ tls }: Props) {
|
||||
const caddy = tls.tls_status?.caddy;
|
||||
|
||||
return (
|
||||
<Panel
|
||||
title="Route certificates"
|
||||
description="Certificates are issued per route with manual DNS-01. No DNS provider token is stored."
|
||||
actions={
|
||||
<Button
|
||||
title="Open routes"
|
||||
href="/admin/routes"
|
||||
size="small"
|
||||
afterIcon={<ArrowRight size={14} />}
|
||||
>
|
||||
Open routes
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Stack className="gap-4 items-stretch">
|
||||
{!caddy ? (
|
||||
<InlineAlert tone="info" msg="Loading TLS ingress status…" />
|
||||
) : !caddy.installed ? (
|
||||
<InlineAlert
|
||||
tone="warning"
|
||||
msg="Caddy is not installed. Use Install Caddy above to install the ingress."
|
||||
/>
|
||||
) : null}
|
||||
<P className="text-[15px]!">
|
||||
Open a route to issue or renew its certificate. The dashboard
|
||||
displays the exact <MonoText>_acme-challenge</MonoText> TXT record;
|
||||
add it at Cloudflare, wait for propagation, then click
|
||||
Validate.
|
||||
</P>
|
||||
<P className="text-[15px]!">
|
||||
Certificates are renewed manually from the route page because
|
||||
DNS validation needs your registrar. The route list shows
|
||||
expiry warnings.
|
||||
</P>
|
||||
<P className="text-[15px]!">
|
||||
The Caddy ingress listens on port {caddy?.https_port || 8443}.
|
||||
It never binds ports 80 or 443.
|
||||
</P>
|
||||
</Stack>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import Button from "@/src/components/twui/layout/Button";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import Panel from "@/src/components/general/panel";
|
||||
import StatusDot from "@/src/components/general/status-dot";
|
||||
import InlineAlert from "@/src/components/general/inline-alert";
|
||||
import KeyValueList from "@/src/components/general/key-value-list";
|
||||
import type useTlsStatus from "../../(hooks)/use-tls-status";
|
||||
|
||||
type Props = {
|
||||
tls: ReturnType<typeof useTlsStatus>;
|
||||
};
|
||||
|
||||
export default function TlsStatusSection({ tls }: Props) {
|
||||
const { tls_status, refresh, sync, loading, error } = tls;
|
||||
const caddy = tls_status?.caddy;
|
||||
|
||||
return (
|
||||
<Panel
|
||||
title="Ingress"
|
||||
description="Caddy runs as its own service and terminates TLS on the configured high port."
|
||||
actions={
|
||||
<>
|
||||
<StatusDot
|
||||
tone={
|
||||
!caddy?.installed
|
||||
? "error"
|
||||
: caddy.service_active === false
|
||||
? "warning"
|
||||
: caddy.state?.last_error
|
||||
? "warning"
|
||||
: "success"
|
||||
}
|
||||
label={
|
||||
!caddy?.installed
|
||||
? "Caddy not installed"
|
||||
: caddy.service_active === false
|
||||
? "wgui-caddy stopped"
|
||||
: caddy.version || "Caddy installed"
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
title="Sync Caddy config"
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
size="small"
|
||||
loading={loading}
|
||||
onClick={() => void sync()}
|
||||
beforeIcon={<RefreshCw size={14} />}
|
||||
>
|
||||
Sync
|
||||
</Button>
|
||||
<Button
|
||||
title="Refresh TLS status"
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
size="small"
|
||||
loading={loading}
|
||||
onClick={refresh}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Stack className="gap-4 items-stretch">
|
||||
{error ? <InlineAlert tone="error" msg={error} /> : null}
|
||||
{!caddy?.installed ? (
|
||||
<InlineAlert
|
||||
tone="warning"
|
||||
msg="Caddy is not installed yet. Run the installer/update script, or install it to /opt/caddy/caddy."
|
||||
/>
|
||||
) : null}
|
||||
{caddy?.state?.last_error ? (
|
||||
<InlineAlert
|
||||
tone="error"
|
||||
msg={`Last reload: ${caddy.state.last_error}`}
|
||||
/>
|
||||
) : null}
|
||||
{caddy?.state?.warnings?.length ? (
|
||||
<InlineAlert
|
||||
tone="warning"
|
||||
msg={caddy.state.warnings.join("; ")}
|
||||
/>
|
||||
) : null}
|
||||
<KeyValueList
|
||||
items={[
|
||||
{
|
||||
key: "ingress",
|
||||
label: "HTTPS port",
|
||||
value: caddy
|
||||
? `${caddy.https_port}/tcp`
|
||||
: "—",
|
||||
},
|
||||
{
|
||||
key: "config",
|
||||
label: "Caddyfile",
|
||||
value: caddy?.config_path || "—",
|
||||
mono: true,
|
||||
},
|
||||
{
|
||||
key: "reload",
|
||||
label: "Last reload",
|
||||
value: caddy?.state?.last_reload_at
|
||||
? new Date(
|
||||
caddy.state.last_reload_at,
|
||||
).toLocaleString()
|
||||
: "Never",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Stack>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { PagePropsType } from "@/src/types";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
import guardSetupPage from "../(functions)/guard-setup-page";
|
||||
|
||||
const server: BunextPageServerFn<PagePropsType> = async ({ req }) => {
|
||||
const redirect = await guardSetupPage({ req });
|
||||
|
||||
if (redirect) {
|
||||
return { redirect };
|
||||
}
|
||||
|
||||
return { props: {} };
|
||||
};
|
||||
|
||||
export default server;
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
import useTlsStatus from "../(hooks)/use-tls-status";
|
||||
import SetupPageFrame from "../(sections)/setup-page-frame";
|
||||
import TlsStatusSection from "./(sections)/tls-status-section";
|
||||
import TlsInstallSection from "./(sections)/tls-install-section";
|
||||
import TlsSetupSection from "./(sections)/tls-setup-section";
|
||||
|
||||
export default function SetupTlsPage() {
|
||||
const tls = useTlsStatus();
|
||||
|
||||
return (
|
||||
<SetupPageFrame
|
||||
title="TLS ingress"
|
||||
description="Serve configured client routes over HTTPS without touching the host's ports 80 or 443."
|
||||
>
|
||||
<TlsStatusSection tls={tls} />
|
||||
<TlsInstallSection tls={tls} />
|
||||
<TlsSetupSection tls={tls} />
|
||||
</SetupPageFrame>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `TLS ingress | ${SiteData["SiteName"]}`,
|
||||
description: `Caddy ingress and client-route certificates`,
|
||||
};
|
||||
Reference in New Issue
Block a user