Use config.oauth.providerName (e.g. "Keycloak", "Google") instead of
the raw provider ID "oauth2" in audit summaries. Include user name or
email in sign-in and sign-up messages for easier log reading.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Intermittent failure — the default 5s wasn't enough when the page
loaded slowly during a long E2E run (227/228 passed).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Traffic (24h) card and Recent Activity section were visible to
user/viewer roles even though they received empty data. Now both
sections are conditionally rendered only for admin users.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- dashboard.spec.ts: anchor regex /^\d+\s+Proxy Hosts/ to not match
"L4 Proxy Hosts" sidebar link
- role-access.spec.ts: use exact: true for "Proxy Hosts" link
- users.spec.ts: match any user count (/\d+ users?/) since other test
suites create additional users
- groups.spec.ts: remove unused emptyText variable
- link-account.spec.ts: remove unused context parameter
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- dashboard: Match stat cards via link role with count+label pattern
to avoid matching subtitle paragraph containing "certificates"
- role-access: Use Bun.password.hash (built-in bcrypt) instead of
bcryptjs which is not installed in the production container
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- api-docs: Don't rely on CDN-loaded Swagger UI class in test env
- dashboard: Use `p` locator for stat card labels to avoid matching nav
- groups: Scope add-member click to bordered container to avoid nav match
- link-account: Remove assertion on error= URL param (not always present)
- portal: Use exact:true for "Sign in" button (OAuth button also matches)
- role-access: Use ESM imports in bun -e script, use getByLabel for login
fields, increase waitForURL timeout, use exact button match
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Allow non-admin users (user/viewer) to access / and /profile while
blocking admin-only pages. The dashboard layout now uses requireUser()
instead of requireAdmin(), and the sidebar filters nav items by role.
Non-admin users see a minimal welcome page without stat cards.
New test files (86 tests across 7 files):
- dashboard, users, groups, api-docs, portal, link-account specs
- role-access spec with full RBAC coverage for all 3 roles
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove unused imports, functions, and variables flagged by
@typescript-eslint/no-unused-vars and no-useless-assignment rules.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs caused mTLS to be silently disabled when all issued client
certificates for a CA were revoked:
1. New cert-based trust model (caddy.ts): When deriving CA IDs from
trusted cert IDs, revoked certs were invisible (active-only query),
causing derivedCaIds to be empty and the domain to be dropped from
mTlsDomainMap entirely — no mTLS policy at all. Fix by falling back
to a cert-ID-to-CA-ID lookup that includes revoked certs, keeping the
domain in the map so it gets a fail-closed policy.
2. Legacy CA-based model (caddy-mtls.ts): buildClientAuthentication
returned null when all certs were revoked, relying on Caddy's
experimental "drop" TLS connection policy field which didn't work
reliably. Fix by pinning to the CA cert itself as a trusted_leaf_certs
entry — no client cert can hash-match a CA certificate (and presenting
the CA cert would require its private key, already a full compromise).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pentest found that all 8 analytics API endpoints, the GeoIP status
endpoint, and the OpenAPI spec were accessible to any authenticated
user. Since the user role should only have access to forward auth
and self-service, these are now admin-only.
- analytics/*: requireUser → requireAdmin
- geoip-status: requireUser → requireAdmin
- openapi.json: add requireApiAdmin + change Cache-Control to private
- analytics/api-docs pages: requireUser → requireAdmin (defense-in-depth)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix broken rate limiting: add registerFailedAttempt/resetAttempts calls
- Remove raw session token from exchange table; generate fresh token at redemption
- Fix TOCTOU race: atomic UPDATE...WHERE used=false for exchange redemption
- Delete exchange records immediately after redemption
- Change bcrypt.compareSync to async bcrypt.compare to prevent event loop blocking
- Fix IP extraction: prefer x-real-ip, fall back to last x-forwarded-for entry
- Restrict redirect URI scheme to http/https only
- Add Origin header CSRF check on login and session-login endpoints
- Remove admin auto-access bypass from checkHostAccess (deny-by-default for all)
- Revoke forward auth sessions when user status changes away from active
- Validate portal domain against registered forward-auth hosts to prevent phishing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New /users page with search, inline editing, role/status changes, and deletion
- Model: added updateUserRole, updateUserStatus, deleteUser functions
- API: PUT /api/v1/users/[id] now supports role and status fields, added DELETE
- Safety: cannot change own role/status or delete own account
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CPM can now act as its own forward auth provider for proxied sites.
Users authenticate at a login portal (credentials or OAuth) and Caddy
gates access via a verify subrequest, eliminating the need for external
IdPs like Authentik.
Key components:
- Forward auth flow: verify endpoint, exchange code callback, login portal
- User groups with membership management
- Per-proxy-host access control (users and/or groups)
- Caddy config generation for forward_auth handler + callback route
- OAuth and credential login on the portal page
- Admin UI: groups page, inline user/group assignment in proxy host form
- REST API: /api/v1/groups, /api/v1/forward-auth-sessions, per-host access
- Integration tests for groups and forward auth schema
Also fixes mTLS E2E test selectors broken by the RBAC refactor.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Caddy's certmagic creates storage dirs with hardcoded 0700 permissions,
making the web container's supplementary group membership ineffective.
Rather than working around this with ACLs or chmod hacks, remove the
feature entirely — it was cosmetic (issuer/expiry display) for certs
that Caddy auto-manages anyway.
Also bump access list dropdown timeout from 5s to 10s to fix flaky E2E test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Traffic (24h) card's "Blocked" percentage only counted
geo-blocks from trafficEvents. Now also queries wafEvents to
include WAF-blocked requests in the total.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The caddy-blocker plugin already emits "request blocked" log entries
for geo/IP blocks, but they were going to Caddy's default log (stdout)
instead of /logs/access.log because http.handlers.blocker was not in
the access log include list. The existing log parser and dashboard were
already wired up to count these — they just never received the data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add LocationRule schema with path and upstreams fields
- Add location_rules to ProxyHost and ProxyHostInput schemas
- Fix response_headers using concrete example instead of generic additionalProperties
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove unused `/* global process */` in next.config.mjs
- Attach cause to rethrown error in secret.ts legacy key expiry
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The H7 fix made trustHost default to false, which caused redirect loops
in environments where NEXTAUTH_URL is set (including Docker and tests).
When NEXTAUTH_URL is explicitly configured, the operator has declared
the canonical URL, making Host header validation unnecessary.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix Radix Select interaction in proxy host E2E helper: scroll trigger
into view and wait for option visibility before clicking (fixes flaky
access-control.spec.ts timeout)
- Apply same fix to certificate selector for consistency
- Remove stale eslint-disable directives from pre-existing test files
(now covered by test-wide eslint config override)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unused imports (users, and) from api-tokens model
- Fix password_hash destructure lint error in user routes
- Fix apiErrorResponse mock pattern in all 12 test files (use instanceof)
- Remove stale eslint-disable directives from test files
- Add eslint override for tests (no-explicit-any, no-require-imports)
- Fix unused vars in settings and tokens tests
- Fix unused tokenB in integration test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- API token model (SHA-256 hashed, debounced lastUsedAt) with Bearer auth
- Dual auth middleware (session + API token) in src/lib/api-auth.ts
- 23 REST endpoints under /api/v1/ covering all functionality:
tokens, proxy-hosts, l4-proxy-hosts, certificates, ca-certificates,
client-certificates, access-lists, settings, instances, users,
audit-log, caddy/apply
- OpenAPI 3.1 spec at /api/v1/openapi.json with fully typed schemas
- Swagger UI docs page at /api-docs in the dashboard
- API token management integrated into the Profile page
- Fix: next build now works under Node.js (bun:sqlite aliased to better-sqlite3)
- 89 new API route unit tests + 11 integration tests (592 total)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The l4-port-manager service had a Dockerfile but was missing from the
GitHub Actions build matrix, so it was never built or pushed to GHCR.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DataTable renders mobile (block md:hidden) and desktop (hidden md:block)
variants. .first() resolved to the hidden mobile element at desktop viewport.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>