Previously the script only ran port resolution when no compose
containers were already running, on the assumption that running
containers meant the persisted port was still ours. On the shared
optical-dev server that's wrong: another app can grab a port
between our deploys, leaving us with a stale .env value that fails
to bind.
Now find_free_port treats a port as "available for us" if either
nothing is listening, or one of our own compose containers is
publishing it (so re-deploys don't shuffle). Other-app listeners
trigger a re-pick and a warning.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a Postgres-backed user store with bcrypt + JWT cookie sessions,
login screen, role-gated UI, and Microsoft SSO scaffolding ready to
fill in.
Backend
- New `db` service (Postgres 16-alpine) in compose, healthcheck-gated
app startup, free-port autodetect (5435-5499) like other apps.
- `server/db.js` runs versioned `.sql` migrations on boot.
- `server/auth.js`: bcrypt + JWT cookie (httpOnly, sameSite=strict,
path-scoped to /adeo-maturity), rate-limited login (10/15min),
dummy bcrypt-compare on missing users to defeat timing oracles.
- `requireAdmin` on all writes (POST/import/sync); `authenticate`
on all reads. /api/health stays public.
- Microsoft SSO endpoints stubbed at /api/auth/msft/{login,callback}
(return 501); DB has azure_oid column ready; comments document
exactly how to wire @azure/msal-node.
Frontend
- Login screen with email/password + greyed-out "Sign in with
Microsoft" button; init() checks /api/auth/me first.
- Logout button + user badge in header.
- body.role-user CSS hides .admin-only elements (Update tab, New
Client cards). Server enforces regardless.
Deploy
- deploy.sh generates DB_PASSWORD and AUTH_SECRET on first run and
persists to .env, then runs `seed-users.js seed-defaults` to
create admin@oliver.agency + user@oliver.agency with random
passwords printed once. Subsequent deploys skip seeding unless
--reseed is passed.
- node server/seed-users.js set-password <email> <pw> for ad-hoc
resets later.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Subpath-aware deploy for /adeo-maturity/ behind the shared optical-dev
vhost. Auto-picks a free host port (prefers 3102, scans 3102-3199) and
persists it to .env so re-deploys are idempotent. Renders the Apache
conf from a template on each run.
- script.js: detect URL prefix at load time and prepend it to all /api/
calls, so the same code works at the root locally and under a
sub-path behind Apache.
- Dockerfile: fix broken package.json copy (lives at repo root, not
server/) and install python3 + reportlab + openpyxl for the sync/
import/PDF endpoints that shell out.
- docker-compose: pin top-level name (per global docker policy),
configurable host port, bind 127.0.0.1 only.
- deploy/: new deploy.sh + apache-adeo.conf.tmpl. Old root deploy.sh
removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>