21 Commits

Author SHA1 Message Date
c27f4e40a3 changed port 3001 to 3000
Some checks are pending
Build and Push Docker Images (Trusted) / build-and-push (., docker/caddy/Dockerfile, caddy) (push) Waiting to run
Build and Push Docker Images (Trusted) / build-and-push (., docker/l4-port-manager/Dockerfile, l4-port-manager) (push) Waiting to run
Build and Push Docker Images (Trusted) / build-and-push (., docker/web/Dockerfile, web) (push) Waiting to run
Tests / test (push) Waiting to run
2026-04-23 06:52:54 +00:00
f861b95c9b set primary domain
Some checks failed
Build and Push Docker Images (Trusted) / build-and-push (., docker/caddy/Dockerfile, caddy) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/l4-port-manager/Dockerfile, l4-port-manager) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/web/Dockerfile, web) (push) Has been cancelled
Tests / test (push) Has been cancelled
2026-04-22 10:39:37 +00:00
d6bb1871dd changed bind mounts back to volumes
Some checks failed
Build and Push Docker Images (Trusted) / build-and-push (., docker/caddy/Dockerfile, caddy) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/l4-port-manager/Dockerfile, l4-port-manager) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/web/Dockerfile, web) (push) Has been cancelled
Tests / test (push) Has been cancelled
2026-04-22 10:38:45 +00:00
12da316ace testing
Some checks failed
Build and Push Docker Images (Trusted) / build-and-push (., docker/caddy/Dockerfile, caddy) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/l4-port-manager/Dockerfile, l4-port-manager) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/web/Dockerfile, web) (push) Has been cancelled
Tests / test (push) Has been cancelled
2026-04-22 10:35:36 +00:00
3fb643f41e changed volumes to bind mounts
Some checks failed
Build and Push Docker Images (Trusted) / build-and-push (., docker/caddy/Dockerfile, caddy) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/l4-port-manager/Dockerfile, l4-port-manager) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/web/Dockerfile, web) (push) Has been cancelled
Tests / test (push) Has been cancelled
2026-04-22 10:28:30 +00:00
25cd4669b2 changed docker-compose to compose
Some checks failed
Build and Push Docker Images (Trusted) / build-and-push (., docker/caddy/Dockerfile, caddy) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/l4-port-manager/Dockerfile, l4-port-manager) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/web/Dockerfile, web) (push) Has been cancelled
Tests / test (push) Has been cancelled
2026-04-22 10:26:00 +00:00
47af056a7c changed port
Some checks failed
Build and Push Docker Images (Trusted) / build-and-push (., docker/caddy/Dockerfile, caddy) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/l4-port-manager/Dockerfile, l4-port-manager) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/web/Dockerfile, web) (push) Has been cancelled
Tests / test (push) Has been cancelled
2026-04-22 10:23:33 +00:00
7a91843c79 updated config
Some checks failed
Build and Push Docker Images (Trusted) / build-and-push (., docker/caddy/Dockerfile, caddy) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/l4-port-manager/Dockerfile, l4-port-manager) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/web/Dockerfile, web) (push) Has been cancelled
Tests / test (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
2026-04-21 22:54:55 +00:00
99819b70ff added caddy-proxy-manager for testing
Some checks failed
Build and Push Docker Images (Trusted) / build-and-push (., docker/caddy/Dockerfile, caddy) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/l4-port-manager/Dockerfile, l4-port-manager) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/web/Dockerfile, web) (push) Has been cancelled
Tests / test (push) Has been cancelled
2026-04-21 22:49:08 +00:00
fuomag9
4c5ad53370 Fix inflated certificate counts in dashboard and certificates page
The dashboard overview counted all proxy hosts without a cert as ACME
certs, ignoring wildcard deduplication. The certificates page only
deduplicated the current page of results (25 rows) but used a full
count(*) for the total, so hosts on other pages covered by wildcards
were never subtracted.

Now both pages fetch all ACME hosts, apply full deduplication (cert
wildcard coverage + ACME wildcard collapsing), then paginate/count
from the deduplicated result.

Also fixes a strict-mode violation in the E2E test where DataTable's
dual mobile/desktop rendering caused getByText to match two elements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 21:01:39 +02:00
fuomag9
d710ad1247 Remove unnecessary IONOS field name migration
The wrong field name only existed for one commit before the fix, and
the only affected user already re-entered their credentials.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 18:09:33 +02:00
fuomag9
6515da666f Fix duplicate ACME entries for subdomains covered by wildcard ACME host
The previous fix (92fa1cb) only collapsed subdomains when an explicit
managed/imported certificate had a wildcard. When all hosts use Caddy
Auto (no certificate assigned), the wildcard ACME host was not checked
against sibling subdomain hosts, so each showed as a separate entry.

Also adds a startup migration to rename the stored IONOS DNS credential
key from api_token to auth_api_token for users who configured IONOS
before ef62ef2.

Closes #110

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 18:04:38 +02:00
fuomag9
96bac86934 Fix L4 port manager failing to recreate caddy after Docker restart
The sidecar's `docker compose up` command lacked `--pull never`, so
Docker Compose would attempt to pull the caddy image from ghcr.io when
the local image was missing or stale. Since the sidecar has no registry
credentials this failed with 403 Forbidden.

Closes #117

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 11:35:12 +02:00
fuomag9
dbfc340ea4 Fix logout redirect to 0.0.0.0 instead of configured BASE_URL
Closes #113

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 09:51:51 +02:00
dependabot[bot]
521a059414 deps(deps): Bump better-auth in the production-dependencies group (#116)
Bumps the production-dependencies group with 1 update: [better-auth](https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth).


Updates `better-auth` from 1.6.4 to 1.6.5
- [Release notes](https://github.com/better-auth/better-auth/releases)
- [Changelog](https://github.com/better-auth/better-auth/blob/main/packages/better-auth/CHANGELOG.md)
- [Commits](https://github.com/better-auth/better-auth/commits/better-auth@1.6.5/packages/better-auth)

---
updated-dependencies:
- dependency-name: better-auth
  dependency-version: 1.6.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 05:07:02 +00:00
dependabot[bot]
1be8fc2629 deps(deps-dev): Bump the development-dependencies group with 3 updates (#115)
Bumps the development-dependencies group with 3 updates: [eslint](https://github.com/eslint/eslint), [shadcn](https://github.com/shadcn-ui/ui/tree/HEAD/packages/shadcn) and [typescript](https://github.com/microsoft/TypeScript).


Updates `eslint` from 10.2.0 to 10.2.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.2.0...v10.2.1)

Updates `shadcn` from 4.2.0 to 4.3.0
- [Release notes](https://github.com/shadcn-ui/ui/releases)
- [Changelog](https://github.com/shadcn-ui/ui/blob/main/packages/shadcn/CHANGELOG.md)
- [Commits](https://github.com/shadcn-ui/ui/commits/shadcn@4.3.0/packages/shadcn)

Updates `typescript` from 6.0.2 to 6.0.3
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v6.0.2...v6.0.3)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.2.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: shadcn
  dependency-version: 4.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 05:06:24 +00:00
dependabot[bot]
6d2827a132 ci(deps): Bump dependabot/fetch-metadata from 2 to 3 (#114)
Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 2 to 3.
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](https://github.com/dependabot/fetch-metadata/compare/v2...v3)

---
updated-dependencies:
- dependency-name: dependabot/fetch-metadata
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 05:03:28 +00:00
fuomag9
eb11856994 Update README for multi-provider DNS, forward auth excluded paths
- Add DNS Providers feature listing all 12 supported providers
- Update Certificate Management section for multi-provider DNS-01
- Mention excluded paths in Forward Auth Portal feature
- Remove completed roadmap item (additional DNS providers)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 22:24:04 +02:00
fuomag9
7d61528dad Fix login rejection for usernames containing hyphens
better-auth's default username validator only allows [a-zA-Z0-9_.],
rejecting hyphens with a generic "invalid username or password" error.
Added a custom validator that also permits hyphens.

Closes #112

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 12:15:18 +02:00
fuomag9
92fa1cb9d8 Fix duplicate certificate display for wildcard-covered subdomains
When a wildcard cert (e.g. *.domain.de) existed and a proxy host was created
for a subdomain (e.g. sub.domain.de) without explicitly linking it, the
certificates page showed it as a separate ACME entry. Now hosts covered by
an existing wildcard cert are attributed to that cert's "Used by" list instead.

Closes #110

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 12:10:17 +02:00
fuomag9
ef62ef232f Fix IONOS DNS provider field name (api_token -> auth_api_token)
The IONOS libdns provider uses auth_api_token as the JSON field name,
not api_token. This caused Caddy to reject the config with
"unknown field api_token".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 00:57:36 +02:00
406 changed files with 355 additions and 60 deletions

0
.dockerignore Normal file → Executable file
View File

0
.env.example Normal file → Executable file
View File

0
.github/FUNDING.yml vendored Normal file → Executable file
View File

0
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file → Executable file
View File

0
.github/ISSUE_TEMPLATE/dns_challenge_request.md vendored Normal file → Executable file
View File

0
.github/ISSUE_TEMPLATE/feature_request.md vendored Normal file → Executable file
View File

0
.github/dependabot.yml vendored Normal file → Executable file
View File

2
.github/workflows/dependabot-automerge.yml vendored Normal file → Executable file
View File

@@ -17,7 +17,7 @@ jobs:
steps: steps:
- name: Fetch Dependabot metadata - name: Fetch Dependabot metadata
id: metadata id: metadata
uses: dependabot/fetch-metadata@v2 uses: dependabot/fetch-metadata@v3
- name: Auto-approve the PR - name: Auto-approve the PR
run: gh pr review --approve "$PR_URL" run: gh pr review --approve "$PR_URL"

0
.github/workflows/docker-build-pr.yml vendored Normal file → Executable file
View File

0
.github/workflows/docker-build-trusted.yml vendored Normal file → Executable file
View File

0
.github/workflows/stale.yml vendored Normal file → Executable file
View File

0
.github/workflows/test.yml vendored Normal file → Executable file
View File

0
.gitignore vendored Normal file → Executable file
View File

0
.version Normal file → Executable file
View File

0
LICENSE Normal file → Executable file
View File

9
README.md Normal file → Executable file
View File

@@ -38,7 +38,7 @@ Data persists in Docker volumes (caddy-manager-data, caddy-data, caddy-config, c
- **L4 Proxy Hosts** - TCP/UDP stream proxying with TLS SNI matching, proxy protocol (v1/v2), load balancing, health checks, and per-host geo blocking. Automatic Docker Compose port management via sidecar - **L4 Proxy Hosts** - TCP/UDP stream proxying with TLS SNI matching, proxy protocol (v1/v2), load balancing, health checks, and per-host geo blocking. Automatic Docker Compose port management via sidecar
- **Location Rules** - Path-based routing to different upstreams per proxy host (e.g. `/api/*` to one backend, `/ws/*` to another) - **Location Rules** - Path-based routing to different upstreams per proxy host (e.g. `/api/*` to one backend, `/ws/*` to another)
- **Redirect & Rewrite** - Per-host redirect rules (301/302/307/308) and path prefix rewriting - **Redirect & Rewrite** - Per-host redirect rules (301/302/307/308) and path prefix rewriting
- **Forward Auth Portal** - Built-in identity provider for protecting proxy hosts without an external IdP. Credential and OAuth login portal, user groups with membership management, and per-host access control by user or group - **Forward Auth Portal** - Built-in identity provider for protecting proxy hosts without an external IdP. Credential and OAuth login portal, user groups with membership management, per-host access control by user or group, and excluded paths that bypass authentication
- **WAF** - Web Application Firewall powered by Coraza with optional OWASP Core Rule Set (SQLi, XSS, LFI, RCE). Per-host enable/disable, global and per-host rule suppression, custom SecLang directives, and a searchable event log with severity and blocked/detected classification - **WAF** - Web Application Firewall powered by Coraza with optional OWASP Core Rule Set (SQLi, XSS, LFI, RCE). Per-host enable/disable, global and per-host rule suppression, custom SecLang directives, and a searchable event log with severity and blocked/detected classification
- **Analytics** - Live traffic charts, protocol breakdown, country map, top user agents, and blocked request log with configurable time ranges - **Analytics** - Live traffic charts, protocol breakdown, country map, top user agents, and blocked request log with configurable time ranges
- **Geo Blocking** - Block or allow traffic by country, continent, ASN, CIDR range, or exact IP per proxy host. Allow rules override block rules. Fail-closed mode, custom response codes/bodies, and trusted proxy support - **Geo Blocking** - Block or allow traffic by country, continent, ASN, CIDR range, or exact IP per proxy host. Allow rules override block rules. Fail-closed mode, custom response codes/bodies, and trusted proxy support
@@ -55,7 +55,8 @@ Data persists in Docker volumes (caddy-manager-data, caddy-data, caddy-config, c
- **API Tokens** - Create and manage API tokens with optional expiration for programmatic access - **API Tokens** - Create and manage API tokens with optional expiration for programmatic access
- **Instance Sync** - Master/slave configuration sync for multi-instance deployments. The master pushes proxy hosts, certificates, access lists, and settings to slaves on every change - **Instance Sync** - Master/slave configuration sync for multi-instance deployments. The master pushes proxy hosts, certificates, access lists, and settings to slaves on every change
- **OAuth / SSO** - OAuth2/OIDC authentication with any compliant provider (Authentik, Keycloak, Auth0, etc.). Account linking from the Profile page - **OAuth / SSO** - OAuth2/OIDC authentication with any compliant provider (Authentik, Keycloak, Auth0, etc.). Account linking from the Profile page
- **Settings** - ACME email, Cloudflare DNS-01, upstream DNS pinning defaults, Authentik outpost, Prometheus metrics, logging format - **DNS Providers** - Multi-provider DNS-01 challenge support for ACME certificates: Cloudflare, Route 53, DigitalOcean, Duck DNS, Hetzner, Vultr, Porkbun, GoDaddy, Namecheap, OVH, IONOS, and Linode. Credentials encrypted at rest. Per-certificate provider override supported
- **Settings** - ACME email, DNS provider configuration, upstream DNS pinning defaults, Authentik outpost, Prometheus metrics, logging format
- **Audit Log** - Searchable configuration change history with user attribution and pagination - **Audit Log** - Searchable configuration change history with user attribution and pagination
- **Search & Pagination** - Server-side search and pagination on all data tables - **Search & Pagination** - Server-side search and pagination on all data tables
- **Dark Mode** - Full dark/light theme support with system preference detection - **Dark Mode** - Full dark/light theme support with system preference detection
@@ -158,7 +159,7 @@ New users default to the **user** role. The initial admin account is created fro
Caddy automatically obtains Let's Encrypt certificates for all proxy hosts. Caddy automatically obtains Let's Encrypt certificates for all proxy hosts.
**Cloudflare DNS-01** (optional): Configure in Settings with a Cloudflare API token (`Zone.DNS:Edit` permissions). **DNS-01 Challenge** (optional): Configure a DNS provider in **Settings → DNS Providers** for wildcard certificates and environments where ports 80/443 are not public. Supported providers: Cloudflare, Route 53, DigitalOcean, Duck DNS, Hetzner, Vultr, Porkbun, GoDaddy, Namecheap, OVH, IONOS, and Linode. Credentials are encrypted at rest with AES-256-GCM. You can override the DNS provider per certificate.
**Custom Certificates** (optional): Import your own certificates via the Certificates page. Private keys are stored unencrypted in SQLite. **Custom Certificates** (optional): Import your own certificates via the Certificates page. Private keys are stored unencrypted in SQLite.
@@ -325,8 +326,6 @@ Each forward-auth-protected host has its own access list of allowed users and/or
## Roadmap ## Roadmap
- [ ] Additional DNS providers (Route53, Namecheap, etc.)
[Open an issue](https://github.com/fuomag9/caddy-proxy-manager/issues) for feature requests. [Open an issue](https://github.com/fuomag9/caddy-proxy-manager/issues) for feature requests.
--- ---

0
SECURITY.md Normal file → Executable file
View File

0
app/(auth)/link-account/LinkAccountClient.tsx Normal file → Executable file
View File

0
app/(auth)/link-account/page.tsx Normal file → Executable file
View File

0
app/(auth)/login/LoginClient.tsx Normal file → Executable file
View File

0
app/(auth)/login/page.tsx Normal file → Executable file
View File

0
app/(auth)/portal/PortalLoginForm.tsx Normal file → Executable file
View File

0
app/(auth)/portal/page.tsx Normal file → Executable file
View File

0
app/(dashboard)/DashboardLayoutClient.tsx Normal file → Executable file
View File

0
app/(dashboard)/OverviewClient.tsx Normal file → Executable file
View File

0
app/(dashboard)/access-lists/AccessListsClient.tsx Normal file → Executable file
View File

0
app/(dashboard)/access-lists/actions.ts Normal file → Executable file
View File

0
app/(dashboard)/access-lists/page.tsx Normal file → Executable file
View File

0
app/(dashboard)/analytics/AnalyticsClient.tsx Normal file → Executable file
View File

0
app/(dashboard)/analytics/WorldMapInner.tsx Normal file → Executable file
View File

0
app/(dashboard)/analytics/page.tsx Normal file → Executable file
View File

0
app/(dashboard)/api-docs/ApiDocsClient.tsx Normal file → Executable file
View File

0
app/(dashboard)/api-docs/page.tsx Normal file → Executable file
View File

0
app/(dashboard)/api-tokens/actions.ts Normal file → Executable file
View File

0
app/(dashboard)/audit-log/AuditLogClient.tsx Normal file → Executable file
View File

0
app/(dashboard)/audit-log/page.tsx Normal file → Executable file
View File

0
app/(dashboard)/certificates/CertificatesClient.tsx Normal file → Executable file
View File

0
app/(dashboard)/certificates/actions.ts Normal file → Executable file
View File

0
app/(dashboard)/certificates/ca-actions.ts Normal file → Executable file
View File

0
app/(dashboard)/certificates/components/AcmeTab.tsx Normal file → Executable file
View File

View File

0
app/(dashboard)/certificates/components/CaTab.tsx Normal file → Executable file
View File

View File

View File

View File

View File

79
app/(dashboard)/certificates/page.tsx Normal file → Executable file
View File

@@ -7,6 +7,7 @@ import CertificatesClient from './CertificatesClient';
import { listCaCertificates, type CaCertificate } from '@/src/lib/models/ca-certificates'; import { listCaCertificates, type CaCertificate } from '@/src/lib/models/ca-certificates';
import { listIssuedClientCertificates, type IssuedClientCertificate } from '@/src/lib/models/issued-client-certificates'; import { listIssuedClientCertificates, type IssuedClientCertificate } from '@/src/lib/models/issued-client-certificates';
import { listMtlsRoles, type MtlsRole } from '@/src/lib/models/mtls-roles'; import { listMtlsRoles, type MtlsRole } from '@/src/lib/models/mtls-roles';
import { isDomainCoveredByCert } from '@/src/lib/cert-domain-match';
export type { CaCertificate }; export type { CaCertificate };
export type { IssuedClientCertificate }; export type { IssuedClientCertificate };
@@ -78,6 +79,7 @@ function getExpiryStatus(validToIso: string): CertExpiryStatus {
return 'ok'; return 'ok';
} }
export default async function CertificatesPage({ searchParams }: PageProps) { export default async function CertificatesPage({ searchParams }: PageProps) {
await requireAdmin(); await requireAdmin();
const { page: pageParam } = await searchParams; const { page: pageParam } = await searchParams;
@@ -89,7 +91,7 @@ export default async function CertificatesPage({ searchParams }: PageProps) {
]); ]);
const mtlsRoles = await listMtlsRoles().catch(() => []); const mtlsRoles = await listMtlsRoles().catch(() => []);
const [acmeRows, acmeTotal, certRows, usageRows] = await Promise.all([ const [allAcmeRows, certRows, usageRows] = await Promise.all([
db db
.select({ .select({
id: proxyHosts.id, id: proxyHosts.id,
@@ -100,14 +102,7 @@ export default async function CertificatesPage({ searchParams }: PageProps) {
}) })
.from(proxyHosts) .from(proxyHosts)
.where(isNull(proxyHosts.certificateId)) .where(isNull(proxyHosts.certificateId))
.orderBy(proxyHosts.name) .orderBy(proxyHosts.name),
.limit(PER_PAGE)
.offset(offset),
db
.select({ value: count() })
.from(proxyHosts)
.where(isNull(proxyHosts.certificateId))
.then(([r]) => r?.value ?? 0),
db.select().from(certificates), db.select().from(certificates),
db db
.select({ .select({
@@ -120,7 +115,7 @@ export default async function CertificatesPage({ searchParams }: PageProps) {
.where(isNotNull(proxyHosts.certificateId)), .where(isNotNull(proxyHosts.certificateId)),
]); ]);
const acmeHosts: AcmeHost[] = acmeRows.map(r => ({ const allAcmeHosts: AcmeHost[] = allAcmeRows.map(r => ({
id: r.id, id: r.id,
name: r.name, name: r.name,
domains: JSON.parse(r.domains) as string[], domains: JSON.parse(r.domains) as string[],
@@ -140,6 +135,66 @@ export default async function CertificatesPage({ searchParams }: PageProps) {
usageMap.set(u.certId, hosts); usageMap.set(u.certId, hosts);
} }
// Build a map of cert ID -> its domain list (including wildcard entries)
const certDomainMap = new Map<number, string[]>();
for (const cert of certRows) {
const domainNames = JSON.parse(cert.domainNames) as string[];
// For imported certs, also check PEM SANs which may include wildcards
if (cert.type === 'imported' && cert.certificatePem) {
const pemInfo = parsePemInfo(cert.certificatePem);
if (pemInfo?.sanDomains.length) {
certDomainMap.set(cert.id, pemInfo.sanDomains);
continue;
}
}
certDomainMap.set(cert.id, domainNames);
}
// Filter out ACME hosts whose domains are fully covered by an existing certificate's wildcard,
// and attribute them to that certificate's usedBy list instead.
const filteredAcmeHosts: AcmeHost[] = [];
for (const host of allAcmeHosts) {
let coveredByCertId: number | null = null;
for (const [certId, certDomains] of certDomainMap) {
if (host.domains.every(d => isDomainCoveredByCert(d, certDomains))) {
coveredByCertId = certId;
break;
}
}
if (coveredByCertId !== null) {
// Move this host to the cert's usedBy list
const hosts = usageMap.get(coveredByCertId) ?? [];
hosts.push({ id: host.id, name: host.name, domains: host.domains });
usageMap.set(coveredByCertId, hosts);
} else {
filteredAcmeHosts.push(host);
}
}
// Among ACME auto-managed hosts, collapse subdomain hosts under wildcard hosts.
// e.g. if *.domain.de is an ACME host, sub.domain.de should not appear separately.
const wildcardAcmeHosts = filteredAcmeHosts.filter(h => h.domains.some(d => d.startsWith('*.')));
const wildcardDomainSets = wildcardAcmeHosts.map(h => h.domains);
const deduplicatedAcmeHosts: AcmeHost[] = [];
for (const host of filteredAcmeHosts) {
// Never collapse a host that itself has a wildcard domain
if (host.domains.some(d => d.startsWith('*.'))) {
deduplicatedAcmeHosts.push(host);
continue;
}
// Check if all of this host's domains are covered by any wildcard ACME host
const coveredByWildcard = wildcardDomainSets.some(wcDomains =>
host.domains.every(d => isDomainCoveredByCert(d, wcDomains))
);
if (!coveredByWildcard) {
deduplicatedAcmeHosts.push(host);
}
}
// Paginate the deduplicated ACME hosts
const adjustedAcmeTotal = deduplicatedAcmeHosts.length;
const paginatedAcmeHosts = deduplicatedAcmeHosts.slice(offset, offset + PER_PAGE);
const importedCerts: ImportedCertView[] = []; const importedCerts: ImportedCertView[] = [];
const managedCerts: ManagedCertView[] = []; const managedCerts: ManagedCertView[] = [];
const issuedByCa = issuedClientCerts.reduce<Map<number, IssuedClientCertificate[]>>((map, cert) => { const issuedByCa = issuedClientCerts.reduce<Map<number, IssuedClientCertificate[]>>((map, cert) => {
@@ -174,11 +229,11 @@ export default async function CertificatesPage({ searchParams }: PageProps) {
return ( return (
<CertificatesClient <CertificatesClient
acmeHosts={acmeHosts} acmeHosts={paginatedAcmeHosts}
importedCerts={importedCerts} importedCerts={importedCerts}
managedCerts={managedCerts} managedCerts={managedCerts}
caCertificates={caCertificateViews} caCertificates={caCertificateViews}
acmePagination={{ total: acmeTotal, page, perPage: PER_PAGE }} acmePagination={{ total: adjustedAcmeTotal, page, perPage: PER_PAGE }}
mtlsRoles={mtlsRoles} mtlsRoles={mtlsRoles}
issuedClientCerts={issuedClientCerts} issuedClientCerts={issuedClientCerts}
/> />

0
app/(dashboard)/groups/GroupsClient.tsx Normal file → Executable file
View File

0
app/(dashboard)/groups/actions.ts Normal file → Executable file
View File

0
app/(dashboard)/groups/page.tsx Normal file → Executable file
View File

0
app/(dashboard)/l4-proxy-hosts/L4ProxyHostsClient.tsx Normal file → Executable file
View File

0
app/(dashboard)/l4-proxy-hosts/actions.ts Normal file → Executable file
View File

0
app/(dashboard)/l4-proxy-hosts/page.tsx Normal file → Executable file
View File

0
app/(dashboard)/layout.tsx Normal file → Executable file
View File

41
app/(dashboard)/page.tsx Normal file → Executable file
View File

@@ -11,6 +11,7 @@ import { count, desc, isNull, sql } from "drizzle-orm";
import { ArrowLeftRight, ShieldCheck, KeyRound } from "lucide-react"; import { ArrowLeftRight, ShieldCheck, KeyRound } from "lucide-react";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { getAnalyticsSummary } from "@/src/lib/analytics-db"; import { getAnalyticsSummary } from "@/src/lib/analytics-db";
import { isDomainCoveredByCert } from "@/src/lib/cert-domain-match";
type StatCard = { type StatCard = {
label: string; label: string;
@@ -20,19 +21,51 @@ type StatCard = {
}; };
async function loadStats(): Promise<StatCard[]> { async function loadStats(): Promise<StatCard[]> {
const [proxyHostCountResult, acmeCertCountResult, importedCertCountResult, accessListCountResult] = const [proxyHostCountResult, acmeRows, certRows, importedCertCountResult, accessListCountResult] =
await Promise.all([ await Promise.all([
db.select({ value: count() }).from(proxyHosts), db.select({ value: count() }).from(proxyHosts),
// Proxy hosts with no explicit cert → Caddy auto-issues one ACME cert per host // All proxy hosts with no explicit cert (for ACME deduplication)
db.select({ value: count() }).from(proxyHosts).where(isNull(proxyHosts.certificateId)), db.select({ domains: proxyHosts.domains }).from(proxyHosts).where(isNull(proxyHosts.certificateId)),
// All certs (for wildcard coverage check)
db.select({ id: certificates.id, type: certificates.type, domainNames: certificates.domainNames, certificatePem: certificates.certificatePem }).from(certificates),
// Imported certs with actual PEM data (valid, user-managed) // Imported certs with actual PEM data (valid, user-managed)
db.select({ value: count() }).from(certificates).where( db.select({ value: count() }).from(certificates).where(
sql`${certificates.type} = 'imported' AND ${certificates.certificatePem} IS NOT NULL` sql`${certificates.type} = 'imported' AND ${certificates.certificatePem} IS NOT NULL`
), ),
db.select({ value: count() }).from(accessLists) db.select({ value: count() }).from(accessLists)
]); ]);
// Build cert domain map for wildcard coverage checks
const certDomainMap = new Map<number, string[]>();
for (const cert of certRows) {
certDomainMap.set(cert.id, JSON.parse(cert.domainNames) as string[]);
}
// Deduplicate ACME hosts: remove those covered by a cert's wildcard or another ACME wildcard
const acmeHostDomains = acmeRows.map(r => JSON.parse(r.domains) as string[]);
const wildcardAcmeDomainSets = acmeHostDomains.filter(domains => domains.some((d: string) => d.startsWith('*.')));
let acmeCount = 0;
for (const domains of acmeHostDomains) {
// Check if covered by an existing certificate's wildcard
let covered = false;
for (const [, certDomains] of certDomainMap) {
if (domains.every((d: string) => isDomainCoveredByCert(d, certDomains))) {
covered = true;
break;
}
}
// Check if this non-wildcard host is covered by a wildcard ACME host
if (!covered && !domains.some((d: string) => d.startsWith('*.'))) {
covered = wildcardAcmeDomainSets.some(wcDomains =>
domains.every((d: string) => isDomainCoveredByCert(d, wcDomains))
);
}
if (!covered) acmeCount++;
}
const proxyHostsCount = proxyHostCountResult[0]?.value ?? 0; const proxyHostsCount = proxyHostCountResult[0]?.value ?? 0;
const certificatesCount = (acmeCertCountResult[0]?.value ?? 0) + (importedCertCountResult[0]?.value ?? 0); const certificatesCount = acmeCount + (importedCertCountResult[0]?.value ?? 0);
const accessListsCount = accessListCountResult[0]?.value ?? 0; const accessListsCount = accessListCountResult[0]?.value ?? 0;
return [ return [

0
app/(dashboard)/profile/ProfileClient.tsx Normal file → Executable file
View File

0
app/(dashboard)/profile/page.tsx Normal file → Executable file
View File

0
app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx Normal file → Executable file
View File

0
app/(dashboard)/proxy-hosts/actions.ts Normal file → Executable file
View File

0
app/(dashboard)/proxy-hosts/page.tsx Normal file → Executable file
View File

0
app/(dashboard)/settings/OAuthProvidersSection.tsx Normal file → Executable file
View File

0
app/(dashboard)/settings/SettingsClient.tsx Normal file → Executable file
View File

0
app/(dashboard)/settings/actions.ts Normal file → Executable file
View File

0
app/(dashboard)/settings/page.tsx Normal file → Executable file
View File

0
app/(dashboard)/users/UsersClient.tsx Normal file → Executable file
View File

0
app/(dashboard)/users/actions.ts Normal file → Executable file
View File

0
app/(dashboard)/users/page.tsx Normal file → Executable file
View File

0
app/(dashboard)/waf/WafEventsClient.tsx Normal file → Executable file
View File

0
app/(dashboard)/waf/page.tsx Normal file → Executable file
View File

0
app/api/analytics/blocked/route.ts Normal file → Executable file
View File

0
app/api/analytics/countries/route.ts Normal file → Executable file
View File

0
app/api/analytics/hosts/route.ts Normal file → Executable file
View File

0
app/api/analytics/protocols/route.ts Normal file → Executable file
View File

0
app/api/analytics/summary/route.ts Normal file → Executable file
View File

0
app/api/analytics/timeline/route.ts Normal file → Executable file
View File

0
app/api/analytics/user-agents/route.ts Normal file → Executable file
View File

0
app/api/analytics/waf-stats/route.ts Normal file → Executable file
View File

0
app/api/auth/[...all]/route.ts Normal file → Executable file
View File

0
app/api/auth/link-account/route.ts Normal file → Executable file
View File

3
app/api/auth/logout/route.ts Normal file → Executable file
View File

@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { getAuth } from "@/src/lib/auth-server"; import { getAuth } from "@/src/lib/auth-server";
import { checkSameOrigin } from "@/src/lib/auth"; import { checkSameOrigin } from "@/src/lib/auth";
import { config } from "@/src/lib/config";
import { headers } from "next/headers"; import { headers } from "next/headers";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -10,5 +11,5 @@ export async function POST(request: NextRequest) {
if (originCheck) return originCheck; if (originCheck) return originCheck;
await getAuth().api.signOut({ headers: await headers() }); await getAuth().api.signOut({ headers: await headers() });
return NextResponse.redirect(new URL("/login", request.url)); return NextResponse.redirect(new URL("/login", config.baseUrl));
} }

0
app/api/forward-auth/callback/route.ts Normal file → Executable file
View File

0
app/api/forward-auth/login/route.ts Normal file → Executable file
View File

0
app/api/forward-auth/session-login/route.ts Normal file → Executable file
View File

0
app/api/forward-auth/verify/route.ts Normal file → Executable file
View File

0
app/api/geoip-status/route.ts Normal file → Executable file
View File

0
app/api/health/route.ts Normal file → Executable file
View File

0
app/api/instances/sync/route.ts Normal file → Executable file
View File

0
app/api/l4-ports/route.ts Normal file → Executable file
View File

0
app/api/user/change-password/route.ts Normal file → Executable file
View File

0
app/api/user/link-oauth-start/route.ts Normal file → Executable file
View File

0
app/api/user/unlink-oauth/route.ts Normal file → Executable file
View File

0
app/api/user/update-avatar/route.ts Normal file → Executable file
View File

View File

0
app/api/v1/access-lists/[id]/entries/route.ts Normal file → Executable file
View File

0
app/api/v1/access-lists/[id]/route.ts Normal file → Executable file
View File

0
app/api/v1/access-lists/route.ts Normal file → Executable file
View File

0
app/api/v1/audit-log/route.ts Normal file → Executable file
View File

0
app/api/v1/ca-certificates/[id]/route.ts Normal file → Executable file
View File

0
app/api/v1/ca-certificates/route.ts Normal file → Executable file
View File

0
app/api/v1/caddy/apply/route.ts Normal file → Executable file
View File

Some files were not shown because too many files have changed in this diff Show More