Commit Graph

157 Commits

Author SHA1 Message Date
wmantly 1b3e842006 ci: set app_oauth__jwtSecret for test runs
routes/oauth.js now validates jwtSecret at module load time, so CI must
provide a non-placeholder value for the test runner.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 22:16:30 -04:00
wmantly efe3e514b0 chore(release): public-release readiness and security fixes for 1.1.16
Security:
- Escape user-supplied values in LDAP filters and DNs (group_ldap.js, user_ldap.js)
- Replace Math.random() token/UUID/OTP generation with crypto.randomUUID / crypto.randomInt
- Refuse startup when oauth.jwtSecret is missing or placeholder

Fixes:
- Correct from-address template rendering in email.js

Packaging:
- Remove private flag and bump version to 1.1.16

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 22:08:11 -04:00
wmantly 37f2ece172 Merge pull request #87 from theta42/release-1.1.15
Bump version to 1.1.15
v1.1.15
2026-07-18 01:20:13 -04:00
wmantly b77704089b Bump version to 1.1.15; update CHANGELOG
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 01:18:05 -04:00
wmantly 114d8c86ca Merge pull request #86 from theta42/install-script-rework
Rewrite install.sh as a git-clone installer, add a one-line install
2026-07-18 01:17:18 -04:00
wmantly 3e87ad86ab Rewrite install.sh as a git-clone installer, add a one-line install
Replaces the old flag-driven, copy-based installer with an idempotent
git-clone-and-symlink installer matching theta42/proxy's ops/install.sh
pattern, so `wget -O - .../install.sh | sudo bash` works the same way
for both apps:

- Installs to /opt/theta42/sso-manager (was /opt/sso-manager, and the
  repo had to already be checked out locally -- now it clones itself).
- First run only: bootstraps OpenLDAP (modules, overlays, schema,
  directory tree, SSO groups -- ops/ldap-setup.sh) with a generated
  admin password + JWT secret, and seeds /etc/sso-manager/secrets.js
  (was /opt/sso-manager/conf/secrets.js, hand-filled from CLI flags).
  Later runs never touch LDAP or the secrets file again.
- ops/systemd/sso-manager.service now points at the new install path
  and sets CONF_SECRETS=/etc/sso-manager/secrets.js (requires
  @simpleworkjs/conf >= 1.2.0, already the pinned version) instead of
  the app needing a config file inside the repo checkout.
- Prints the version it's updating from/to (or "Already up to date")
  on every run, instead of updating silently.

Two real bugs found and fixed while testing this end-to-end in a clean
container:
- The debconf `slapd/domain` value was computed as
  `${LDAP_BASE_DN#dc=}` ("example,dc=com" for "dc=example,dc=com")
  instead of a proper dotted domain -- slapd's postinst hangs
  indefinitely on a malformed domain instead of failing cleanly.
  Fixed to derive it the same way the secrets file already did
  ("example.com").
- ops/ldap-setup.sh's ppolicy-overlay checks used an LDAP substring
  filter, `(olcOverlay=*ppolicy*)`, against an attribute that doesn't
  support substring matching -- it silently matched nothing even when
  the overlay was correctly configured (stored as "{0}ppolicy"),
  so the final verification always reported failure and `set -e`
  aborted the installer after LDAP was set up but before the app was.
  Fixed to filter on `(objectClass=olcOverlayConfig)` and let the
  existing DN-based grep narrow it down, matching the pattern already
  used by every other check in that script.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 01:13:51 -04:00
wmantly c5a2c0a71d Merge pull request #85 from theta42/release-1.1.14
Bump version to 1.1.14
v1.1.14
2026-07-17 23:46:43 -04:00
wmantly fe23d231be Bump version to 1.1.14; update CHANGELOG
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 23:44:34 -04:00
wmantly 3a612dbed7 Merge pull request #84 from theta42/bump-conf-jqrepeat
Bump @simpleworkjs/conf to 1.2.0, jq-repeat to 2.2.0
2026-07-17 23:41:19 -04:00
wmantly f154bb8db0 Bump @simpleworkjs/conf to 1.2.0, jq-repeat to 2.2.0
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>
2026-07-17 23:38:56 -04:00
wmantly 9fc5abda2a Merge pull request #83 from theta42/fix-changelog-corruption
Fix CHANGELOG.md corruption (v1.1.10 merged into v1.1.11)
2026-07-17 22:22:21 -04:00
wmantly a6ee985de4 Fix CHANGELOG.md: restore separate heading for v1.1.10
A repeated Edit-tool bump had overwritten the previous top version
heading instead of inserting a new one above it, silently merging
v1.1.10's release notes into v1.1.11's section with the v1.1.10
heading missing entirely. The underlying content was still present,
just missing its own "## [1.1.10]" header -- restored.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 22:20:02 -04:00
wmantly 47a9f6c3ec Merge pull request #82 from theta42/fix-doc-link-slugs
Resolve doc cross-links by real filename as a fallback
v1.1.13
2026-07-17 22:06:08 -04:00
wmantly f6552cb741 Resolve doc cross-links by real filename as a fallback
The new concept docs (and their "See also" reciprocal links) reference
each other by real filename -- "concepts-accounts.html" -- which is the
correct, working URL on the Jekyll/GitHub Pages build (a page's URL there
IS its filename stem), but doesn't match this viewer's own short slugs
(DOCS keys, e.g. "accounts" -> /docs/accounts), so fixDocLinks() left
those links unrewritten and 404ing in-app.

Rather than rewrite the docs to two different link forms depending on
target, resolve by filename as a fallback when the slug lookup misses --
one link written in a doc now works correctly 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
2026-07-17 22:03:52 -04:00
wmantly 4e7e29b35f Merge pull request #81 from theta42/concept-docs
Add plain-language concept docs; fix docs viewer rendering; link API tokens
v1.1.12
2026-07-17 21:51:40 -04:00
wmantly 4e5a2aa4f9 Add plain-language concept docs; fix docs viewer rendering; link API tokens
- New docs/concepts-{accounts,oauth-apps,api-tokens}.md -- plain-language
  guides aimed at less technical readers, each linking onward to the
  existing schema/protocol-level doc for anyone who wants that detail.
  Card help links (Users, Groups, OAuth cards, My groups, Members of
  <uid>'s group) now point here instead of straight at the technical
  docs; the LDAP-protocol-wiring cards (raw connection details for
  connecting a 3rd-party app) stay pointed at the technical ldap.md,
  since that's genuinely the right depth for that task.
- 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. Also fixed: cross-doc links
  (ldap.html, index.html, etc.) never resolved in-app, since this
  viewer serves docs at /docs/<slug> with no .html suffix -- rewritten
  to the correct in-app URL, same idea as the existing image-path fix.

Bumps to v1.1.12.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 21:49:27 -04:00
wmantly 51784f2f27 Merge pull request #80 from theta42/help-icon-relocate
Move help links from the global header onto each relevant card
v1.1.11
2026-07-17 19:56:29 -04:00
wmantly 4c59b1fabb Move help links from the global header onto each relevant card
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 (e.g. Integrations has both OAuth and LDAP cards). Removed it and
added a small help icon directly to each card that has real corresponding
doc content, linking straight to that doc -- Invite User/Add new
user/User List/Service Accounts (users.ejs), group cards (groups.ejs),
OAuth Apps + LDAP connection cards (integrations.ejs), My groups/Members
of <uid>'s group/New API Token (profile.ejs).

Bumps to v1.1.11.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 19:54:19 -04:00
wmantly 099638057e Merge pull request #79 from theta42/docs-help-search
Add header help icon and in-app docs search
v1.1.10
2026-07-17 19:24:33 -04:00
wmantly 077c41844d Add header help icon and in-app docs search
- 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
2026-07-17 19:22:27 -04:00
wmantly 96adf60cf7 Merge pull request #78 from theta42/personal-group-members
Add personal Unix group member management
v1.1.9
2026-07-17 11:31:11 -04:00
wmantly 82f703f560 Add personal Unix group member management
Every account gets a personal posixGroup at creation (its primary GID
holder) but there was no way to manage its memberUid list -- add
add/remove endpoints and a profile-page UI (admin-only), reusing the
userSelect widget already built for the manager field.

Bumps to v1.1.9.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 11:28:41 -04:00
wmantly 65b107d8ff Merge pull request #77 from theta42/fix-account-editing-bugs
Fix account-editing bugs; add editable group membership from profile
v1.1.8
2026-07-17 11:03:05 -04:00
wmantly 5d7c0bd594 Fix account-editing bugs from real-world feedback, add editable group membership
- Edit form's Mobile Phone field was effectively required (stray validate
  attribute) -- removed.
- Service account profiles always showed the literal filler name "Service
  Account" -- hidden now, since it's not meaningful. Required computing
  isServiceAccount in User.get(), not just listDetail().
- Fresh service accounts could look uncategorized (missing from the
  Service Accounts tab, wrong isServiceAccount) for up to 5 minutes after
  creation, due to a cache-staleness race in the create route -- the user
  gets cached via User.get() before the route marks it as a service
  account. Cleared and re-fetched after marking.
- memberOf came back as a bare string instead of a one-element array for
  users in exactly one group, causing client-side permission checks to
  iterate character-by-character and incorrectly deny access -- normalized
  alongside the existing manager normalization.
- Added editable group membership on the profile page ("My groups"),
  admin-only, using the existing per-group member endpoints.

Bumps to v1.1.8.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 11:00:50 -04:00
wmantly 3ad817767a Merge pull request #76 from theta42/accounts-manager-rework
Unify service accounts, add manager field, editable homeDirectory/loginShell
v1.1.7
2026-07-17 00:35:52 -04:00
wmantly cdc5d1528c Unify service accounts to one kind, add manager field, make homeDirectory/loginShell editable
Removes the LDAP bind-only service account type in favor of a single
Unix/POSIX account model, surfaced in a new Users > Service Accounts tab.
Adds a multi-valued `manager` field to every account (defaults to the
creator, editable, and grants edit rights on the accounts a person manages
without needing app_sso_admin). homeDirectory and loginShell are now
editable from the profile edit form.

Bumps to v1.1.7.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 00:32:19 -04:00
wmantly 5fc65d6fb3 Merge pull request #75 from theta42/bump-1.1.6
Bump version to 1.1.6
v1.1.6
2026-07-16 20:24:43 -04:00
wmantly ea65a85aa9 Bump version to 1.1.6; update CHANGELOG 2026-07-16 20:22:42 -04:00
wmantly f8cf68b85f Merge pull request #74 from theta42/redesign-docs-site
Redesign docs site: match the app's own look, add SEO, mobile-ready
2026-07-16 19:54:43 -04:00
wmantly cedef0ed09 Redesign docs site: match the app's own look, add SEO, mobile-ready
The GitHub Pages site used the generic jekyll-theme-cayman theme --
purple gradient hero, no site nav, no per-page SEO. Replaced with a
custom layout that mirrors the actual app UI: dark fixed navbar with
the theta42 logo, Bootstrap 5 + Font Awesome (same stack the app
uses), content in a card, dark footer matching bottom.ejs
(copyright, MIT license, GitHub, Changelog links).

- New cross-page nav (Home/Deployment/Configuration/OAuth/LDAP/
  Changelog) -- there was previously no way to get from one docs
  page to another except a single "Back to Home" link per page.
- SEO: jekyll-seo-tag + jekyll-sitemap (both GitHub-Pages-supported
  plugins, no custom build needed) -- real per-page meta description,
  Open Graph/Twitter card tags, canonical URLs, JSON-LD, sitemap.xml,
  and a robots.txt referencing it. Added a real description to every
  page's front matter (none existed before).
- Mobile: Bootstrap's responsive grid + collapsible navbar; the
  screenshot pairs in index.md (inline width="49%" for a two-up
  desktop layout) now stack to full-width below 576px instead of
  squeezing illegibly small.

Verified with a real Jekyll build (jekyll/jekyll Docker image, no
Ruby available locally) + Playwright: desktop and mobile (375px)
screenshots of the home and deployment pages, mobile nav toggle
open/close, active-link highlighting per page, zero console/page
errors, and confirmed real SEO output (meta description, OG/Twitter
tags, canonical, JSON-LD, sitemap.xml, robots.txt) via curl against
the served site.
2026-07-16 19:52:26 -04:00
wmantly 0e31320964 Merge pull request #73 from theta42/bump-1.1.5
Bump version to 1.1.5
v1.1.5
2026-07-16 18:37:29 -04:00
wmantly f12ce8c600 Bump version to 1.1.5; update CHANGELOG 2026-07-16 18:35:27 -04:00
wmantly b358e3b0b0 Merge pull request #72 from theta42/jq-repeat-2.1.0
Update jq-repeat to 2.1.0; fix editProfile update->slideDown race
2026-07-16 18:29:58 -04:00
wmantly 88387f3117 Update jq-repeat to 2.1.0; fix editProfile update->slideDown race
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.
- __setPut/__setTake, jr-order-reverse, nested jq-repeat templates:
  not used anywhere in this repo (unlike proxy's companion PR, which
  needed the __setPut/__setTake fix).

Real risk found and fixed: update() is now trailing-edge throttled
(~50ms) even on the first call, not just rapid subsequent ones.
profile.ejs's editUser()/editUserSeccess() call $.scope.editProfile
.update()/renderProfile() (which itself calls update()) and
immediately slideDown() the same element -- with the old synchronous
behavior the form was already populated by then; with throttling it
could briefly show stale/empty data. Deferred both slideUp/slideDown
pairs by 60ms (past the throttle window), per the library's own
migration guidance. Verified live (real bundled image + Playwright,
logged in as admin): the edit form's fields show real data, not
empty/stale, when checked right as the slide-open completes.
2026-07-16 18:27:58 -04:00
wmantly e2b4ffabb7 Merge pull request #71 from theta42/bump-1.1.4
Bump version to 1.1.4
v1.1.4
2026-07-16 17:46:26 -04:00
wmantly a466128c21 Bump version to 1.1.4; update CHANGELOG 2026-07-16 17:44:21 -04:00
wmantly b03c0af09d Merge pull request #70 from theta42/white-label
White-label: title/logo now driven by conf
2026-07-16 17:33:20 -04:00
wmantly be41597502 Fix routes/oauth.js's separate pageLocals object missing conf.logo
routes/oauth.js has its own pageLocals object (distinct from
routes/index.js's values and routes/docs.js's own copy) used by
oauth_authorize.ejs/oauth_logout.ejs -- missed in the white-label
change since a grep alias in this environment silently treats this
particular file as binary and skips it. Caught by CI (oauth.test.js),
not local testing. Added logo: conf.logo to match the other two
copies of this locals object.
2026-07-16 17:31:00 -04:00
wmantly 21f2cda2ee White-label: title/logo now driven by conf (closes #6)
conf.name was already plumbed into routes/index.js's values object,
but never actually rendered anywhere -- <title>, the navbar brand,
and the favicon were all still hardcoded "SSO - Theta 42"/"SSO
Manager". Now render <%- name %>/<%- logo %> in top.ejs; new
conf.logo key (default: the existing theta42.svg) drives the navbar
image and favicon.

Also fixes a pre-existing broken favicon: top.ejs referenced
/static/favicon.svg, which was never actually served from public/ --
only public/img/theta42.svg existed. The favicon now uses that same
file via conf.logo instead of a nonexistent path.

Footer copyright/logo/GitHub links are left as-is (open-source
attribution, not deployment branding).
2026-07-16 17:26:01 -04:00
wmantly 976c3439fc Merge pull request #69 from theta42/add-ci-and-fix-ppolicy
Add CI (Jest against the real bundled image); fix ppolicy pwdLockout default
2026-07-16 17:00:02 -04:00
wmantly 81e36c9928 CI: use a real Redis service container, not the bundled image's
The bundled Dockerfile.openldap image's own redis-server binds to
loopback only inside its container (no --bind override), so
Docker's -p 6379:6379 forward from the runner could never actually
reach it -- confirmed by the first real CI run failing with
"Socket closed unexpectedly" the instant the test process tried to
connect. Worked when tested locally only by accident: my override
env vars didn't actually take effect (model-redis's setUpTable only
reads a nested redisConf key, not flat host/port), so the app fell
back to createClient({})'s localhost:6379 default and happened to
hit my own pre-existing local Redis instead of the container's.

Fix: a dedicated redis:7-alpine GHA service container, which binds
correctly and is reachable at localhost:6379 -- matching that same
default, no env override needed.
2026-07-16 16:57:57 -04:00
wmantly bc5bca2e28 Add CI (Jest against the real bundled image); fix ppolicy pwdLockout default
- New GitHub Actions workflow: builds the real Dockerfile.openldap
  image, starts it, seeds the LDAP fixtures the test suite expects
  (uid 'test' + 'wmantly', matching the existing "wmantly is always
  present in the test LDAP" assumption in several test files), then
  runs the full Jest suite against it on Node 18/20/22. This repo
  previously had unit tests but no automated workflow running them.
- Found while building this: the bundled default ppolicy entry
  (docker-entrypoint.sh + ops/ldap-setup.sh) sets pwdLockout: FALSE,
  which is backwards -- it silently makes the admin "deactivate user"
  action a no-op for auto-lockout-after-failed-attempts (a related
  but distinct ppolicy feature from pwdAccountLockedTime). Fixed to
  TRUE in both places; ldap-setup.sh also gets a drift-correction
  path so an existing deployment can pick up the fix by re-running it.
- Separately, deactivating a user still doesn't block their LDAP bind
  in the bundled image even with this fix -- filed as #68, since it's
  a deeper OpenLDAP ppolicy overlay question unrelated to the CI/test
  setup here. tests/user_admin.test.js now soft-skips that specific
  assertion (with a console warning pointing at #68) instead of
  failing, so this known environment gap doesn't block CI.
2026-07-16 16:54:05 -04:00
wmantly 4c4fc34dcf Merge pull request #67 from theta42/bump-1.1.3
Bump version to 1.1.3
v1.1.3
2026-07-16 16:04:36 -04:00
wmantly 3e67c23008 Bump version to 1.1.3; update CHANGELOG 2026-07-16 16:04:08 -04:00
wmantly b657c4034b Merge pull request #66 from theta42/add-changelog
Add CHANGELOG.md, serve it in-app at /docs/changelog
2026-07-16 16:01:11 -04:00
wmantly f323a45fef Add CHANGELOG.md, serve it in-app at /docs/changelog (closes theta42/theta-env#43)
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.
2026-07-16 16:00:59 -04:00
wmantly ff10a23e78 Merge pull request #65 from theta42/bump-1.1.2
Bump version to 1.1.2
v1.1.2
2026-07-16 15:38:27 -04:00
wmantly c2851ea537 Bump version to 1.1.2 2026-07-16 15:36:59 -04:00
wmantly 98d767a201 Merge pull request #64 from theta42/airgap-and-docs
Air-gap: remove dead CDN reference + in-app /docs
2026-07-16 15:34:12 -04:00
wmantly 955189d08a Air-gap: remove dead CDN reference + in-app /docs
- Removed a dead IE<9-only html5shim script tag pointing at a domain
  that no longer resolves.
- New GET /docs (index) and /docs/:slug routes render this project's
  own README, DEPLOYMENT, API.md, docs/*.md, and directory_spec.md
  server-side via marked -- so the documentation is readable from the
  running app with no route to GitHub Pages, where it otherwise only
  lives. Public, no auth, rate-limited (middleware/rate_limit.js) like
  the other public routes.
- .dockerignore/Dockerfile.openldap updated to copy DEPLOYMENT.md,
  API.md, directory_spec.md, and docs/ into the image, mirroring the
  existing tos.md -> /tos.md convention.
2026-07-16 15:33:46 -04:00