Update Reverse proxy and TLS configuration on Caddy

This commit is contained in:
2026-10-21 13:01:33 +01:00
parent ed4a6f3ec8
commit 42feb73264
22 changed files with 539 additions and 37 deletions
+4
View File
@@ -351,6 +351,10 @@ const schema: BUN_SQLITE_DatabaseSchemaType = {
fieldName: "acme_credentials",
dataType: "TEXT",
},
{
fieldName: "allowed_ips",
dataType: "TEXT",
},
],
uniqueConstraints: [
{
+2 -1
View File
@@ -191,6 +191,7 @@ export type BUN_SQLITE_WGUI_CLIENT_ROUTES = {
acme_dns_provider?: string;
acme_staging?: 0 | 1 | "";
acme_credentials?: string;
allowed_ips?: string;
}
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)
*/
updated_at?: number | "";
key?: "test_var" | "";
key?: "test_var" | "caddy_https_port" | "caddy_https_redirect" | "";
value?: string;
}
@@ -12,6 +12,8 @@ import FormSection from "../form-section";
import FormActions from "../form-actions";
import InlineAlert from "../inline-alert";
import submitClientRouteForm from "./submit-client-route-form";
import { useContext } from "react";
import { AppContext } from "@/src/pages/__root";
type Props = {
host_id: number;
@@ -25,6 +27,7 @@ export default function ClientRouteForm({
cancel_href,
}: Props) {
const { confirm } = useConfirm();
const { user } = useContext(AppContext);
const init = useFormInit<ClientRouteFormObject>({
existing: existing_route,
@@ -33,6 +36,14 @@ export default function ClientRouteForm({
? `Edit route v2 ${existing_route.id}`
: `Add client route v2 ${host_id}`,
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;
@@ -56,7 +67,11 @@ export default function ClientRouteForm({
}
>
{status?.error ? (
<InlineAlert tone="error" msg={status.msg} className="mb-6" />
<InlineAlert
tone="error"
msg={status.msg}
className="mb-6"
/>
) : null}
<FormSection
@@ -161,6 +176,26 @@ export default function ClientRouteForm({
/>
</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
title="TLS certificate"
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,
enabled: form.enabled == 0 ? 0 : 1,
notes: form.notes || "",
allowed_ips: form.allowed_ips || "",
acme_email: form.acme_email || "",
acme_staging: form.acme_staging == 1 ? 1 : 0,
};
@@ -53,6 +53,16 @@ export default function buildClientRoutesTableColumns({
header: "Path",
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",
header: "Upstream",
+10
View File
@@ -4,6 +4,16 @@ export const Variables = [
value: "test_var",
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`,
// value: "main_host_wg_ip_address",
@@ -2,6 +2,7 @@ import type {
BUN_SQLITE_WGUI_CLIENT_ROUTES,
BUN_SQLITE_WGUI_TLS_CERTIFICATES,
} from "@/db/types/db";
import { TlsData } from "@/src/data/tls-data";
import readCertificateExpiry from "./read-certificate-expiry";
export type ClientRouteView = BUN_SQLITE_WGUI_CLIENT_ROUTES & {
@@ -9,16 +10,40 @@ export type ClientRouteView = BUN_SQLITE_WGUI_CLIENT_ROUTES & {
acme_credentials_configured?: boolean;
certificate_status?: string;
certificate_expires_at?: number | null;
public_url?: string;
};
type Params = {
route: BUN_SQLITE_WGUI_CLIENT_ROUTES;
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({
route,
certificate,
https_port,
}: Params): ClientRouteView {
const { acme_credentials, ...safe_route } = route;
const expires_at = readCertificateExpiry({
@@ -45,5 +70,6 @@ export default function decorateClientRoute({
acme_credentials_configured: Boolean(acme_credentials),
certificate_status,
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 { TlsData } from "@/src/data/tls-data";
import grabCaddyStatus from "../caddy/grab-caddy-status";
import grabCaddySettings from "../caddy/grab-caddy-settings";
import grabCertDirNames from "../caddy/grab-cert-dir-names";
import readAcmeEnv from "./read-acme-env";
import readCertificateExpiry from "./read-certificate-expiry";
@@ -95,8 +96,10 @@ export default async function grabTlsStatus() {
(a.hostname || "").localeCompare(b.hostname || ""),
);
const settings = await grabCaddySettings();
return {
caddy: grabCaddyStatus(),
caddy: grabCaddyStatus(settings),
certificates,
acme: {
env_path: acme_env.exists ? `/etc/wgui/acme.env` : null,
@@ -112,6 +115,8 @@ export default async function grabTlsStatus() {
),
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("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("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", () => {
const res = buildCaddyfile({
...base,
@@ -88,6 +99,25 @@ describe("buildCaddyfile", () => {
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", () => {
const res = buildCaddyfile({
...base,
@@ -102,4 +132,22 @@ describe("buildCaddyfile", () => {
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");
});
});
+51 -16
View File
@@ -20,6 +20,8 @@ type Params = {
hosts?: BUN_SQLITE_WGUI_HOSTS[];
https_port: number;
http_port: number;
/** Redirect plain HTTP requests to HTTPS. Enabled by default. */
redirect?: boolean;
admin_socket: string;
storage_dir: string;
};
@@ -47,6 +49,33 @@ function reverseProxyBlock({
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({
route,
index,
@@ -59,8 +88,9 @@ function routeBlock({
const path_prefix = route.path_prefix || "/";
const matcher = `route_${index}`;
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(...reverseProxyBlock({ route, indent: " " }));
lines.push(` }`);
@@ -68,17 +98,13 @@ function routeBlock({
}
if (path_prefix == "/") {
lines.push(
` @${matcher} remote_ip ${subnet} 127.0.0.1/32 ::1/128`,
);
lines.push(` @${matcher} remote_ip ${remote_ip_value}`);
} else {
lines.push(` @${matcher} {`);
lines.push(` path ${path_prefix} ${path_prefix}/*`);
if (subnet) {
lines.push(
` remote_ip ${subnet} 127.0.0.1/32 ::1/128`,
);
if (remote_ip_value.trim()) {
lines.push(` remote_ip ${remote_ip_value}`);
}
lines.push(` }`);
@@ -107,6 +133,7 @@ export default function buildCaddyfile({
hosts,
https_port,
http_port,
redirect = true,
admin_socket,
storage_dir,
}: Params): CaddyBuildResult {
@@ -117,16 +144,25 @@ export default function buildCaddyfile({
for (let i = 0; i < (hosts || []).length; i++) {
const host = hosts?.[i];
const parts = (host?.wg_ip_address || "").trim().split(".");
const matchers: string[] = [];
if (
host?.id !== undefined &&
parts.length == 4 &&
parts.every((part) => /^\d{1,3}$/.test(part))
) {
subnet_by_host_id.set(
Number(host.id),
`${parts.slice(0, 3).join(".")}.0/24`,
);
matchers.push(`${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<
@@ -142,8 +178,7 @@ export default function buildCaddyfile({
hostname &&
certificate?.cert_path &&
certificate.key_path &&
(certificate.status == "active" ||
certificate.status == "expiring")
(certificate.status == "active" || certificate.status == "expiring")
) {
certificates_by_hostname.set(hostname, {
...certificate,
@@ -215,7 +250,7 @@ export default function buildCaddyfile({
`{`,
` https_port ${https_port}`,
` http_port ${http_port}`,
` auto_https disable_redirects`,
...(redirect ? [] : [` auto_https disable_redirects`]),
` admin unix/${admin_socket}`,
` 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;
}
export default function grabCaddyStatus() {
type Settings = {
https_port?: number;
http_port?: number;
};
export default function grabCaddyStatus(settings?: Settings) {
const state = readState();
return {
@@ -85,8 +90,8 @@ export default function grabCaddyStatus() {
acme_installed: existsSync(WGUI_ACME_SCRIPT),
acme_env_exists: existsSync(WGUI_ACME_ENV_FILE),
version: version(),
https_port: TlsData.CaddyHttpsPort,
http_port: TlsData.CaddyHttpPort,
https_port: settings?.https_port || TlsData.CaddyHttpsPort,
http_port: settings?.http_port || TlsData.CaddyHttpPort,
enabled: TlsData.IngressEnabled,
config_path: WGUI_LIB_CADDY_CONFIG_FILE,
state,
@@ -93,6 +93,10 @@ export default function sanitizeClientRoute({
typeof route.acme_credentials == "string"
? route.acme_credentials
: "",
allowed_ips:
typeof route.allowed_ips == "string"
? route.allowed_ips.trim()
: "",
} satisfies BUN_SQLITE_WGUI_CLIENT_ROUTES,
};
}
@@ -20,6 +20,7 @@ import { TlsData } from "@/src/data/tls-data";
import buildCaddyfile from "./build-caddyfile";
import reloadCaddyService from "./reload-caddy-service";
import applyCaddyIngressFirewallRule from "./apply-caddy-ingress-firewall-rule";
import grabCaddySettings from "./grab-caddy-settings";
export type CaddySyncResult = {
success: boolean;
@@ -120,12 +121,15 @@ async function doSync({ reason }: Params): Promise<CaddySyncResult> {
: ("active" as const),
}));
const settings = await grabCaddySettings();
const built = buildCaddyfile({
routes,
certificates,
hosts,
https_port: TlsData.CaddyHttpsPort,
http_port: TlsData.CaddyHttpPort,
https_port: settings.https_port,
http_port: settings.http_port,
redirect: settings.redirect,
admin_socket: WGUI_LIB_CADDY_ADMIN_SOCKET,
storage_dir: WGUI_LIB_CADDY_DATA_DIR,
});
@@ -218,7 +222,10 @@ async function doSync({ reason }: Params): Promise<CaddySyncResult> {
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) {
warnings.push(firewall.msg);
@@ -45,6 +45,13 @@ export default function RouteOverviewSection({ route, host }: Props) {
mono: true,
copy_value: route.hostname || "",
},
{
key: "public_url",
label: "Public URL",
value: route.public_url || "—",
mono: true,
copy_value: route.public_url || "",
},
{
key: "path",
label: "Path prefix",
@@ -89,6 +96,12 @@ export default function RouteOverviewSection({ route, host }: Props) {
label: "Staging CA",
value: route.acme_staging == 1 ? "Yes" : "No",
},
{
key: "allowed_ips",
label: "Allowed IPs",
value: route.allowed_ips || "All from host subnet",
mono: true,
},
]}
/>
</Panel>
@@ -3,6 +3,7 @@ import type {
BUN_SQLITE_WGUI_HOSTS,
} from "@/db/types/db";
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 { grabManualDnsChallenge } from "@/src/functions/backend/acme/manual-dns-challenges";
import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
@@ -31,6 +32,7 @@ const server: BunextPageServerFn<PagePropsType> = async ({ query }) => {
});
const route = route_res.singleRes;
const settings = await grabCaddySettings();
const certificate = route
? await grabCertificateByHostname({ hostname: route.hostname })
: null;
@@ -42,7 +44,11 @@ const server: BunextPageServerFn<PagePropsType> = async ({ query }) => {
props: {
host: host_res.singleRes || null,
client_route: route
? decorateClientRoute({ route, certificate })
? decorateClientRoute({
route,
certificate,
https_port: settings.https_port,
})
: null,
client_certificate: certificate,
client_challenge: pending
@@ -3,6 +3,7 @@ import type {
BUN_SQLITE_WGUI_HOSTS,
} from "@/db/types/db";
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 type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite";
@@ -30,6 +31,7 @@ const server: BunextPageServerFn<PagePropsType> = async ({ query }) => {
});
const route = route_res.singleRes;
const settings = await grabCaddySettings();
return {
props: {
@@ -40,6 +42,7 @@ const server: BunextPageServerFn<PagePropsType> = async ({ query }) => {
certificate: await grabCertificateByHostname({
hostname: route.hostname,
}),
https_port: settings.https_port,
})
: 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>
);
}
+2
View File
@@ -3,6 +3,7 @@ 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 TlsSettingsSection from "./(sections)/tls-settings-section";
import TlsInstallSection from "./(sections)/tls-install-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."
>
<TlsStatusSection tls={tls} />
<TlsSettingsSection tls={tls} />
<TlsInstallSection tls={tls} />
<TlsSetupSection tls={tls} />
</SetupPageFrame>
@@ -4,6 +4,7 @@ import type {
} from "@/db/types/db";
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
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 type { AdminCrudAPIParams, TableType } from "@/src/types";
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 view = decorateClientRoute({
route,
certificate: certificates_by_hostname.get(route.hostname || ""),
https_port: settings.https_port,
});
if (
+107
View File
@@ -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
className="w-full items-stretch gap-5"
onSubmit={async () => {
const confirmed = await confirm({
title: "Sign in?",
message: "Use these credentials to sign in to the dashboard.",
confirm_label: "Sign in",
});
// const confirmed = await confirm({
// title: "Sign in?",
// message: "Use these credentials to sign in to the dashboard.",
// confirm_label: "Sign in",
// });
// if (confirmed) {
// submitLoginForm(init);
// }
if (confirmed) {
submitLoginForm(init);
}
}}
>
{status?.error ? <InlineAlert tone="error" msg={status.msg} /> : null}
{status?.error ? (
<InlineAlert tone="error" msg={status.msg} />
) : null}
<LoginFormEmailUsername {...init} />
<LoginFormPassword {...init} />
<LoginFormAction {...init} />