Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c27f4e40a3 | |||
| f861b95c9b | |||
| d6bb1871dd | |||
| 12da316ace | |||
| 3fb643f41e | |||
| 25cd4669b2 | |||
| 47af056a7c | |||
| 7a91843c79 | |||
| 99819b70ff | |||
|
|
4c5ad53370 | ||
|
|
d710ad1247 | ||
|
|
6515da666f | ||
|
|
96bac86934 | ||
|
|
dbfc340ea4 | ||
|
|
521a059414 | ||
|
|
1be8fc2629 | ||
|
|
6d2827a132 | ||
|
|
eb11856994 | ||
|
|
7d61528dad | ||
|
|
92fa1cb9d8 | ||
|
|
ef62ef232f |
0
.dockerignore
Normal file → Executable file
0
.dockerignore
Normal file → Executable file
0
.env.example
Normal file → Executable file
0
.env.example
Normal file → Executable file
0
.github/FUNDING.yml
vendored
Normal file → Executable file
0
.github/FUNDING.yml
vendored
Normal file → Executable file
0
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file → Executable file
0
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file → Executable file
0
.github/ISSUE_TEMPLATE/dns_challenge_request.md
vendored
Normal file → Executable file
0
.github/ISSUE_TEMPLATE/dns_challenge_request.md
vendored
Normal file → Executable file
0
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file → Executable file
0
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file → Executable file
0
.github/dependabot.yml
vendored
Normal file → Executable file
0
.github/dependabot.yml
vendored
Normal file → Executable file
2
.github/workflows/dependabot-automerge.yml
vendored
Normal file → Executable file
2
.github/workflows/dependabot-automerge.yml
vendored
Normal file → Executable 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
0
.github/workflows/docker-build-pr.yml
vendored
Normal file → Executable file
0
.github/workflows/docker-build-trusted.yml
vendored
Normal file → Executable file
0
.github/workflows/docker-build-trusted.yml
vendored
Normal file → Executable file
0
.github/workflows/stale.yml
vendored
Normal file → Executable file
0
.github/workflows/stale.yml
vendored
Normal file → Executable file
0
.github/workflows/test.yml
vendored
Normal file → Executable file
0
.github/workflows/test.yml
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
9
README.md
Normal file → Executable file
9
README.md
Normal file → Executable 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
0
SECURITY.md
Normal file → Executable file
0
app/(auth)/link-account/LinkAccountClient.tsx
Normal file → Executable file
0
app/(auth)/link-account/LinkAccountClient.tsx
Normal file → Executable file
0
app/(auth)/link-account/page.tsx
Normal file → Executable file
0
app/(auth)/link-account/page.tsx
Normal file → Executable file
0
app/(auth)/login/LoginClient.tsx
Normal file → Executable file
0
app/(auth)/login/LoginClient.tsx
Normal file → Executable file
0
app/(auth)/login/page.tsx
Normal file → Executable file
0
app/(auth)/login/page.tsx
Normal file → Executable file
0
app/(auth)/portal/PortalLoginForm.tsx
Normal file → Executable file
0
app/(auth)/portal/PortalLoginForm.tsx
Normal file → Executable file
0
app/(auth)/portal/page.tsx
Normal file → Executable file
0
app/(auth)/portal/page.tsx
Normal file → Executable file
0
app/(dashboard)/DashboardLayoutClient.tsx
Normal file → Executable file
0
app/(dashboard)/DashboardLayoutClient.tsx
Normal file → Executable file
0
app/(dashboard)/OverviewClient.tsx
Normal file → Executable file
0
app/(dashboard)/OverviewClient.tsx
Normal file → Executable file
0
app/(dashboard)/access-lists/AccessListsClient.tsx
Normal file → Executable file
0
app/(dashboard)/access-lists/AccessListsClient.tsx
Normal file → Executable file
0
app/(dashboard)/access-lists/actions.ts
Normal file → Executable file
0
app/(dashboard)/access-lists/actions.ts
Normal file → Executable file
0
app/(dashboard)/access-lists/page.tsx
Normal file → Executable file
0
app/(dashboard)/access-lists/page.tsx
Normal file → Executable file
0
app/(dashboard)/analytics/AnalyticsClient.tsx
Normal file → Executable file
0
app/(dashboard)/analytics/AnalyticsClient.tsx
Normal file → Executable file
0
app/(dashboard)/analytics/WorldMapInner.tsx
Normal file → Executable file
0
app/(dashboard)/analytics/WorldMapInner.tsx
Normal file → Executable file
0
app/(dashboard)/analytics/page.tsx
Normal file → Executable file
0
app/(dashboard)/analytics/page.tsx
Normal file → Executable file
0
app/(dashboard)/api-docs/ApiDocsClient.tsx
Normal file → Executable file
0
app/(dashboard)/api-docs/ApiDocsClient.tsx
Normal file → Executable file
0
app/(dashboard)/api-docs/page.tsx
Normal file → Executable file
0
app/(dashboard)/api-docs/page.tsx
Normal file → Executable file
0
app/(dashboard)/api-tokens/actions.ts
Normal file → Executable file
0
app/(dashboard)/api-tokens/actions.ts
Normal file → Executable file
0
app/(dashboard)/audit-log/AuditLogClient.tsx
Normal file → Executable file
0
app/(dashboard)/audit-log/AuditLogClient.tsx
Normal file → Executable file
0
app/(dashboard)/audit-log/page.tsx
Normal file → Executable file
0
app/(dashboard)/audit-log/page.tsx
Normal file → Executable file
0
app/(dashboard)/certificates/CertificatesClient.tsx
Normal file → Executable file
0
app/(dashboard)/certificates/CertificatesClient.tsx
Normal file → Executable file
0
app/(dashboard)/certificates/actions.ts
Normal file → Executable file
0
app/(dashboard)/certificates/actions.ts
Normal file → Executable file
0
app/(dashboard)/certificates/ca-actions.ts
Normal file → Executable file
0
app/(dashboard)/certificates/ca-actions.ts
Normal file → Executable file
0
app/(dashboard)/certificates/components/AcmeTab.tsx
Normal file → Executable file
0
app/(dashboard)/certificates/components/AcmeTab.tsx
Normal file → Executable file
0
app/(dashboard)/certificates/components/CaCertDrawer.tsx
Normal file → Executable file
0
app/(dashboard)/certificates/components/CaCertDrawer.tsx
Normal file → Executable file
0
app/(dashboard)/certificates/components/CaTab.tsx
Normal file → Executable file
0
app/(dashboard)/certificates/components/CaTab.tsx
Normal file → Executable file
0
app/(dashboard)/certificates/components/ImportCertDrawer.tsx
Normal file → Executable file
0
app/(dashboard)/certificates/components/ImportCertDrawer.tsx
Normal file → Executable file
0
app/(dashboard)/certificates/components/ImportedTab.tsx
Normal file → Executable file
0
app/(dashboard)/certificates/components/ImportedTab.tsx
Normal file → Executable file
0
app/(dashboard)/certificates/components/RelativeTime.tsx
Normal file → Executable file
0
app/(dashboard)/certificates/components/RelativeTime.tsx
Normal file → Executable file
0
app/(dashboard)/certificates/components/StatusSummaryBar.tsx
Normal file → Executable file
0
app/(dashboard)/certificates/components/StatusSummaryBar.tsx
Normal file → Executable file
79
app/(dashboard)/certificates/page.tsx
Normal file → Executable file
79
app/(dashboard)/certificates/page.tsx
Normal file → Executable 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
0
app/(dashboard)/groups/GroupsClient.tsx
Normal file → Executable file
0
app/(dashboard)/groups/actions.ts
Normal file → Executable file
0
app/(dashboard)/groups/actions.ts
Normal file → Executable file
0
app/(dashboard)/groups/page.tsx
Normal file → Executable file
0
app/(dashboard)/groups/page.tsx
Normal file → Executable file
0
app/(dashboard)/l4-proxy-hosts/L4ProxyHostsClient.tsx
Normal file → Executable file
0
app/(dashboard)/l4-proxy-hosts/L4ProxyHostsClient.tsx
Normal file → Executable file
0
app/(dashboard)/l4-proxy-hosts/actions.ts
Normal file → Executable file
0
app/(dashboard)/l4-proxy-hosts/actions.ts
Normal file → Executable file
0
app/(dashboard)/l4-proxy-hosts/page.tsx
Normal file → Executable file
0
app/(dashboard)/l4-proxy-hosts/page.tsx
Normal file → Executable file
0
app/(dashboard)/layout.tsx
Normal file → Executable file
0
app/(dashboard)/layout.tsx
Normal file → Executable file
41
app/(dashboard)/page.tsx
Normal file → Executable file
41
app/(dashboard)/page.tsx
Normal file → Executable 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
0
app/(dashboard)/profile/ProfileClient.tsx
Normal file → Executable file
0
app/(dashboard)/profile/page.tsx
Normal file → Executable file
0
app/(dashboard)/profile/page.tsx
Normal file → Executable file
0
app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx
Normal file → Executable file
0
app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx
Normal file → Executable file
0
app/(dashboard)/proxy-hosts/actions.ts
Normal file → Executable file
0
app/(dashboard)/proxy-hosts/actions.ts
Normal file → Executable file
0
app/(dashboard)/proxy-hosts/page.tsx
Normal file → Executable file
0
app/(dashboard)/proxy-hosts/page.tsx
Normal file → Executable file
0
app/(dashboard)/settings/OAuthProvidersSection.tsx
Normal file → Executable file
0
app/(dashboard)/settings/OAuthProvidersSection.tsx
Normal file → Executable file
0
app/(dashboard)/settings/SettingsClient.tsx
Normal file → Executable file
0
app/(dashboard)/settings/SettingsClient.tsx
Normal file → Executable file
0
app/(dashboard)/settings/actions.ts
Normal file → Executable file
0
app/(dashboard)/settings/actions.ts
Normal file → Executable file
0
app/(dashboard)/settings/page.tsx
Normal file → Executable file
0
app/(dashboard)/settings/page.tsx
Normal file → Executable file
0
app/(dashboard)/users/UsersClient.tsx
Normal file → Executable file
0
app/(dashboard)/users/UsersClient.tsx
Normal file → Executable file
0
app/(dashboard)/users/actions.ts
Normal file → Executable file
0
app/(dashboard)/users/actions.ts
Normal file → Executable file
0
app/(dashboard)/users/page.tsx
Normal file → Executable file
0
app/(dashboard)/users/page.tsx
Normal file → Executable file
0
app/(dashboard)/waf/WafEventsClient.tsx
Normal file → Executable file
0
app/(dashboard)/waf/WafEventsClient.tsx
Normal file → Executable file
0
app/(dashboard)/waf/page.tsx
Normal file → Executable file
0
app/(dashboard)/waf/page.tsx
Normal file → Executable file
0
app/api/analytics/blocked/route.ts
Normal file → Executable file
0
app/api/analytics/blocked/route.ts
Normal file → Executable file
0
app/api/analytics/countries/route.ts
Normal file → Executable file
0
app/api/analytics/countries/route.ts
Normal file → Executable file
0
app/api/analytics/hosts/route.ts
Normal file → Executable file
0
app/api/analytics/hosts/route.ts
Normal file → Executable file
0
app/api/analytics/protocols/route.ts
Normal file → Executable file
0
app/api/analytics/protocols/route.ts
Normal file → Executable file
0
app/api/analytics/summary/route.ts
Normal file → Executable file
0
app/api/analytics/summary/route.ts
Normal file → Executable file
0
app/api/analytics/timeline/route.ts
Normal file → Executable file
0
app/api/analytics/timeline/route.ts
Normal file → Executable file
0
app/api/analytics/user-agents/route.ts
Normal file → Executable file
0
app/api/analytics/user-agents/route.ts
Normal file → Executable file
0
app/api/analytics/waf-stats/route.ts
Normal file → Executable file
0
app/api/analytics/waf-stats/route.ts
Normal file → Executable file
0
app/api/auth/[...all]/route.ts
Normal file → Executable file
0
app/api/auth/[...all]/route.ts
Normal file → Executable file
0
app/api/auth/link-account/route.ts
Normal file → Executable file
0
app/api/auth/link-account/route.ts
Normal file → Executable file
3
app/api/auth/logout/route.ts
Normal file → Executable file
3
app/api/auth/logout/route.ts
Normal file → Executable 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
0
app/api/forward-auth/callback/route.ts
Normal file → Executable file
0
app/api/forward-auth/login/route.ts
Normal file → Executable file
0
app/api/forward-auth/login/route.ts
Normal file → Executable file
0
app/api/forward-auth/session-login/route.ts
Normal file → Executable file
0
app/api/forward-auth/session-login/route.ts
Normal file → Executable file
0
app/api/forward-auth/verify/route.ts
Normal file → Executable file
0
app/api/forward-auth/verify/route.ts
Normal file → Executable file
0
app/api/geoip-status/route.ts
Normal file → Executable file
0
app/api/geoip-status/route.ts
Normal file → Executable file
0
app/api/health/route.ts
Normal file → Executable file
0
app/api/health/route.ts
Normal file → Executable file
0
app/api/instances/sync/route.ts
Normal file → Executable file
0
app/api/instances/sync/route.ts
Normal file → Executable file
0
app/api/l4-ports/route.ts
Normal file → Executable file
0
app/api/l4-ports/route.ts
Normal file → Executable file
0
app/api/user/change-password/route.ts
Normal file → Executable file
0
app/api/user/change-password/route.ts
Normal file → Executable file
0
app/api/user/link-oauth-start/route.ts
Normal file → Executable file
0
app/api/user/link-oauth-start/route.ts
Normal file → Executable file
0
app/api/user/unlink-oauth/route.ts
Normal file → Executable file
0
app/api/user/unlink-oauth/route.ts
Normal file → Executable file
0
app/api/user/update-avatar/route.ts
Normal file → Executable file
0
app/api/user/update-avatar/route.ts
Normal file → Executable file
0
app/api/v1/access-lists/[id]/entries/[entryId]/route.ts
Normal file → Executable file
0
app/api/v1/access-lists/[id]/entries/[entryId]/route.ts
Normal file → Executable file
0
app/api/v1/access-lists/[id]/entries/route.ts
Normal file → Executable file
0
app/api/v1/access-lists/[id]/entries/route.ts
Normal file → Executable file
0
app/api/v1/access-lists/[id]/route.ts
Normal file → Executable file
0
app/api/v1/access-lists/[id]/route.ts
Normal file → Executable file
0
app/api/v1/access-lists/route.ts
Normal file → Executable file
0
app/api/v1/access-lists/route.ts
Normal file → Executable file
0
app/api/v1/audit-log/route.ts
Normal file → Executable file
0
app/api/v1/audit-log/route.ts
Normal file → Executable file
0
app/api/v1/ca-certificates/[id]/route.ts
Normal file → Executable file
0
app/api/v1/ca-certificates/[id]/route.ts
Normal file → Executable file
0
app/api/v1/ca-certificates/route.ts
Normal file → Executable file
0
app/api/v1/ca-certificates/route.ts
Normal file → Executable file
0
app/api/v1/caddy/apply/route.ts
Normal file → Executable file
0
app/api/v1/caddy/apply/route.ts
Normal file → Executable file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user