diff --git a/README.md b/README.md index 657c5b7c..c8676d1e 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,71 @@ # Caddy Proxy Manager -Caddy Proxy Manager is a modern control panel for Caddy that simplifies reverse proxy configuration, TLS automation, access control, and observability. The entire application is built with Next.js and ships with a lean dependency set, OAuth2 login, and a battery of tools for managing hosts, redirects, streams, certificates, and Cloudflare DNS-based certificate issuance. +[https://caddyproxymanager.com](https://caddyproxymanager.com) + +Caddy Proxy Manager is a modern control panel for Caddy that simplifies reverse proxy configuration, TLS automation, access control, and observability. The stack is built with Next.js 16 (App Router), Material UI, and a lightweight SQLite data layer. It ships with OAuth2 SSO, first-class Caddy admin API integration, and tooling for Cloudflare DNS challenge automation. ## Highlights -- **Next.js 14 App Router** UI and API in a single project, backed by an embedded SQLite database. -- **OAuth2 single sign-on** with PKCE and configurable claim mapping. The first authenticated user becomes the administrator. -- **End-to-end Caddy orchestration** using the admin API, generating JSON configurations for HTTP, HTTPS, redirects, custom 404 hosts, and TCP/UDP streams. -- **Cloudflare DNS challenge integration** via xcaddy-built Caddy binary with `cloudflare` and `layer4` modules; credentials are stored in the UI. -- **Access lists** (HTTP basic auth), custom certificates (managed or imported PEM), and a full audit log of administrative changes. -- **Default HSTS configuration** (`Strict-Transport-Security: max-age=63072000`) baked into every HTTP route to meet security baseline requirements. +- **Next.js 16 App Router** – server components for data loading, client components for interactivity, and a unified API surface. +- **Material UI dark mode** – fast, responsive dashboard with ready-made components and accessibility baked in. +- **OAuth2 single sign-on** – PKCE flow with configurable claims; the first authenticated user is promoted to administrator. +- **End-to-end Caddy orchestration** – generate JSON for HTTP(S) proxies, redirects, 404 hosts, and TCP/UDP streams via the Caddy admin API. +- **Cloudflare DNS challenge support** – xcaddy build bundles the `cloudflare` DNS and `layer4` modules; credentials are configurable in the UI. +- **Security-by-default** – HSTS (`Strict-Transport-Security: max-age=63072000`) applied to every managed host. +- **Embedded audit log** – every configuration change is recorded with actor, summary, and timestamp. ## Project Structure ``` . -├── app/ # Next.js app router (auth, dashboard, APIs) +├── app/ # Next.js App Router entrypoint (layouts, routes, server actions) +│ ├── (auth)/ # Login + OAuth setup flows +│ ├── (dashboard)/ # Dashboard layout, feature surface, client renderers +│ ├── api/ # Route handlers for auth callbacks/logout +│ ├── providers.tsx # Global MUI theme + CssBaseline +│ └── layout.tsx # Root HTML/body wrapper ├── src/ -│ └── lib/ # Database, Caddy integration, models, settings -├── docker/ # Dockerfiles for web + Caddy -├── compose.yaml # Production-ready docker compose definition -└── data/ # (Generated) SQLite database, TLS material, Caddy data +│ └── lib/ # SQLite integration, migrations, models, Caddy config builder +├── docker/ +│ ├── web/ # Next.js production image (standalone output) +│ └── caddy/ # xcaddy build with Cloudflare + layer4 modules +├── compose.yaml # Multi-container deployment (Next.js app + Caddy) +├── data/ # Generated at runtime (SQLite DB, cert storage, Caddy state) +└── README.md # You are here ``` +### Dashboard Modules + +- `ProxyHostsClient.tsx` – create/update/delete HTTP(S) reverse proxies, assign certs/access lists. +- `RedirectsClient.tsx` – manage 301/302 redirects with optional path/query preservation. +- `DeadHostsClient.tsx` – serve custom offline pages with programmable status codes. +- `StreamsClient.tsx` – configure TCP/UDP layer4 proxies. +- `AccessListsClient.tsx` – manage HTTP basic auth credentials and membership. +- `CertificatesClient.tsx` – import PEMs or request managed ACME certificates. +- `SettingsClient.tsx` – general metadata, OAuth2 endpoints, Cloudflare DNS token. +- `AuditLogClient.tsx` – list chronological administrative activity. + +## Feature Overview + +### Authentication & Authorization +- OAuth2/OIDC login with PKCE. +- First user bootstrap to admin role. +- Session persistence via signed, rotating cookies stored in SQLite. + +### Reverse Proxy Management +- HTTP(S) proxy hosts with TLS enforcement, WebSocket + HTTP/2 toggles. +- Redirect hosts with custom status codes and query preservation. +- Dead/maintenance hosts with custom responses. +- Stream (TCP/UDP) forwarding powered by the Caddy layer4 module. +- Access list (basic auth) integration for protected hosts. +- TLS certificate lifecycle: managed ACME (DNS-01 via Cloudflare) or imported PEMs. + +### Operations & Observability +- Full audit log with actor/action/summary/time. +- One-click revalidation of Caddy configuration after mutations. +- Migrations run automatically on startup; upgrades are seamless. +- Docker-first deployment, HSTS defaults, Cloudflare DNS automation. + ## Requirements - Node.js 20+ (development) diff --git a/app/(auth)/login/LoginClient.tsx b/app/(auth)/login/LoginClient.tsx index 9bbfaace..8fe79919 100644 --- a/app/(auth)/login/LoginClient.tsx +++ b/app/(auth)/login/LoginClient.tsx @@ -2,8 +2,13 @@ import Link from "next/link"; import { Alert, Box, Button, Card, CardContent, Stack, Typography } from "@mui/material"; +import { signIn } from "next-auth/react"; + +export default function LoginClient({ oauthConfigured, providerId }: { oauthConfigured: boolean; providerId: string }) { + const handleSignIn = async () => { + await signIn(providerId, { callbackUrl: "/" }); + }; -export default function LoginClient({ oauthConfigured, startOAuth }: { oauthConfigured: boolean; startOAuth: (formData: FormData) => void }) { return ( @@ -19,11 +24,9 @@ export default function LoginClient({ oauthConfigured, startOAuth }: { oauthConf {oauthConfigured ? ( - - - + ) : ( The system administrator needs to configure OAuth2 settings before logins are allowed. If this is a fresh installation, diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 3b37162c..5206d955 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -1,22 +1,19 @@ import { redirect } from "next/navigation"; -import { getSession } from "@/src/lib/auth/session"; -import { buildAuthorizationUrl } from "@/src/lib/auth/oauth"; +import { auth } from "@/src/lib/auth"; import { getOAuthSettings } from "@/src/lib/settings"; import LoginClient from "./LoginClient"; export default async function LoginPage() { - const session = await getSession(); + const session = await auth(); if (session) { redirect("/"); } - const oauthConfigured = Boolean(getOAuthSettings()); + const settings = getOAuthSettings(); + const oauthConfigured = Boolean(settings); - async function startOAuth() { - "use server"; - const target = buildAuthorizationUrl("/"); - redirect(target); - } + // Determine provider ID based on settings + const providerId = settings?.providerType === "authentik" ? "authentik" : "oauth"; - return ; + return ; } diff --git a/app/(dashboard)/access-lists/actions.ts b/app/(dashboard)/access-lists/actions.ts index 3bbf6122..a099f01f 100644 --- a/app/(dashboard)/access-lists/actions.ts +++ b/app/(dashboard)/access-lists/actions.ts @@ -1,7 +1,7 @@ "use server"; import { revalidatePath } from "next/cache"; -import { requireUser } from "@/src/lib/auth/session"; +import { requireUser } from "@/src/lib/auth"; import { addAccessListEntry, createAccessList, diff --git a/app/(dashboard)/certificates/actions.ts b/app/(dashboard)/certificates/actions.ts index 1d729cc0..ee85e101 100644 --- a/app/(dashboard)/certificates/actions.ts +++ b/app/(dashboard)/certificates/actions.ts @@ -1,7 +1,7 @@ "use server"; import { revalidatePath } from "next/cache"; -import { requireUser } from "@/src/lib/auth/session"; +import { requireUser } from "@/src/lib/auth"; import { createCertificate, deleteCertificate, updateCertificate } from "@/src/lib/models/certificates"; function parseDomains(value: FormDataEntryValue | null): string[] { diff --git a/app/(dashboard)/dead-hosts/actions.ts b/app/(dashboard)/dead-hosts/actions.ts index 0078dc60..5324f06d 100644 --- a/app/(dashboard)/dead-hosts/actions.ts +++ b/app/(dashboard)/dead-hosts/actions.ts @@ -1,7 +1,7 @@ "use server"; import { revalidatePath } from "next/cache"; -import { requireUser } from "@/src/lib/auth/session"; +import { requireUser } from "@/src/lib/auth"; import { createDeadHost, deleteDeadHost, updateDeadHost } from "@/src/lib/models/dead-hosts"; function parseDomains(value: FormDataEntryValue | null): string[] { diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index 30508d00..ff6b7640 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from "react"; -import { requireUser } from "@/src/lib/auth/session"; +import { requireUser } from "@/src/lib/auth"; import DashboardLayoutClient from "./DashboardLayoutClient"; export default async function DashboardLayout({ children }: { children: ReactNode }) { diff --git a/app/(dashboard)/page.tsx b/app/(dashboard)/page.tsx index 63e1d671..5764bed8 100644 --- a/app/(dashboard)/page.tsx +++ b/app/(dashboard)/page.tsx @@ -1,5 +1,5 @@ import db from "@/src/lib/db"; -import { requireUser } from "@/src/lib/auth/session"; +import { requireUser } from "@/src/lib/auth"; import OverviewClient from "./OverviewClient"; type StatCard = { diff --git a/app/(dashboard)/proxy-hosts/actions.ts b/app/(dashboard)/proxy-hosts/actions.ts index 63083c12..46f16eff 100644 --- a/app/(dashboard)/proxy-hosts/actions.ts +++ b/app/(dashboard)/proxy-hosts/actions.ts @@ -1,7 +1,7 @@ "use server"; import { revalidatePath } from "next/cache"; -import { requireUser } from "@/src/lib/auth/session"; +import { requireUser } from "@/src/lib/auth"; import { createProxyHost, deleteProxyHost, updateProxyHost } from "@/src/lib/models/proxy-hosts"; function parseCsv(value: FormDataEntryValue | null): string[] { diff --git a/app/(dashboard)/redirects/actions.ts b/app/(dashboard)/redirects/actions.ts index 68178fb4..507c4907 100644 --- a/app/(dashboard)/redirects/actions.ts +++ b/app/(dashboard)/redirects/actions.ts @@ -1,7 +1,7 @@ "use server"; import { revalidatePath } from "next/cache"; -import { requireUser } from "@/src/lib/auth/session"; +import { requireUser } from "@/src/lib/auth"; import { createRedirectHost, deleteRedirectHost, updateRedirectHost } from "@/src/lib/models/redirect-hosts"; function parseList(value: FormDataEntryValue | null): string[] { diff --git a/app/(dashboard)/settings/SettingsClient.tsx b/app/(dashboard)/settings/SettingsClient.tsx index f8382055..01eb3742 100644 --- a/app/(dashboard)/settings/SettingsClient.tsx +++ b/app/(dashboard)/settings/SettingsClient.tsx @@ -1,6 +1,7 @@ "use client"; -import { Box, Button, Card, CardContent, Stack, TextField, Typography } from "@mui/material"; +import { useState } from "react"; +import { Box, Button, Card, CardContent, FormControl, FormControlLabel, FormLabel, Radio, RadioGroup, Stack, TextField, Typography } from "@mui/material"; import type { CloudflareSettings, GeneralSettings, OAuthSettings } from "@/src/lib/settings"; import { updateCloudflareSettingsAction, @@ -15,6 +16,7 @@ type Props = { }; export default function SettingsClient({ general, oauth, cloudflare }: Props) { + const [providerType, setProviderType] = useState<"authentik" | "generic">(oauth?.providerType || "authentik"); return ( @@ -56,24 +58,55 @@ export default function SettingsClient({ general, oauth, cloudflare }: Props) { - OAuth2 Authentication + OAuth2/OIDC Authentication - Provide the OAuth 2.0 endpoints and client credentials issued by your identity provider. Scopes should include profile and + Provide the OAuth 2.0/OIDC endpoints and client credentials issued by your identity provider. Scopes should include profile and email data. - - - - - - - - - - - + + Provider Type + setProviderType(e.target.value as "authentik" | "generic")} + > + } label="Authentik (OIDC)" /> + } label="Generic OAuth2" /> + + + + {providerType === "authentik" ? ( + <> + + + + + + ) : ( + <> + + + + + + + + + + + + + )}