Adds a concrete "Connecting a 3rd-party app or container" section:
a bind-parameter reference table, a worked Gitea example, a generic
Docker LDAP_* env var pattern, and a pointer to theta42/ldap-client for
full host-level (SSH/sudo/PAM) integration as opposed to a single app.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Rewrite docs/index.md as a short landing page (what it is, screenshots,
why this over the alternatives, features, a minimal "get it" snippet)
instead of a full documentation dump — full docs live in the repo
(README, docs/*.md) and are linked from here.
- Cross-link to Proxy and theta-env's own Pages sites.
- Screenshots are now clickable (open full size) on both the Pages site
and the README.
- Disable show_downloads in docs/_config.yml — the Cayman theme's
"Download .zip/.tar.gz" buttons are gone; "View on GitHub" (which links
back to the repo) is the only header link now.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Captured from a fresh theta-env install with demo data, via headless
Chrome + Playwright (scripted login, no manual UI interaction needed to
reproduce). Also adds a top-of-README Documentation link pointing at
GitHub Pages, matching theta42/proxy's README.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
theta42/proxy fronts an arbitrary number of hosts behind SSO, each with its
own callback URL (https://<host>/__proxy_auth/callback) — proxy's own code
comment already assumed "a wildcard redirect URI covers all", but no
wildcard matching existed here, so every proxied host's callback had to be
registered on the shared OAuth client individually or /oauth/authorize
would reject it with InvalidRedirectURI.
Add `*` (one hostname label) / `**` (any number of labels) wildcard support
to redirect_uri matching, e.g. `https://**.example.com/__proxy_auth/callback`
now covers every host proxy fronts under example.com. Exact matches still
work exactly as before.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reported: creating any user via the API failed with
{"name":"InvalidSyntaxError","message":"gidNumber: value #0 invalid per syntax Code: 0x15"}
Root cause: addPosixGroup() computes the next gidNumber as
`Math.max(...groups.map(i => i.gidNumber)) + 1`. theta-env's
bootstrap.js creates the first admin via raw ldapadd with a hardcoded
uidNumber/gidNumber (10000) directly on the user entry, but never
creates a matching posixGroup entry -- so on a theta-env-bootstrapped
directory there are zero posixGroup entries, `Math.max()` on an empty
array is `-Infinity` in JS (not 0), and `-Infinity + 1` stringifies to
"-Infinity" -- an invalid LDAP integer, rejected by the directory. This
broke every single user creation, not just this one.
Separately: the reporter's intended scheme is for organically-created
users to start at uidNumber/gidNumber 1500, distinct from the
bootstrap admin's reserved 10000. Fixing the crash with a bare "floor
of 1500" alone wouldn't achieve that, since addPosixAccount's own
Math.max() would still find the admin's posixAccount entry (uidNumber
10000, found via a different, correctly-indexed search) and allocate
10001 for the next user.
Added a shared nextPosixId(entries, key) helper: takes the highest
existing value strictly below conf.ldap.uidGidReservedFloor (default
9000) plus one, or conf.ldap.uidGidMin (default 1500) if there are no
such entries. Ids at/above the reserved floor -- like the bootstrap
admin's 10000 -- are ignored entirely when computing the next
available number, so real users always start at 1500 and grow upward
regardless of the admin's reserved id.
Verified against a real theta-env deployment end to end:
- Reproduced the exact reported crash on a fresh bootstrap
- After the fix: first real user gets uidNumber/gidNumber "1500",
second gets "1501" -- admin's 10000 never enters the calculation
- New unit tests (nodejs/tests/posix_id.test.js, no LDAP required):
6/6 pass, covering the empty-array case, the reserved-floor
exclusion, and the NaN-from-missing-value case
- npm test: 18/18 passing tests still pass (unchanged); the other 155
failures are pre-existing/environmental (no LDAP server in this
sandbox) -- confirmed via git stash before starting this fix
* README: rewrite with feature overview, comparison, and tiered quick start
Expands the README with a fuller feature description, a "why this over
the alternatives" comparison against Keycloak/Authentik/Authelia/Zitadel,
an architecture diagram, and a three-tier quick start (unified theta-env
stack, standalone Docker, bare metal) instead of the old OpenLDAP-setup-
first structure.
* docs/index.md: fix stale setup.env quickstart snippet (.env -> setup.env, CFG_DOMAIN)
Personal access tokens so scripts/CI can call the management API without a
browser session. Each logged-in user mints their own token; it authenticates as
the creator (carries their LDAP group permissions, re-resolved live), so the
existing permission.byGroup checks apply unchanged.
- models/api_token.js: new ApiToken model (sso_<id>_<secret> format; id is the
lookup key, secret bcrypt-hashed + isPrivate, shown once). add()/rotate()/
authenticate(); optional expires_at; best-effort last_used_on. No _ttl
(persists; lifetime via expires_at).
- routes/api_token.js: self-service CRUD (list/get/update/delete/rotate),
owner-scoped (created_by === req.user.uid, 403 otherwise).
- middleware/auth.js + models/auth.js: accept `Authorization: Bearer sso_...`
(precedence over the auth-token session header); checkApiToken collapses
every failure to one generic 401 (no existence/secret/expiry leak).
- views/api_tokens.ejs + routes/index.js (GET /api-tokens): self-service page
(forceLogin, no group gate) — create (token shown once), edit, rotate, revoke.
- views/top.ejs: "API Tokens" nav entry visible to all logged-in users.
- public/js/app.js: app.apiToken client module.
- DEPLOYMENT.md + docs/deployment.md: API tokens section.
Co-authored-by: Claude <noreply@anthropic.com>
Lossless upgrades + config story for the all-in-one image.
Redis persistence (Part A):
- Replace in-memory `--save "" --appendonly no` with AOF + RDB persisted to /data
(--appendonly yes, periodic saves, --dbfilename dump.rdb). OAuth clients,
tokens, and other model-redis state now survive container recreation.
- Add the `sso-data` named volume -> /data in docker-compose.yml.
Config from ./config/sso-secrets.js (Part B):
- docker-entrypoint.sh: when /config/sso-secrets.js is mounted, symlink it to
/app/conf/secrets.js and read the server-side LDAP vars (base DN, admin pass,
org, domain, cert CN, JWT) from the file via one `node` call (base64-decoded,
no eval/quoting hazards). No app_* env is exported in this mode, so the file
is authoritative (@simpleworkjs/conf precedence: base < env < secrets.js <
app_* env). Falls back to the existing LDAP_* env-var mode when the file is
absent (standalone/bare-metal still works).
- docker-compose.yml: trim `environment:` to NODE_ENV/NODE_PORT only and add
`./config:/config:ro`. Removing the app_* env is required — any leftover
app_* would silently override secrets.js.
- secrets.js.example: add orchestrator-only `stack`, `bootstrap`, and
`serviceAccountPass` keys (ignored by the app; read by the entrypoint, the
theta-env bootstrap, and setup.sh).
Backup/restore docs:
- Full "Backups and restore" runbook in DEPLOYMENT.md (what lives where,
manual backup, full / Redis-only / LDAP-only restore, AOF-vs-RDB note,
upgrades). Restore uses slapadd -f (static slapd.conf), and RDB restore
requires deleting the AOF first (AOF wins on startup).
- Pointers in docs/deployment.md and docs/ldap.md; update the Docker Setup
section for the new ./config/ approach (env vars now advanced/optional).
Co-authored-by: Claude <noreply@anthropic.com>
Document how to get logs when running the all-in-one image: docker compose
logs for the app + slapd (both stdout/stderr, slapd runs -d 0), plus a direct
ldapsearch health check. Added to README.md, DEPLOYMENT.md (Method 1), and the
GitHub Pages docs/deployment.md.
Co-Authored-By: Claude <noreply@anthropic.com>