Same fix as sso-manager-node: formAJAX's loading indicator passed a raw
<div class="spinner-border"> string to app.messages.action, which
HTML-escapes its message by design (@simpleworkjs/frontend) -- so every
form submit briefly showed the literal markup as text instead of a
spinner. Replaced with plain text ("Saving…").
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Native confirm() blocks all further browser events on the page (found
live, mid browser-automation testing, on sso-manager-node's equivalent
secret-rotate flow -- it froze the tab). Every call site in this app was
already removed in favor of app.messages.action/confirm/toast and
app.modal.open; this static check (scans views/ and public/js|lib/js for
bare alert(/confirm(/prompt() calls) keeps a regression from shipping
unnoticed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Native confirm() blocks browser automation entirely (found live, mid
browser-test of the app.messages/app.modal adoption, on sso-manager-node's
equivalent flow). Both functions already receive btn, whose .closest('.card')
is already used for the error path, so app.messages.confirm targets the
same card.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same swap as sso-manager-node/jump-host: vendored app.util.actionMessage/
actionConfirm replaced by @simpleworkjs/frontend's app.messages.action/
confirm (real HTML-escaping, toast fallback); vendored val.js replaced by
the package's app.validate.js.
proxy's host/target/hostname validation rules (mirrored from the backend's
utils/hostname_validate.js — wildcard DNS patterns, not something other
apps need) move to public/js/app.js, registered via $.validateSettings,
since they're proxy-specific and don't belong in the shared package's
generic rule set (eq/user/password/ip).
app.api/app.auth/app.pubsub/app.socket in app-base.js are untouched, same
reasoning as sso-manager-node's PR.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The OIDC provider sends an unauthenticated authorize request through
/login/oauth/authorize?client_id=…&state=…; dropping the query there
loses the whole authorization request. The ?redirect= form is unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
formAJAX always passes the serialized form as the second argument, so a
DELETE-method form (the host/DNS delete buttons) landed its callback in
the data slot and never ran.
The login page's "reveal the card once we know you're logged out" branch
touched an element further down the same page, which threw when
isLoggedIn answered before the parser got there (it always did without a
stored token). It now runs on DOM ready.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
views/top.ejs, views/bottom.ejs and public/lib/js/app-base.js are now
byte-identical across sso-manager-node, proxy and jump-host. Everything
per-app moved into utils/ui.js, exposed to every render as `ui` via
app.locals (nav items + their group gates, footer repo/docs/ToS links,
favicon, profile/logout targets, update-banner on/off + label).
Client framework changes:
- One gating model everywhere: app-base.js reveals .group-required-<cn>
for each of the current user user/me groups. sso-manager-node sends LDAP
DNs in memberOf, the OIDC clients send CNs in groups; both normalise to
CNs, and the clients isAdmin flag becomes a synthetic `admin` group, so
proxy nav-admin items are now group-required-admin.
- user/me is fetched once per page load and cached (app.auth.loadUser);
nav, forceLogin and group-required elements all read that one promise.
- isLoggedIn is dual-mode (Promise + node-style callback), so the async
and callback call styles both work from one shared top.ejs.
- forceLogin no longer uses $.holdReady (removed in jQuery 4): it redirects
to /login?redirect=<path>, and still enforces required groups.
- logOut only clears the session; the caller decides where to go next.
- post/put/delete are dual-mode Promise/callback, which also removes the
undefined `callback2` reference that threw on a non-function callback.
Dependencies: jquery ^4.0.0 and ejs ^3.1.10 in all three apps.
proxy specifics:
- .group-required base rule added to styles.css; the admin nav items lost
their inline display:none in favour of it.
- The brand link points at / instead of #.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rewire onto the shared @simpleworkjs/oidc-client, /ldap, and /app-stack
packages (deleting the byte-identical local forks of the same code), close the
LDAP filter-injection in User.get by routing the username through escapeFilter
(RFC 4515), align model-redis ^1.6.0 and ldapts ^8.1.8, and unify build_info to
{buildVersion, buildHash, buildYear}. package-lock regenerated from the npm
registry (no file:/link:), so npm ci is clean in docker builds.
Co-Authored-By: Claude <noreply@anthropic.com>
Every request to a host with additional load-balancing targets 500'd:
targetinfo.lua required 'resty.balancer.round_robin', which does not
exist in the lua-resty-balancer rock actually installed by the
Dockerfile/install.sh. That rock provides resty.roundrobin instead,
with a different constructor (roundrobin:new(nodes), not
:new() + :reinit(nodes)).
Verified end-to-end in a rebuilt image: requests to a load-balanced
host now return 200 and alternate across both backend targets, with
no Lua errors in the OpenResty log.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The load-balancing feature (merged in #172/#173) was tagged v1.2.0 on
GitHub, but nodejs/package.json and CHANGELOG.md were never bumped for
it. Backfill a 1.2.0 CHANGELOG entry and bump this release to 1.2.1
instead of 1.1.18, so package.json/CHANGELOG stay ahead of the highest
existing tag rather than behind it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
models/user_redis.js hardcoded the bootstrap anti-lockout admin
username to 'proxyadmin2', while migrations/permission_bootstrap.js
grants the global-admin permission to conf.auth.adminUsers[0]. An
operator who customized adminUsers away from the default ended up
with a bootstrapped account that had no admin permissions -- a
silent lockout. user_redis.js now derives the bootstrap username the
same way permission_bootstrap.js does.
Also corrected a secrets.js.example comment that claimed the
bootstrap password defaults to the username itself (it actually
generates and logs a random password), and refreshed all README
screenshots against the current UI, including a new load-balancing
screenshot.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes#47.
- Added lua-resty-balancer to dependencies (Dockerfile & install.sh).
- Added 'targets' field to the Host model to hold additional targets.
- Updated the UI to allow inputting additional targets (IP:port).
- Updated targetinfo.lua to parse the additional targets and load balance between them and the primary target using resty.balancer.round_robin.
Move the wildcard sibling-parent fix into its own 1.1.17 section (it is not
in the v1.1.16 tag) and bump nodejs/package.json + lockfile to 1.1.17.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Host.lookUpWildcardParent() walked all labels of the host down to its own
leaf and only inspected that leaf's "*" child, so it found a wildcard
nested under the host (the base-domain case, e.g. *.cool.mysite.com for
cool.mysite.com) but missed the common case where the wildcard is a
SIBLING of the host's leftmost label (e.g. *.nl.wgnode.com covering an
already-existing sso.nl.wgnode.com). The /wildcard-parent route then
returned nothing and the edit form's "Parent Wildcard" option stayed
greyed out, leaving no way to convert an existing auto-SSL host onto a
wildcard issued afterward.
Track the parent node during the walk and check the sibling "*" slot too.
The never-created-subdomain case is unchanged (plain lookUp()'s wildcard
fallback in the route still handles it). Sync the test mock and add
regression coverage for the sibling case (existing host, never-created
host via the route fallback, and a deeper-wildcard negative case).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Replace isomorphic-dompurify with xss to avoid ESM-only jsdom/htmlparser2
transitive deps that break the existing Jest test suite.
- Keep rendered docs HTML sanitized via xss() in routes/docs.js.
Co-Authored-By: Claude <noreply@anthropic.com>
- Add isomorphic-dompurify to sanitize rendered docs HTML
- Tighten SocketServerJson socket permissions from 777 to 660
- Keep package.json version at 1.1.16
Co-Authored-By: Claude <noreply@anthropic.com>
- 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>
conf 1.2.0 adds CONF_SECRETS, an env var to point at the secrets file
directly -- use it in the Docker entrypoint instead of symlinking the
mounted file into /app/conf/secrets.js, so the app no longer needs
write access to its own conf/ directory to pick up mounted secrets.
jq-repeat 2.2.0 is a compatible feature release (sort(), replace(),
faster leading-edge update() timing); no call-site changes needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- New docs/concepts-{hosts,dns,access,api-tokens}.md -- plain-language
guides aimed at less technical readers, each linking onward to the
existing system-design-level doc for anyone who wants that detail.
Card help links (Proxy List, Add/Edit host, DNS Provider cards,
Users/Permissions/Groups cards) now point here instead of straight at
Installation/Architecture.
- The "New API Token" card had no help link at all -- added, pointing to
the new API Tokens doc.
- Fixed the in-app docs viewer rendering every docs/*.md page with a
garbled heading + stray <hr> at the top: Jekyll front matter (meant
only for the GitHub Pages build) was never stripped before being
handed to the markdown renderer.
- Fixed cross-doc links never resolving in-app, since this viewer serves
docs at /docs/<slug> with no .html suffix: rewritten to the correct
in-app URL, first by registered slug, falling back to the doc's real
filename (the correct, working link form on the Jekyll/GitHub Pages
build) -- same idea as the existing image-path fix, and lets one link
written in a doc work on both targets.
Bumps to v1.1.13.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
- The edit form's "Parent Wildcard" option stayed greyed out even when a
valid wildcard existed, since hostEditOpen() never ran the eligibility
check (only the host field's keyup handler did, which setting .val()
programmatically doesn't fire) -- and the check itself, GET
/host/lookup/:item, had the same self-match bug as the recently-fixed
Host.prototype.update() case: it resolves an already-existing host to
its own record instead of a sibling wildcard. Added a dedicated
/host/wildcard-parent/:item route combining lookUp() (handles a
brand-new subdomain) with lookUpWildcardParent() (handles an
already-existing host), and hostEditOpen() now actually runs it.
- Migrated ops/nginx_conf/autossl.conf's deprecated "listen ... http2"
directive to the standalone "http2 on;" directive (nginx 1.25.1+).
Bumps to v1.1.12.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
The single header-wide help icon (added last release) pointed at a
per-page doc guess, but a page can have several cards covering different
topics. Removed it and added a small help icon directly to each card
that has real corresponding doc content, linking straight to that doc:
Proxy List + Add/Edit host modal (hosts.ejs), Add DNS Provider + Dynamic
A Records (dns.ejs), Add New User + User List (users.ejs), Add
Permission + Permissions (permissions.ejs), Add Group (groups.ejs).
Bumps to v1.1.11.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
- A ? icon in the top-right header deep-links to the doc most relevant to
the current page (client-side path mapping, same pattern already used
for top-nav active-link highlighting -- no server-side "current section"
local exists to key off of instead). Falls back to the docs index.
- GET /docs/search does a plain line-substring search over the existing
allowlisted doc set. No new dependency, stays usable with no internet
access.
Bumps to v1.1.10.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
- Plain hosts can now be renamed after creation (wildcard/child/cache hosts
stay locked, since other records reference them by name). Migrates the
cert cache key on rename.
- Along the way, found and fixed a real bug in the vendored model-redis
library: its rename path leaves a stray, incomplete hash behind under
the old key when an `always`-type field (updated_on) is defined earlier
in the schema than the primary key -- silently blocking that hostname
from ever being reused. Worked around at the Host model level (can't
patch node_modules).
- Host list now shows who created each host, and when.
- Host modal's tabs now scroll horizontally on narrow screens instead of
overflowing awkwardly.
- Added missing inline help text (Target SSL, wildcard matching behavior).
Bumps to v1.1.9.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
- Host.prototype.update() had no challengeType handling (only create() did),
so selecting "Parent Wildcard" on an existing host's edit form silently
did nothing. Added the same wildcard-parent lookup to update(), using a
new Host.lookUpWildcardParent() -- the existing lookUp() can't be reused
here since an already-created host resolves to its own leaf rather than
falling through to a sibling wildcard.
- A wildcard's issued cert covers both the base domain and *.base domain
(altNames), but the lookup tree stores the wildcard one level below its
base -- looking up the bare base domain landed on an empty parent node
and found nothing. buildLookUpObj() now also stamps that parent node,
order-independent (a real host explicitly created at that exact name
always still wins).
Verified both fixes against a real Redis-backed Host model (not just the
mocked lookup-tree tests) -- see PR description.
Bumps to v1.1.8.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
The three auth_mode radios (Off / Basic / SSO) had no shared [name]
attribute, so per the HTML spec each was its own independent group --
clicking one didn't uncheck the others, letting multiple options
appear selected at once despite the page's own text saying "basic
auth and SSO can't both be enabled."
Added name="auth_mode" to restore native browser radio-group
behavior. The original comment claimed the radios were deliberately
kept nameless to avoid polluting the submitted form data (formAJAX
serializes every [name] field in the form), but that reasoning
doesn't hold: model-redis's processKeys() rebuilds the saved object
strictly from the Host model's own _keyMap, so an unrecognized
auth_mode field is silently stripped before anything is ever
persisted -- confirmed directly with model-redis's own
object_validate.js. Updated the stale comment accordingly.
jq-repeat 2.1.0 (release notes: https://github.com/wmantly/jq-repeat/releases/tag/v2.1.0)
brings real fixes (throttled-update race conditions, sorted-list
reverse() leaking elements, nested-scope isolation) and a few
behavior changes. Audited every usage in this repo against the
changelog before upgrading:
- push()/unshift() now return the new array length -- every call
site in this repo is a bare statement, none consume the return
value. No risk.
- update() is now trailing-edge throttled (~50ms) even on the first
call, not just rapid subsequent ones -- no code in this repo reads
DOM/item state immediately after calling update(), so no risk here
(unlike sso-manager-node's companion PR, which needed a fix).
- jr-order-reverse and nested jq-repeat templates: not used anywhere
in this repo.
Real breakage found and fixed: users.ejs/groups.ejs/permissions.ejs
called $.scope.X.__setPut(fn)/__setTake(fn) as setter METHODS -- that
API is gone in 2.1.0. Insert/remove hooks are now set via direct
property assignment ($.scope.X.__put = fn), per the current README.
Verified live (real dev server + Playwright): before the fix, all
three pages threw "__setTake is not a function" and the
insert/remove row animations were broken; after, zero errors and the
hooks fire correctly.
<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.
GitHub Releases already carried real changelog notes per tag, but
those require internet access to view -- exactly what the /docs
route exists to avoid. CHANGELOG.md is a committed, Keep-a-Changelog
style file (backfilled from the v1.1.0/v1.1.1/v1.1.2 release notes),
linked from README and served at /docs/changelog alongside the rest
of the project's docs.
GitHub Releases already carried real changelog notes per tag, but
those require internet access to view -- exactly what the /docs
route exists to avoid. CHANGELOG.md is a committed, Keep-a-Changelog
style file (backfilled from the v1.1.0/v1.1.1/v1.1.2 release notes),
linked from README and served at /docs/changelog alongside the rest
of the project's docs.
Public route reading from disk on every request with no throttling
-- add a per-IP limiter matching the routes/auth.js/routes/host.js
convention already used elsewhere in this repo.
Air-gap:
- DynamicRecord.refreshAll() called getPublicIp() (api.ipify.org,
icanhazip.com, ifconfig.me) every 4h on a timer regardless of
whether any dynamic records were configured -- the one background
call in the repo not actually gated by feature use. Now skips the
lookup entirely when there's nothing to refresh.
- Removed the stray, unauthenticated GET /test page (a leftover
jq-repeat demo) that loaded jQuery + Mustache from external CDNs.
- Removed a dead IE<9-only html5shim script tag pointing at a domain
that no longer resolves.
Docs:
- New GET /docs (index) and /docs/:slug routes render this project's
own README, DEPLOYMENT, api.md, and docs/*.md server-side via
marked (new dependency) -- so the documentation is readable from
the running app with no route to GitHub Pages, where it otherwise
only lives. Public, no auth, same tier as the health endpoint.
- .dockerignore/Dockerfile updated: docs/, DEPLOYMENT.md, and
nodejs/api.md were previously excluded from the image entirely
("served via GitHub Pages, not from the image") -- now copied in
alongside README.md/tos.md-style, since they're needed at runtime.
listDomains() used to validate the token by calling DuckDNS's update
endpoint with ip/ipv6 omitted, which makes DuckDNS auto-detect and
apply this host's public IP -- so adding a provider instantly
repointed the domain. Validate via a fixed TXT marker instead, which
DuckDNS's API supports independently and doesn't touch routing.
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.
CodeQL flagged POST/PUT/DELETE /api/host* as missing rate limiting despite
performing authorization -- same authLimiter pattern routes/auth.js already
uses, applied here with a higher ceiling since legitimate admin work (bulk
edits) is expected on these routes.
CodeQL also flagged utils/basicauth.js's SHA-1 hashing as reachable from the
new basicauth-user route -- this is the existing, documented htpasswd-
compatible {SHA} scheme (see the comment on hashPassword), not something
this PR changes; left as-is per that comment's existing "follow-up" note,
since swapping it requires a coordinated change to
ops/nginx_conf/hostfeatures.lua's verification and a migration path for
already-stored hashes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>