Update Reverse proxy and TLS configuration on Caddy
This commit is contained in:
@@ -351,6 +351,10 @@ const schema: BUN_SQLITE_DatabaseSchemaType = {
|
|||||||
fieldName: "acme_credentials",
|
fieldName: "acme_credentials",
|
||||||
dataType: "TEXT",
|
dataType: "TEXT",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldName: "allowed_ips",
|
||||||
|
dataType: "TEXT",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
uniqueConstraints: [
|
uniqueConstraints: [
|
||||||
{
|
{
|
||||||
|
|||||||
+2
-1
@@ -191,6 +191,7 @@ export type BUN_SQLITE_WGUI_CLIENT_ROUTES = {
|
|||||||
acme_dns_provider?: string;
|
acme_dns_provider?: string;
|
||||||
acme_staging?: 0 | 1 | "";
|
acme_staging?: 0 | 1 | "";
|
||||||
acme_credentials?: string;
|
acme_credentials?: string;
|
||||||
|
allowed_ips?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BUN_SQLITE_WGUI_TLS_CERTIFICATES = {
|
export type BUN_SQLITE_WGUI_TLS_CERTIFICATES = {
|
||||||
@@ -232,7 +233,7 @@ export type BUN_SQLITE_WGUI_VARIABLES = {
|
|||||||
* The time when the record was updated. (Unix Timestamp)
|
* The time when the record was updated. (Unix Timestamp)
|
||||||
*/
|
*/
|
||||||
updated_at?: number | "";
|
updated_at?: number | "";
|
||||||
key?: "test_var" | "";
|
key?: "test_var" | "caddy_https_port" | "caddy_https_redirect" | "";
|
||||||
value?: string;
|
value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import FormSection from "../form-section";
|
|||||||
import FormActions from "../form-actions";
|
import FormActions from "../form-actions";
|
||||||
import InlineAlert from "../inline-alert";
|
import InlineAlert from "../inline-alert";
|
||||||
import submitClientRouteForm from "./submit-client-route-form";
|
import submitClientRouteForm from "./submit-client-route-form";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { AppContext } from "@/src/pages/__root";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
host_id: number;
|
host_id: number;
|
||||||
@@ -25,6 +27,7 @@ export default function ClientRouteForm({
|
|||||||
cancel_href,
|
cancel_href,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { confirm } = useConfirm();
|
const { confirm } = useConfirm();
|
||||||
|
const { user } = useContext(AppContext);
|
||||||
|
|
||||||
const init = useFormInit<ClientRouteFormObject>({
|
const init = useFormInit<ClientRouteFormObject>({
|
||||||
existing: existing_route,
|
existing: existing_route,
|
||||||
@@ -33,6 +36,14 @@ export default function ClientRouteForm({
|
|||||||
? `Edit route v2 ${existing_route.id}`
|
? `Edit route v2 ${existing_route.id}`
|
||||||
: `Add client route v2 ${host_id}`,
|
: `Add client route v2 ${host_id}`,
|
||||||
local_storage_exclude_keys: ["acme_credentials"],
|
local_storage_exclude_keys: ["acme_credentials"],
|
||||||
|
async before_ready_function(params) {
|
||||||
|
if (!params.form.acme_email) {
|
||||||
|
params.setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
acme_email: user?.email,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { form, setForm, loading, status, ready } = init;
|
const { form, setForm, loading, status, ready } = init;
|
||||||
@@ -56,7 +67,11 @@ export default function ClientRouteForm({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{status?.error ? (
|
{status?.error ? (
|
||||||
<InlineAlert tone="error" msg={status.msg} className="mb-6" />
|
<InlineAlert
|
||||||
|
tone="error"
|
||||||
|
msg={status.msg}
|
||||||
|
className="mb-6"
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<FormSection
|
<FormSection
|
||||||
@@ -161,6 +176,26 @@ export default function ClientRouteForm({
|
|||||||
/>
|
/>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
|
<FormSection
|
||||||
|
title="Access"
|
||||||
|
description="Additional IP addresses or CIDR ranges allowed to reach this route through Caddy. The host's WireGuard subnet and loopback are always included."
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name="allowed_ips"
|
||||||
|
title="Allowed IPs"
|
||||||
|
placeholder="203.0.113.0/24, 198.51.100.0/24"
|
||||||
|
defaultValue={form.allowed_ips}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
allowed_ips: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
showLabel
|
||||||
|
info="Comma-separated IPv4 CIDR ranges. These are appended to the Caddy remote_ip matcher for this route."
|
||||||
|
/>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
<FormSection
|
<FormSection
|
||||||
title="TLS certificate"
|
title="TLS certificate"
|
||||||
description="Manual DNS-01. After saving, the route page shows the TXT record to add at your registrar."
|
description="Manual DNS-01. After saving, the route page shows the TXT record to add at your registrar."
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export default async function submitClientRouteForm({
|
|||||||
upstream_tls_verify: form.upstream_tls_verify == 0 ? 0 : 1,
|
upstream_tls_verify: form.upstream_tls_verify == 0 ? 0 : 1,
|
||||||
enabled: form.enabled == 0 ? 0 : 1,
|
enabled: form.enabled == 0 ? 0 : 1,
|
||||||
notes: form.notes || "",
|
notes: form.notes || "",
|
||||||
|
allowed_ips: form.allowed_ips || "",
|
||||||
acme_email: form.acme_email || "",
|
acme_email: form.acme_email || "",
|
||||||
acme_staging: form.acme_staging == 1 ? 1 : 0,
|
acme_staging: form.acme_staging == 1 ? 1 : 0,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,6 +53,16 @@ export default function buildClientRoutesTableColumns({
|
|||||||
header: "Path",
|
header: "Path",
|
||||||
render: ({ item }) => <MonoText>{item.path_prefix || "/"}</MonoText>,
|
render: ({ item }) => <MonoText>{item.path_prefix || "/"}</MonoText>,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "public_url",
|
||||||
|
header: "Public URL",
|
||||||
|
hide_below: "lg",
|
||||||
|
render: ({ item }) => (
|
||||||
|
<MonoText className="text-muted">
|
||||||
|
{item.public_url || "—"}
|
||||||
|
</MonoText>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "upstream",
|
key: "upstream",
|
||||||
header: "Upstream",
|
header: "Upstream",
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ export const Variables = [
|
|||||||
value: "test_var",
|
value: "test_var",
|
||||||
description: `Just a test variable to define the schema`,
|
description: `Just a test variable to define the schema`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: `Caddy HTTPS Port`,
|
||||||
|
value: "caddy_https_port",
|
||||||
|
description: `Public HTTPS port Caddy listens on. Defaults to 8443.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: `Caddy HTTPS Redirect`,
|
||||||
|
value: "caddy_https_redirect",
|
||||||
|
description: `Redirect HTTP requests to HTTPS. Set to 0 to disable. Defaults to 1.`,
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// title: `Main Host Wireguard IP Address`,
|
// title: `Main Host Wireguard IP Address`,
|
||||||
// value: "main_host_wg_ip_address",
|
// value: "main_host_wg_ip_address",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type {
|
|||||||
BUN_SQLITE_WGUI_CLIENT_ROUTES,
|
BUN_SQLITE_WGUI_CLIENT_ROUTES,
|
||||||
BUN_SQLITE_WGUI_TLS_CERTIFICATES,
|
BUN_SQLITE_WGUI_TLS_CERTIFICATES,
|
||||||
} from "@/db/types/db";
|
} from "@/db/types/db";
|
||||||
|
import { TlsData } from "@/src/data/tls-data";
|
||||||
import readCertificateExpiry from "./read-certificate-expiry";
|
import readCertificateExpiry from "./read-certificate-expiry";
|
||||||
|
|
||||||
export type ClientRouteView = BUN_SQLITE_WGUI_CLIENT_ROUTES & {
|
export type ClientRouteView = BUN_SQLITE_WGUI_CLIENT_ROUTES & {
|
||||||
@@ -9,16 +10,40 @@ export type ClientRouteView = BUN_SQLITE_WGUI_CLIENT_ROUTES & {
|
|||||||
acme_credentials_configured?: boolean;
|
acme_credentials_configured?: boolean;
|
||||||
certificate_status?: string;
|
certificate_status?: string;
|
||||||
certificate_expires_at?: number | null;
|
certificate_expires_at?: number | null;
|
||||||
|
public_url?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
route: BUN_SQLITE_WGUI_CLIENT_ROUTES;
|
route: BUN_SQLITE_WGUI_CLIENT_ROUTES;
|
||||||
certificate?: BUN_SQLITE_WGUI_TLS_CERTIFICATES | null;
|
certificate?: BUN_SQLITE_WGUI_TLS_CERTIFICATES | null;
|
||||||
|
https_port?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function buildPublicUrl({
|
||||||
|
route,
|
||||||
|
https_port,
|
||||||
|
}: {
|
||||||
|
route: BUN_SQLITE_WGUI_CLIENT_ROUTES;
|
||||||
|
https_port?: number;
|
||||||
|
}) {
|
||||||
|
const hostname = route.hostname?.trim();
|
||||||
|
|
||||||
|
if (!hostname) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const port = https_port || TlsData.CaddyHttpsPort;
|
||||||
|
const port_part = port == 443 ? "" : `:${port}`;
|
||||||
|
const path_prefix = route.path_prefix?.trim() || "/";
|
||||||
|
const path = path_prefix == "/" ? "/" : path_prefix;
|
||||||
|
|
||||||
|
return `https://${hostname}${port_part}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
export default function decorateClientRoute({
|
export default function decorateClientRoute({
|
||||||
route,
|
route,
|
||||||
certificate,
|
certificate,
|
||||||
|
https_port,
|
||||||
}: Params): ClientRouteView {
|
}: Params): ClientRouteView {
|
||||||
const { acme_credentials, ...safe_route } = route;
|
const { acme_credentials, ...safe_route } = route;
|
||||||
const expires_at = readCertificateExpiry({
|
const expires_at = readCertificateExpiry({
|
||||||
@@ -45,5 +70,6 @@ export default function decorateClientRoute({
|
|||||||
acme_credentials_configured: Boolean(acme_credentials),
|
acme_credentials_configured: Boolean(acme_credentials),
|
||||||
certificate_status,
|
certificate_status,
|
||||||
certificate_expires_at: expires_at || null,
|
certificate_expires_at: expires_at || null,
|
||||||
|
public_url: buildPublicUrl({ route, https_port }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import BunSQLite from "@moduletrace/bun-sqlite";
|
|||||||
import type { TableType } from "@/src/types";
|
import type { TableType } from "@/src/types";
|
||||||
import { TlsData } from "@/src/data/tls-data";
|
import { TlsData } from "@/src/data/tls-data";
|
||||||
import grabCaddyStatus from "../caddy/grab-caddy-status";
|
import grabCaddyStatus from "../caddy/grab-caddy-status";
|
||||||
|
import grabCaddySettings from "../caddy/grab-caddy-settings";
|
||||||
import grabCertDirNames from "../caddy/grab-cert-dir-names";
|
import grabCertDirNames from "../caddy/grab-cert-dir-names";
|
||||||
import readAcmeEnv from "./read-acme-env";
|
import readAcmeEnv from "./read-acme-env";
|
||||||
import readCertificateExpiry from "./read-certificate-expiry";
|
import readCertificateExpiry from "./read-certificate-expiry";
|
||||||
@@ -95,8 +96,10 @@ export default async function grabTlsStatus() {
|
|||||||
(a.hostname || "").localeCompare(b.hostname || ""),
|
(a.hostname || "").localeCompare(b.hostname || ""),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const settings = await grabCaddySettings();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
caddy: grabCaddyStatus(),
|
caddy: grabCaddyStatus(settings),
|
||||||
certificates,
|
certificates,
|
||||||
acme: {
|
acme: {
|
||||||
env_path: acme_env.exists ? `/etc/wgui/acme.env` : null,
|
env_path: acme_env.exists ? `/etc/wgui/acme.env` : null,
|
||||||
@@ -112,6 +115,8 @@ export default async function grabTlsStatus() {
|
|||||||
),
|
),
|
||||||
staging: TlsData.AcmeStaging,
|
staging: TlsData.AcmeStaging,
|
||||||
},
|
},
|
||||||
https_port: TlsData.CaddyHttpsPort,
|
https_port: settings.https_port,
|
||||||
|
http_port: settings.http_port,
|
||||||
|
redirect: settings.redirect,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,11 +37,22 @@ describe("buildCaddyfile", () => {
|
|||||||
|
|
||||||
expect(res.config).toContain("https_port 8443");
|
expect(res.config).toContain("https_port 8443");
|
||||||
expect(res.config).toContain("http_port 8081");
|
expect(res.config).toContain("http_port 8081");
|
||||||
expect(res.config).toContain("auto_https disable_redirects");
|
expect(res.config).not.toContain("auto_https disable_redirects");
|
||||||
expect(res.config).not.toContain("https_port 443");
|
expect(res.config).not.toContain("https_port 443");
|
||||||
expect(res.config).not.toContain("http_port 80\n");
|
expect(res.config).not.toContain("http_port 80\n");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can disable automatic HTTPS redirects", () => {
|
||||||
|
const res = buildCaddyfile({
|
||||||
|
...base,
|
||||||
|
redirect: false,
|
||||||
|
routes: [],
|
||||||
|
certificates: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.config).toContain("auto_https disable_redirects");
|
||||||
|
});
|
||||||
|
|
||||||
test("orders longer prefixes before the catch-all", () => {
|
test("orders longer prefixes before the catch-all", () => {
|
||||||
const res = buildCaddyfile({
|
const res = buildCaddyfile({
|
||||||
...base,
|
...base,
|
||||||
@@ -88,6 +99,25 @@ describe("buildCaddyfile", () => {
|
|||||||
expect(res.config).toContain("remote_ip 10.5.0.0/24");
|
expect(res.config).toContain("remote_ip 10.5.0.0/24");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("appends route allowed_ips to the remote_ip matcher", () => {
|
||||||
|
const res = buildCaddyfile({
|
||||||
|
...base,
|
||||||
|
certificates: [certificate],
|
||||||
|
hosts: [{ id: 0, wg_ip_address: "10.5.0.1" }],
|
||||||
|
routes: [
|
||||||
|
route({
|
||||||
|
host_id: 0,
|
||||||
|
path_prefix: "/app",
|
||||||
|
allowed_ips: "203.0.113.0/24, 198.51.100.0/24",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.config).toContain(
|
||||||
|
"remote_ip 10.5.0.0/24 127.0.0.1/32 ::1/128 203.0.113.0/24 198.51.100.0/24",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("maps insecure upstream TLS to Caddy", () => {
|
test("maps insecure upstream TLS to Caddy", () => {
|
||||||
const res = buildCaddyfile({
|
const res = buildCaddyfile({
|
||||||
...base,
|
...base,
|
||||||
@@ -102,4 +132,22 @@ describe("buildCaddyfile", () => {
|
|||||||
|
|
||||||
expect(res.config).toContain("tls_insecure_skip_verify");
|
expect(res.config).toContain("tls_insecure_skip_verify");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("allows a catch-all route with allowed_ips to have a matcher", () => {
|
||||||
|
const res = buildCaddyfile({
|
||||||
|
...base,
|
||||||
|
certificates: [certificate],
|
||||||
|
hosts: [{ id: 0, wg_ip_address: "10.5.0.1" }],
|
||||||
|
routes: [
|
||||||
|
route({
|
||||||
|
host_id: 0,
|
||||||
|
path_prefix: "/",
|
||||||
|
allowed_ips: "203.0.113.0/24",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.config).toContain("@route_1 remote_ip");
|
||||||
|
expect(res.config).toContain("203.0.113.0/24");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ type Params = {
|
|||||||
hosts?: BUN_SQLITE_WGUI_HOSTS[];
|
hosts?: BUN_SQLITE_WGUI_HOSTS[];
|
||||||
https_port: number;
|
https_port: number;
|
||||||
http_port: number;
|
http_port: number;
|
||||||
|
/** Redirect plain HTTP requests to HTTPS. Enabled by default. */
|
||||||
|
redirect?: boolean;
|
||||||
admin_socket: string;
|
admin_socket: string;
|
||||||
storage_dir: string;
|
storage_dir: string;
|
||||||
};
|
};
|
||||||
@@ -47,6 +49,33 @@ function reverseProxyBlock({
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildRemoteIpValue({
|
||||||
|
subnet,
|
||||||
|
route,
|
||||||
|
}: {
|
||||||
|
subnet?: string;
|
||||||
|
route: BUN_SQLITE_WGUI_CLIENT_ROUTES;
|
||||||
|
}) {
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (subnet) {
|
||||||
|
parts.push(subnet as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.push("127.0.0.1/32", "::1/128");
|
||||||
|
|
||||||
|
const allowed = (route.allowed_ips || "")
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s.length > 0);
|
||||||
|
|
||||||
|
for (const ip of allowed) {
|
||||||
|
parts.push(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
function routeBlock({
|
function routeBlock({
|
||||||
route,
|
route,
|
||||||
index,
|
index,
|
||||||
@@ -59,8 +88,9 @@ function routeBlock({
|
|||||||
const path_prefix = route.path_prefix || "/";
|
const path_prefix = route.path_prefix || "/";
|
||||||
const matcher = `route_${index}`;
|
const matcher = `route_${index}`;
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
|
const remote_ip_value = buildRemoteIpValue({ subnet, route });
|
||||||
|
|
||||||
if (path_prefix == "/" && !subnet) {
|
if (path_prefix == "/" && !subnet && !route.allowed_ips) {
|
||||||
lines.push(` handle {`);
|
lines.push(` handle {`);
|
||||||
lines.push(...reverseProxyBlock({ route, indent: " " }));
|
lines.push(...reverseProxyBlock({ route, indent: " " }));
|
||||||
lines.push(` }`);
|
lines.push(` }`);
|
||||||
@@ -68,17 +98,13 @@ function routeBlock({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (path_prefix == "/") {
|
if (path_prefix == "/") {
|
||||||
lines.push(
|
lines.push(` @${matcher} remote_ip ${remote_ip_value}`);
|
||||||
` @${matcher} remote_ip ${subnet} 127.0.0.1/32 ::1/128`,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
lines.push(` @${matcher} {`);
|
lines.push(` @${matcher} {`);
|
||||||
lines.push(` path ${path_prefix} ${path_prefix}/*`);
|
lines.push(` path ${path_prefix} ${path_prefix}/*`);
|
||||||
|
|
||||||
if (subnet) {
|
if (remote_ip_value.trim()) {
|
||||||
lines.push(
|
lines.push(` remote_ip ${remote_ip_value}`);
|
||||||
` remote_ip ${subnet} 127.0.0.1/32 ::1/128`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push(` }`);
|
lines.push(` }`);
|
||||||
@@ -107,6 +133,7 @@ export default function buildCaddyfile({
|
|||||||
hosts,
|
hosts,
|
||||||
https_port,
|
https_port,
|
||||||
http_port,
|
http_port,
|
||||||
|
redirect = true,
|
||||||
admin_socket,
|
admin_socket,
|
||||||
storage_dir,
|
storage_dir,
|
||||||
}: Params): CaddyBuildResult {
|
}: Params): CaddyBuildResult {
|
||||||
@@ -117,16 +144,25 @@ export default function buildCaddyfile({
|
|||||||
for (let i = 0; i < (hosts || []).length; i++) {
|
for (let i = 0; i < (hosts || []).length; i++) {
|
||||||
const host = hosts?.[i];
|
const host = hosts?.[i];
|
||||||
const parts = (host?.wg_ip_address || "").trim().split(".");
|
const parts = (host?.wg_ip_address || "").trim().split(".");
|
||||||
|
const matchers: string[] = [];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
host?.id !== undefined &&
|
|
||||||
parts.length == 4 &&
|
parts.length == 4 &&
|
||||||
parts.every((part) => /^\d{1,3}$/.test(part))
|
parts.every((part) => /^\d{1,3}$/.test(part))
|
||||||
) {
|
) {
|
||||||
subnet_by_host_id.set(
|
matchers.push(`${parts.slice(0, 3).join(".")}.0/24`);
|
||||||
Number(host.id),
|
}
|
||||||
`${parts.slice(0, 3).join(".")}.0/24`,
|
|
||||||
);
|
// Requests made to the host's own WireGuard address are masqueraded to
|
||||||
|
// its LAN/public interface, so Caddy sees that address as the source.
|
||||||
|
// Include it explicitly or those requests fall through to an empty 200.
|
||||||
|
const public_ip = (host?.public_ip_address || "").trim();
|
||||||
|
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(public_ip)) {
|
||||||
|
matchers.push(`${public_ip}/32`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host?.id !== undefined && matchers.length) {
|
||||||
|
subnet_by_host_id.set(Number(host.id), matchers.join(" "));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const certificates_by_hostname = new Map<
|
const certificates_by_hostname = new Map<
|
||||||
@@ -142,8 +178,7 @@ export default function buildCaddyfile({
|
|||||||
hostname &&
|
hostname &&
|
||||||
certificate?.cert_path &&
|
certificate?.cert_path &&
|
||||||
certificate.key_path &&
|
certificate.key_path &&
|
||||||
(certificate.status == "active" ||
|
(certificate.status == "active" || certificate.status == "expiring")
|
||||||
certificate.status == "expiring")
|
|
||||||
) {
|
) {
|
||||||
certificates_by_hostname.set(hostname, {
|
certificates_by_hostname.set(hostname, {
|
||||||
...certificate,
|
...certificate,
|
||||||
@@ -215,7 +250,7 @@ export default function buildCaddyfile({
|
|||||||
`{`,
|
`{`,
|
||||||
` https_port ${https_port}`,
|
` https_port ${https_port}`,
|
||||||
` http_port ${http_port}`,
|
` http_port ${http_port}`,
|
||||||
` auto_https disable_redirects`,
|
...(redirect ? [] : [` auto_https disable_redirects`]),
|
||||||
` admin unix/${admin_socket}`,
|
` admin unix/${admin_socket}`,
|
||||||
` storage file_system ${storage_dir}`,
|
` storage file_system ${storage_dir}`,
|
||||||
`}`,
|
`}`,
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import type { BUN_SQLITE_WGUI_VARIABLES } from "@/db/types/db";
|
||||||
|
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||||
|
import type { TableType } from "@/src/types";
|
||||||
|
import { TlsData } from "@/src/data/tls-data";
|
||||||
|
|
||||||
|
export type CaddySettings = {
|
||||||
|
https_port: number;
|
||||||
|
http_port: number;
|
||||||
|
redirect: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseHttpsPort(value: unknown, fallback: number) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Number.isInteger(parsed) ||
|
||||||
|
parsed < 1 ||
|
||||||
|
parsed > 65535 ||
|
||||||
|
parsed == 80 ||
|
||||||
|
parsed == 443
|
||||||
|
) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caddy settings are stored in the `variables` table so the port and redirect
|
||||||
|
* behaviour can be changed from the UI without restarting the app.
|
||||||
|
*/
|
||||||
|
export default async function grabCaddySettings(): Promise<CaddySettings> {
|
||||||
|
const res = await BunSQLite.select<BUN_SQLITE_WGUI_VARIABLES, TableType>({
|
||||||
|
table: "variables",
|
||||||
|
});
|
||||||
|
|
||||||
|
const values = new Map<string, string>();
|
||||||
|
|
||||||
|
for (const variable of res.payload || []) {
|
||||||
|
if (variable?.key) {
|
||||||
|
values.set(variable.key, variable.value || "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
https_port: parseHttpsPort(
|
||||||
|
values.get("caddy_https_port"),
|
||||||
|
TlsData.CaddyHttpsPort,
|
||||||
|
),
|
||||||
|
http_port: TlsData.CaddyHttpPort,
|
||||||
|
redirect: values.get("caddy_https_redirect") !== "0",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -76,7 +76,12 @@ function serviceActive() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function grabCaddyStatus() {
|
type Settings = {
|
||||||
|
https_port?: number;
|
||||||
|
http_port?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function grabCaddyStatus(settings?: Settings) {
|
||||||
const state = readState();
|
const state = readState();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -85,8 +90,8 @@ export default function grabCaddyStatus() {
|
|||||||
acme_installed: existsSync(WGUI_ACME_SCRIPT),
|
acme_installed: existsSync(WGUI_ACME_SCRIPT),
|
||||||
acme_env_exists: existsSync(WGUI_ACME_ENV_FILE),
|
acme_env_exists: existsSync(WGUI_ACME_ENV_FILE),
|
||||||
version: version(),
|
version: version(),
|
||||||
https_port: TlsData.CaddyHttpsPort,
|
https_port: settings?.https_port || TlsData.CaddyHttpsPort,
|
||||||
http_port: TlsData.CaddyHttpPort,
|
http_port: settings?.http_port || TlsData.CaddyHttpPort,
|
||||||
enabled: TlsData.IngressEnabled,
|
enabled: TlsData.IngressEnabled,
|
||||||
config_path: WGUI_LIB_CADDY_CONFIG_FILE,
|
config_path: WGUI_LIB_CADDY_CONFIG_FILE,
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ export default function sanitizeClientRoute({
|
|||||||
typeof route.acme_credentials == "string"
|
typeof route.acme_credentials == "string"
|
||||||
? route.acme_credentials
|
? route.acme_credentials
|
||||||
: "",
|
: "",
|
||||||
|
allowed_ips:
|
||||||
|
typeof route.allowed_ips == "string"
|
||||||
|
? route.allowed_ips.trim()
|
||||||
|
: "",
|
||||||
} satisfies BUN_SQLITE_WGUI_CLIENT_ROUTES,
|
} satisfies BUN_SQLITE_WGUI_CLIENT_ROUTES,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { TlsData } from "@/src/data/tls-data";
|
|||||||
import buildCaddyfile from "./build-caddyfile";
|
import buildCaddyfile from "./build-caddyfile";
|
||||||
import reloadCaddyService from "./reload-caddy-service";
|
import reloadCaddyService from "./reload-caddy-service";
|
||||||
import applyCaddyIngressFirewallRule from "./apply-caddy-ingress-firewall-rule";
|
import applyCaddyIngressFirewallRule from "./apply-caddy-ingress-firewall-rule";
|
||||||
|
import grabCaddySettings from "./grab-caddy-settings";
|
||||||
|
|
||||||
export type CaddySyncResult = {
|
export type CaddySyncResult = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -120,12 +121,15 @@ async function doSync({ reason }: Params): Promise<CaddySyncResult> {
|
|||||||
: ("active" as const),
|
: ("active" as const),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const settings = await grabCaddySettings();
|
||||||
|
|
||||||
const built = buildCaddyfile({
|
const built = buildCaddyfile({
|
||||||
routes,
|
routes,
|
||||||
certificates,
|
certificates,
|
||||||
hosts,
|
hosts,
|
||||||
https_port: TlsData.CaddyHttpsPort,
|
https_port: settings.https_port,
|
||||||
http_port: TlsData.CaddyHttpPort,
|
http_port: settings.http_port,
|
||||||
|
redirect: settings.redirect,
|
||||||
admin_socket: WGUI_LIB_CADDY_ADMIN_SOCKET,
|
admin_socket: WGUI_LIB_CADDY_ADMIN_SOCKET,
|
||||||
storage_dir: WGUI_LIB_CADDY_DATA_DIR,
|
storage_dir: WGUI_LIB_CADDY_DATA_DIR,
|
||||||
});
|
});
|
||||||
@@ -218,7 +222,10 @@ async function doSync({ reason }: Params): Promise<CaddySyncResult> {
|
|||||||
|
|
||||||
if (typeof host_id != "number") continue;
|
if (typeof host_id != "number") continue;
|
||||||
|
|
||||||
const firewall = applyCaddyIngressFirewallRule({ host_id });
|
const firewall = applyCaddyIngressFirewallRule({
|
||||||
|
host_id,
|
||||||
|
port: settings.https_port,
|
||||||
|
});
|
||||||
|
|
||||||
if (!firewall.success && firewall.msg) {
|
if (!firewall.success && firewall.msg) {
|
||||||
warnings.push(firewall.msg);
|
warnings.push(firewall.msg);
|
||||||
|
|||||||
+13
@@ -45,6 +45,13 @@ export default function RouteOverviewSection({ route, host }: Props) {
|
|||||||
mono: true,
|
mono: true,
|
||||||
copy_value: route.hostname || "",
|
copy_value: route.hostname || "",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "public_url",
|
||||||
|
label: "Public URL",
|
||||||
|
value: route.public_url || "—",
|
||||||
|
mono: true,
|
||||||
|
copy_value: route.public_url || "",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "path",
|
key: "path",
|
||||||
label: "Path prefix",
|
label: "Path prefix",
|
||||||
@@ -89,6 +96,12 @@ export default function RouteOverviewSection({ route, host }: Props) {
|
|||||||
label: "Staging CA",
|
label: "Staging CA",
|
||||||
value: route.acme_staging == 1 ? "Yes" : "No",
|
value: route.acme_staging == 1 ? "Yes" : "No",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "allowed_ips",
|
||||||
|
label: "Allowed IPs",
|
||||||
|
value: route.allowed_ips || "All from host subnet",
|
||||||
|
mono: true,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
BUN_SQLITE_WGUI_HOSTS,
|
BUN_SQLITE_WGUI_HOSTS,
|
||||||
} from "@/db/types/db";
|
} from "@/db/types/db";
|
||||||
import decorateClientRoute from "@/src/functions/backend/acme/decorate-client-route";
|
import decorateClientRoute from "@/src/functions/backend/acme/decorate-client-route";
|
||||||
|
import grabCaddySettings from "@/src/functions/backend/caddy/grab-caddy-settings";
|
||||||
import grabCertificateByHostname from "@/src/functions/backend/acme/grab-certificate-by-hostname";
|
import grabCertificateByHostname from "@/src/functions/backend/acme/grab-certificate-by-hostname";
|
||||||
import { grabManualDnsChallenge } from "@/src/functions/backend/acme/manual-dns-challenges";
|
import { grabManualDnsChallenge } from "@/src/functions/backend/acme/manual-dns-challenges";
|
||||||
import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
|
import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
|
||||||
@@ -31,6 +32,7 @@ const server: BunextPageServerFn<PagePropsType> = async ({ query }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const route = route_res.singleRes;
|
const route = route_res.singleRes;
|
||||||
|
const settings = await grabCaddySettings();
|
||||||
const certificate = route
|
const certificate = route
|
||||||
? await grabCertificateByHostname({ hostname: route.hostname })
|
? await grabCertificateByHostname({ hostname: route.hostname })
|
||||||
: null;
|
: null;
|
||||||
@@ -42,7 +44,11 @@ const server: BunextPageServerFn<PagePropsType> = async ({ query }) => {
|
|||||||
props: {
|
props: {
|
||||||
host: host_res.singleRes || null,
|
host: host_res.singleRes || null,
|
||||||
client_route: route
|
client_route: route
|
||||||
? decorateClientRoute({ route, certificate })
|
? decorateClientRoute({
|
||||||
|
route,
|
||||||
|
certificate,
|
||||||
|
https_port: settings.https_port,
|
||||||
|
})
|
||||||
: null,
|
: null,
|
||||||
client_certificate: certificate,
|
client_certificate: certificate,
|
||||||
client_challenge: pending
|
client_challenge: pending
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
BUN_SQLITE_WGUI_HOSTS,
|
BUN_SQLITE_WGUI_HOSTS,
|
||||||
} from "@/db/types/db";
|
} from "@/db/types/db";
|
||||||
import decorateClientRoute from "@/src/functions/backend/acme/decorate-client-route";
|
import decorateClientRoute from "@/src/functions/backend/acme/decorate-client-route";
|
||||||
|
import grabCaddySettings from "@/src/functions/backend/caddy/grab-caddy-settings";
|
||||||
import grabCertificateByHostname from "@/src/functions/backend/acme/grab-certificate-by-hostname";
|
import grabCertificateByHostname from "@/src/functions/backend/acme/grab-certificate-by-hostname";
|
||||||
import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
|
import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
|
||||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||||
@@ -30,6 +31,7 @@ const server: BunextPageServerFn<PagePropsType> = async ({ query }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const route = route_res.singleRes;
|
const route = route_res.singleRes;
|
||||||
|
const settings = await grabCaddySettings();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
@@ -40,6 +42,7 @@ const server: BunextPageServerFn<PagePropsType> = async ({ query }) => {
|
|||||||
certificate: await grabCertificateByHostname({
|
certificate: await grabCertificateByHostname({
|
||||||
hostname: route.hostname,
|
hostname: route.hostname,
|
||||||
}),
|
}),
|
||||||
|
https_port: settings.https_port,
|
||||||
})
|
})
|
||||||
: null,
|
: null,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||||
|
import Panel from "@/src/components/general/panel";
|
||||||
|
import Form from "@/src/components/twui/form/Form";
|
||||||
|
import Input from "@/src/components/twui/form/Input";
|
||||||
|
import Checkbox from "@/src/components/twui/form/Checkbox";
|
||||||
|
import FormActions from "@/src/components/general/form-actions";
|
||||||
|
import InlineAlert from "@/src/components/general/inline-alert";
|
||||||
|
import useToast from "@/src/hooks/use-toast";
|
||||||
|
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
|
||||||
|
import type useTlsStatus from "../../(hooks)/use-tls-status";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
tls: ReturnType<typeof useTlsStatus>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SettingsForm = {
|
||||||
|
https_port: string;
|
||||||
|
redirect: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TlsSettingsSection({ tls }: Props) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [form, setForm] = useState<SettingsForm>({
|
||||||
|
https_port: "",
|
||||||
|
redirect: true,
|
||||||
|
});
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tls.tls_status) {
|
||||||
|
setForm({
|
||||||
|
https_port: String(tls.tls_status.https_port || 8443),
|
||||||
|
redirect: tls.tls_status.redirect !== false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [tls.tls_status]);
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
setSaving(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const res = await fetchApi(`/api/admin/tls/settings`, {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
https_port: Number(form.https_port),
|
||||||
|
redirect: form.redirect,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setSaving(false);
|
||||||
|
|
||||||
|
if (res.success) {
|
||||||
|
toast({ msg: "TLS settings saved", color: "success" });
|
||||||
|
await tls.refresh();
|
||||||
|
} else {
|
||||||
|
setError(res.msg || "Could not save TLS settings");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!tls.tls_status) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel
|
||||||
|
title="Caddy settings"
|
||||||
|
description="Public HTTPS port and redirect behaviour for the Caddy ingress."
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
className="w-full gap-0"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
submit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error ? (
|
||||||
|
<InlineAlert tone="error" msg={error} className="mb-4" />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Input
|
||||||
|
name="https_port"
|
||||||
|
title="HTTPS port"
|
||||||
|
placeholder="8443"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={65535}
|
||||||
|
defaultValue={form.https_port}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
https_port: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
showLabel
|
||||||
|
required
|
||||||
|
info="Caddy listens on this port for HTTPS. Ports 80 and 443 are not allowed."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
title="Redirect HTTP to HTTPS"
|
||||||
|
label="Enable HTTP to HTTPS redirect"
|
||||||
|
checked={form.redirect}
|
||||||
|
changeHandler={(value) =>
|
||||||
|
setForm((prev) => ({ ...prev, redirect: value }))
|
||||||
|
}
|
||||||
|
info="When enabled, Caddy redirects plain HTTP requests to the HTTPS port. This is the default."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormActions
|
||||||
|
submit_title="Save settings"
|
||||||
|
loading={saving}
|
||||||
|
cancel_href={undefined}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { SiteData } from "@/src/data/site-data";
|
|||||||
import useTlsStatus from "../(hooks)/use-tls-status";
|
import useTlsStatus from "../(hooks)/use-tls-status";
|
||||||
import SetupPageFrame from "../(sections)/setup-page-frame";
|
import SetupPageFrame from "../(sections)/setup-page-frame";
|
||||||
import TlsStatusSection from "./(sections)/tls-status-section";
|
import TlsStatusSection from "./(sections)/tls-status-section";
|
||||||
|
import TlsSettingsSection from "./(sections)/tls-settings-section";
|
||||||
import TlsInstallSection from "./(sections)/tls-install-section";
|
import TlsInstallSection from "./(sections)/tls-install-section";
|
||||||
import TlsSetupSection from "./(sections)/tls-setup-section";
|
import TlsSetupSection from "./(sections)/tls-setup-section";
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ export default function SetupTlsPage() {
|
|||||||
description="Serve configured client routes over HTTPS without touching the host's ports 80 or 443."
|
description="Serve configured client routes over HTTPS without touching the host's ports 80 or 443."
|
||||||
>
|
>
|
||||||
<TlsStatusSection tls={tls} />
|
<TlsStatusSection tls={tls} />
|
||||||
|
<TlsSettingsSection tls={tls} />
|
||||||
<TlsInstallSection tls={tls} />
|
<TlsInstallSection tls={tls} />
|
||||||
<TlsSetupSection tls={tls} />
|
<TlsSetupSection tls={tls} />
|
||||||
</SetupPageFrame>
|
</SetupPageFrame>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
} from "@/db/types/db";
|
} from "@/db/types/db";
|
||||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||||
import decorateClientRoute from "@/src/functions/backend/acme/decorate-client-route";
|
import decorateClientRoute from "@/src/functions/backend/acme/decorate-client-route";
|
||||||
|
import grabCaddySettings from "@/src/functions/backend/caddy/grab-caddy-settings";
|
||||||
import { grabManualDnsChallenge } from "@/src/functions/backend/acme/manual-dns-challenges";
|
import { grabManualDnsChallenge } from "@/src/functions/backend/acme/manual-dns-challenges";
|
||||||
import type { AdminCrudAPIParams, TableType } from "@/src/types";
|
import type { AdminCrudAPIParams, TableType } from "@/src/types";
|
||||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||||
@@ -51,10 +52,13 @@ export default async function (
|
|||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const settings = await grabCaddySettings();
|
||||||
|
|
||||||
const decorate = (route: BUN_SQLITE_WGUI_CLIENT_ROUTES) => {
|
const decorate = (route: BUN_SQLITE_WGUI_CLIENT_ROUTES) => {
|
||||||
const view = decorateClientRoute({
|
const view = decorateClientRoute({
|
||||||
route,
|
route,
|
||||||
certificate: certificates_by_hostname.get(route.hostname || ""),
|
certificate: certificates_by_hostname.get(route.hostname || ""),
|
||||||
|
https_port: settings.https_port,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import adminApiGuard from "@/src/functions/backend/auth/admin-api-guard";
|
||||||
|
import grabCaddySettings from "@/src/functions/backend/caddy/grab-caddy-settings";
|
||||||
|
import syncCaddyIngress from "@/src/functions/backend/caddy/sync-caddy-ingress";
|
||||||
|
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||||
|
import type {
|
||||||
|
APIResponseObject,
|
||||||
|
BunextAPIRouteHandler,
|
||||||
|
} from "@moduletrace/bunext/types";
|
||||||
|
import type { BUN_SQLITE_WGUI_VARIABLES } from "@/db/types/db";
|
||||||
|
import type { ApiReqParams, TableType } from "@/src/types";
|
||||||
|
|
||||||
|
type SettingsBody = {
|
||||||
|
https_port?: number;
|
||||||
|
redirect?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||||
|
req,
|
||||||
|
body,
|
||||||
|
}) => {
|
||||||
|
const unauthorized = await adminApiGuard({ req });
|
||||||
|
|
||||||
|
if (unauthorized) {
|
||||||
|
return unauthorized;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET") {
|
||||||
|
const settings = await grabCaddySettings();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
singleRes: settings,
|
||||||
|
payload: [settings],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
return { success: false, msg: `Method not allowed` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = (body || {}) as ApiReqParams & SettingsBody;
|
||||||
|
const updates: { key: string; value: string }[] = [];
|
||||||
|
|
||||||
|
if (input.https_port !== undefined) {
|
||||||
|
const port = Number(input.https_port);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Number.isInteger(port) ||
|
||||||
|
port < 1 ||
|
||||||
|
port > 65535 ||
|
||||||
|
port == 80 ||
|
||||||
|
port == 443
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `HTTPS port must be between 1 and 65535 and cannot be 80 or 443`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updates.push({ key: "caddy_https_port", value: String(port) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.redirect !== undefined) {
|
||||||
|
updates.push({
|
||||||
|
key: "caddy_https_redirect",
|
||||||
|
value: input.redirect ? "1" : "0",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updates.length) {
|
||||||
|
return { success: false, msg: `No settings to update` };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const update of updates) {
|
||||||
|
const existing = await BunSQLite.select<
|
||||||
|
BUN_SQLITE_WGUI_VARIABLES,
|
||||||
|
TableType
|
||||||
|
>({
|
||||||
|
table: "variables",
|
||||||
|
});
|
||||||
|
|
||||||
|
const row = existing.payload?.find(
|
||||||
|
(variable) => variable.key === update.key,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (row) {
|
||||||
|
await BunSQLite.update<BUN_SQLITE_WGUI_VARIABLES, TableType>({
|
||||||
|
table: "variables",
|
||||||
|
targetId: row.id,
|
||||||
|
data: { value: update.value },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await BunSQLite.insert({
|
||||||
|
table: "variables",
|
||||||
|
data: [{ key: update.key, value: update.value }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sync = await syncCaddyIngress({ reason: "settings updated" });
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: sync.success,
|
||||||
|
msg: sync.msg,
|
||||||
|
debug: { warnings: sync.warnings },
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -17,18 +17,22 @@ export default function LoginForm() {
|
|||||||
<Form
|
<Form
|
||||||
className="w-full items-stretch gap-5"
|
className="w-full items-stretch gap-5"
|
||||||
onSubmit={async () => {
|
onSubmit={async () => {
|
||||||
const confirmed = await confirm({
|
// const confirmed = await confirm({
|
||||||
title: "Sign in?",
|
// title: "Sign in?",
|
||||||
message: "Use these credentials to sign in to the dashboard.",
|
// message: "Use these credentials to sign in to the dashboard.",
|
||||||
confirm_label: "Sign in",
|
// confirm_label: "Sign in",
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (confirmed) {
|
// if (confirmed) {
|
||||||
submitLoginForm(init);
|
// submitLoginForm(init);
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
submitLoginForm(init);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{status?.error ? <InlineAlert tone="error" msg={status.msg} /> : null}
|
{status?.error ? (
|
||||||
|
<InlineAlert tone="error" msg={status.msg} />
|
||||||
|
) : null}
|
||||||
<LoginFormEmailUsername {...init} />
|
<LoginFormEmailUsername {...init} />
|
||||||
<LoginFormPassword {...init} />
|
<LoginFormPassword {...init} />
|
||||||
<LoginFormAction {...init} />
|
<LoginFormAction {...init} />
|
||||||
|
|||||||
Reference in New Issue
Block a user