Commit Graph

12 Commits

Author SHA1 Message Date
wmantly d1dd40d60a chore(release): public-release readiness fixes for 1.1.16
- Fix MIT LICENSE copyright placeholder
- Remove private flag and correct GitHub repository URL in package.json
- Bump version to 1.1.16
- Genericize committed config defaults (example.com/localhost)
- Harden global error handler against information leakage
- Generate random initial password for proxyadmin2 bootstrap account
- Correct docs to describe CONF_SECRETS instead of symlink behavior

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 22:02:27 -04:00
wmantly fd71485960 White-label: title/logo now driven by conf (closes #45)
<title>, the navbar brand text, and the logo were all hardcoded
"Proxy - Theta 42"/"Dynamic Proxy". New conf.name/conf.logo keys
(defaults matching current text/asset) thread through the existing
values object pattern in routes/render.js and routes/docs.js, and
top.ejs now renders <%- name %>/<%- logo %> for the title and a new
navbar logo image.

Footer (copyright, theta42.com link, GitHub/license links) and the
existing favicon.svg are left as-is -- open-source attribution and a
distinct, already-working icon asset, not deployment branding.
2026-07-16 17:35:49 -04:00
wmantly 1df9916c91 Add standalone backup script and admin update-check banner
ops/backup.sh snapshots Redis (BGSAVE, dynamic RDB path lookup) and
./config for standalone deployments, with retention. A background
service polls GitHub releases every 24h and surfaces an admin-only
banner in the UI when a newer version is published.
2026-07-15 22:32:31 -04:00
wmantly 87c0d024d5 Per-host SSO: Node auth endpoints + Redis session (#57)
Adds the /__proxy_auth OIDC flow served on every proxied host:
- routes/host_auth.js: /start (PKCE+state, per-host redirect_uri), /callback
  (exchange, enforce the host allow-list via utils/host_sso.identityAllowed,
  mint session + set __proxy_sso cookie), /logout.
- models/sso_session.js: SsoSession (Redis-backed, TTL'd; read directly by the
  Lua gate) and HostSsoState (in-flight auth request).
- utils/oidc.js: per-host redirect_uri override on buildAuthUrl/exchangeCode.
- conf.hostSso (reuses conf.oidc). Allow-list logic unit-tested.

Enforcement (Lua gate + nginx location) lands next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 15:41:28 -04:00
wmantly a2d194f855 Add dynamic DNS: keep A records pointed at the current public IP
For deployments on WAN DHCP, operators can declare A records in the DNS section
that the app updates to this box's current public IP every 4 hours (and
immediately on create).

- utils/public_ip.js: getPublicIp() queries external echo services (ipify +
  fallbacks, configurable) with pure isIPv4/extractIp helpers.
- utils/dns_records.js: pure planARecordUpdate() reconciliation decision.
- models/dns_provider.js: Domain.upsertARecord(name, ip) — provider-agnostic
  upsert via getRecords + deleteRecordById + createRecord (createRecord alone is
  not a reliable cross-provider upsert). Apex ('@') handling added to each
  provider (CloudFlare uses the domain name, Porkbun an empty name, DigitalOcean
  '@') via a new DnsApi.apexName().
- models/dynamic_record.js: DynamicRecord model (deterministic id per host,
  apply()/refreshAll()), registered + ModelPs-wrapped for live UI updates.
- services/dynamic_dns.js + conf: 4h scheduler mirroring host_scheduler.
- routes/dns.js: /dynamic CRUD + /dynamic/ip, gated to domain managers/admins.
- views/dns.ejs: "Dynamic A Records (WAN IP)" card with add form + list.
- test/unit/dynamic_record.test.js: public-IP parsing + reconciliation logic.

Verified end-to-end against a live Porkbun domain (create, idempotent, IP-change,
cleanup) plus unit suite (111 pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 23:55:56 -04:00
wmantly 10abd36340 Add OIDC login and per-domain authorization
Authentication previously implied full authorization: any valid token
could manage every host, DNS provider, domain, and user. This adds SSO
login and a per-domain rights model.

OIDC login (authorization_code + PKCE):
- conf.oidc + conf.auth blocks; clientSecret in (gitignored) secrets.js.
- utils/oidc.js (state/PKCE, code exchange, userinfo) using global fetch.
- models/oidc_state.js: short-lived state store, auto-expiring via
  model-redis 1.5 per-key TTL.
- routes/auth.js: GET /auth/oidc/start + /auth/oidc/callback; JIT-provisions
  a local user, mints an AuthToken carrying the SSO groups, hands the token to
  the browser via a URL fragment. "Log in with SSO" button on the login page.

Authorization (groups + app overrides, per-domain, with ownership):
- models/grant.js + utils/roles.js (pure, unit-tested): effective rights from
  conf.auth (admin users/groups, group->role map), Grant records
  (user|group -> global|domain -> viewer|manager|admin), and ownership
  (created_by). Roles rank admin > manager(owner) > viewer.
- AuthToken stores session groups; middleware/auth.js exposes req.groups.
- middleware/authz.js: requireAdmin, requireDomainRole(minRole, resolveDomain),
  filterViewable. Applied across routes: host mutations need manager on the
  host's domain; reads are filtered to visible domains; DNS providers, user
  management, and grant management are global-admin-only; certs need viewer.
- routes/grant.js: admin CRUD for grants. Anti-lockout via conf.auth.adminUsers
  plus migrations/grant_bootstrap.js.

Frontend: /me returns effective rights; nav gates Users/Grants to admins;
grants management page; OIDC token-fragment handling in app-base.js.

Tests: utils/roles and utils/oidc unit-tested (no redis); wired into the test
scripts. Full suite 89 pass. Also verified end-to-end against redis (grant
resolution, middleware allow/deny/403, list filtering) and the OIDC pure flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 12:17:05 -04:00
wmantly 7cc7f95019 Add TTL-based expiry for wildcard subdomain cache entries
Adopt model-redis v1.5.0 and give the on-demand is_cache Host records
(and their Cached tracking records) created by Host.addCache a TTL, so
they auto-expire instead of accumulating forever. Only the record hash
carries the TTL, so OpenResty's direct HGETALL sees a miss once it
expires and re-resolves through the lookup path.

The lifetime is configurable via conf.cacheTTL (seconds, default 3600;
0 disables expiry). This also mitigates the matchAny=false "wrong host"
bug: stale leftover cache entries now expire (and are still busted on
parent update), so undefined subdomains stop being served by old caches.

Add test/unit/wildcard_matchany.test.js covering the matchAny routing
decision (defined vs undefined subdomains, mixed-policy sibling
wildcards, and cache-entry behavior) and wire it into the test scripts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:51:06 -04:00
wmantly 585aa14c0b services 2026-02-25 19:17:25 -05:00
wmantly 6cb2880131 Using the simpleworks conf now 2025-12-31 19:28:26 -05:00
wmantly 99ab768f45 fixed prefix issue 2023-08-17 13:26:47 -04:00
wmantly 7053536353 service for complex look up 2020-12-22 20:20:15 -05:00
wmantly 607a5f33af ldap user 2020-05-01 17:57:25 -04:00