diff --git a/app/(dashboard)/settings/SettingsClient.tsx b/app/(dashboard)/settings/SettingsClient.tsx index 668e60e0..8a36fae1 100644 --- a/app/(dashboard)/settings/SettingsClient.tsx +++ b/app/(dashboard)/settings/SettingsClient.tsx @@ -1,13 +1,14 @@ "use client"; import { useFormState } from "react-dom"; -import { Alert, Box, Button, Card, CardContent, Checkbox, FormControlLabel, Stack, TextField, Typography } from "@mui/material"; -import type { GeneralSettings, AuthentikSettings, MetricsSettings } from "@/src/lib/settings"; +import { Alert, Box, Button, Card, CardContent, Checkbox, FormControlLabel, MenuItem, Stack, TextField, Typography } from "@mui/material"; +import type { GeneralSettings, AuthentikSettings, MetricsSettings, LoggingSettings } from "@/src/lib/settings"; import { updateCloudflareSettingsAction, updateGeneralSettingsAction, updateAuthentikSettingsAction, - updateMetricsSettingsAction + updateMetricsSettingsAction, + updateLoggingSettingsAction } from "./actions"; type Props = { @@ -19,13 +20,15 @@ type Props = { }; authentik: AuthentikSettings | null; metrics: MetricsSettings | null; + logging: LoggingSettings | null; }; -export default function SettingsClient({ general, cloudflare, authentik, metrics }: Props) { +export default function SettingsClient({ general, cloudflare, authentik, metrics, logging }: Props) { const [generalState, generalFormAction] = useFormState(updateGeneralSettingsAction, null); const [cloudflareState, cloudflareFormAction] = useFormState(updateCloudflareSettingsAction, null); const [authentikState, authentikFormAction] = useFormState(updateAuthentikSettingsAction, null); const [metricsState, metricsFormAction] = useFormState(updateMetricsSettingsAction, null); + const [loggingState, loggingFormAction] = useFormState(updateLoggingSettingsAction, null); return ( @@ -201,6 +204,49 @@ export default function SettingsClient({ general, cloudflare, authentik, metrics + + + + + Access Logging + + + Enable HTTP access logging to track all requests going through your proxy hosts. + Logs will be stored in the caddy-logs directory and mounted at /logs/access.log inside the container. + + + {loggingState?.message && ( + + {loggingState.message} + + )} + } + label="Enable access logging" + /> + + JSON + Console (Common Log Format) + + + Access logs will be available at ./caddy-logs/access.log on the host machine. + You can tail them with: docker exec caddy-proxy-manager-caddy tail -f /logs/access.log + + + + + + + ); } diff --git a/app/(dashboard)/settings/actions.ts b/app/(dashboard)/settings/actions.ts index 6eee178f..5c5d9af6 100644 --- a/app/(dashboard)/settings/actions.ts +++ b/app/(dashboard)/settings/actions.ts @@ -3,7 +3,7 @@ import { revalidatePath } from "next/cache"; import { requireAdmin } from "@/src/lib/auth"; import { applyCaddyConfig } from "@/src/lib/caddy"; -import { getCloudflareSettings, saveCloudflareSettings, saveGeneralSettings, saveAuthentikSettings, saveMetricsSettings } from "@/src/lib/settings"; +import { getCloudflareSettings, saveCloudflareSettings, saveGeneralSettings, saveAuthentikSettings, saveMetricsSettings, saveLoggingSettings } from "@/src/lib/settings"; type ActionResult = { success: boolean; @@ -118,3 +118,39 @@ export async function updateMetricsSettingsAction(_prevState: ActionResult | nul return { success: false, message: error instanceof Error ? error.message : "Failed to save metrics settings" }; } } + +export async function updateLoggingSettingsAction(_prevState: ActionResult | null, formData: FormData): Promise { + try { + await requireAdmin(); + const enabled = formData.get("enabled") === "on"; + const format = formData.get("format") ? String(formData.get("format")).trim() : "json"; + + // Validate format + if (format !== "json" && format !== "console") { + return { success: false, message: "Invalid log format. Must be 'json' or 'console'" }; + } + + await saveLoggingSettings({ + enabled, + format: format as "json" | "console" + }); + + // Apply config to enable/disable logging + try { + await applyCaddyConfig(); + revalidatePath("/settings"); + return { success: true, message: "Logging settings saved and applied successfully" }; + } catch (error) { + console.error("Failed to apply Caddy config:", error); + revalidatePath("/settings"); + const errorMsg = error instanceof Error ? error.message : "Unknown error"; + return { + success: true, + message: `Settings saved, but could not apply to Caddy: ${errorMsg}` + }; + } + } catch (error) { + console.error("Failed to save logging settings:", error); + return { success: false, message: error instanceof Error ? error.message : "Failed to save logging settings" }; + } +} diff --git a/app/(dashboard)/settings/page.tsx b/app/(dashboard)/settings/page.tsx index 9e3e8b75..82ba89dc 100644 --- a/app/(dashboard)/settings/page.tsx +++ b/app/(dashboard)/settings/page.tsx @@ -1,15 +1,16 @@ import SettingsClient from "./SettingsClient"; -import { getCloudflareSettings, getGeneralSettings, getAuthentikSettings, getMetricsSettings } from "@/src/lib/settings"; +import { getCloudflareSettings, getGeneralSettings, getAuthentikSettings, getMetricsSettings, getLoggingSettings } from "@/src/lib/settings"; import { requireAdmin } from "@/src/lib/auth"; export default async function SettingsPage() { await requireAdmin(); - const [general, cloudflare, authentik, metrics] = await Promise.all([ + const [general, cloudflare, authentik, metrics, logging] = await Promise.all([ getGeneralSettings(), getCloudflareSettings(), getAuthentikSettings(), - getMetricsSettings() + getMetricsSettings(), + getLoggingSettings() ]); return ( @@ -22,6 +23,7 @@ export default async function SettingsPage() { }} authentik={authentik} metrics={metrics} + logging={logging} /> ); } diff --git a/docker-compose.yml b/docker-compose.yml index ac223999..9618376e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -69,6 +69,7 @@ services: volumes: - ./caddy-data:/data - ./caddy-config:/config + - ./caddy-logs:/logs networks: - caddy-network healthcheck: diff --git a/src/lib/caddy.ts b/src/lib/caddy.ts index 1f7b9cca..b3c2e9c1 100644 --- a/src/lib/caddy.ts +++ b/src/lib/caddy.ts @@ -3,7 +3,7 @@ import { join } from "node:path"; import crypto from "node:crypto"; import db, { nowIso } from "./db"; import { config } from "./config"; -import { getCloudflareSettings, getGeneralSettings, getMetricsSettings, setSetting } from "./settings"; +import { getCloudflareSettings, getGeneralSettings, getMetricsSettings, getLoggingSettings, setSetting } from "./settings"; import { accessListEntries, certificates, @@ -930,6 +930,11 @@ async function buildCaddyDocument() { const metricsEnabled = metricsSettings?.enabled ?? false; const metricsPort = metricsSettings?.port ?? 9090; + // Check if access logging should be enabled + const loggingSettings = await getLoggingSettings(); + const loggingEnabled = loggingSettings?.enabled ?? false; + const loggingFormat = loggingSettings?.format ?? "json"; + const servers: Record = {}; // Main HTTP/HTTPS server for proxy hosts @@ -940,7 +945,9 @@ async function buildCaddyDocument() { // Only disable automatic HTTPS if we have TLS automation policies // This allows Caddy to handle HTTP-01 challenges for managed certificates ...(tlsApp ? {} : { automatic_https: { disable: true } }), - ...(hasTls ? { tls_connection_policies: tlsConnectionPolicies } : {}) + ...(hasTls ? { tls_connection_policies: tlsConnectionPolicies } : {}), + // Enable access logging if configured + ...(loggingEnabled ? { logs: { default_logger_name: "http_access" } } : {}) }; } @@ -966,10 +973,31 @@ async function buildCaddyDocument() { const httpApp = Object.keys(servers).length > 0 ? { http: { servers } } : {}; + // Build logging configuration if enabled + const loggingApp = loggingEnabled + ? { + logging: { + logs: { + http_access: { + writer: { + output: "file", + filename: "/logs/access.log" + }, + encoder: { + format: loggingFormat + }, + include: ["http.log.access"] + } + } + } + } + : {}; + return { admin: { listen: "0.0.0.0:2019" }, + ...loggingApp, apps: { ...httpApp, ...(tlsApp ? { tls: tlsApp } : {}) diff --git a/src/lib/settings.ts b/src/lib/settings.ts index 4c271d2e..b1d971a6 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -26,6 +26,11 @@ export type MetricsSettings = { port?: number; // Port to expose metrics on (default: 9090, separate from admin API) }; +export type LoggingSettings = { + enabled: boolean; + format?: "json" | "console"; // Log format (default: json) +}; + export async function getSetting(key: string): Promise> { const setting = await db.query.settings.findFirst({ where: (table, { eq }) => eq(table.key, key) @@ -94,3 +99,11 @@ export async function getMetricsSettings(): Promise { export async function saveMetricsSettings(settings: MetricsSettings): Promise { await setSetting("metrics", settings); } + +export async function getLoggingSettings(): Promise { + return await getSetting("logging"); +} + +export async function saveLoggingSettings(settings: LoggingSettings): Promise { + await setSetting("logging", settings); +}