diff --git a/.dockerignore b/.dockerignore index 194639e..8b934b9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -19,7 +19,10 @@ docs/ .github/ # Git + editor + secrets. -.git/ +# NOTE: .git/ is intentionally NOT excluded — the gitinfo build stage in the +# Dockerfile reads it to bake the commit hash into the image (see +# nodejs/utils/build_info.js), then it's discarded before the final stage. +# It never ends up in the final image. .gitignore *.md !README.md diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 69c5ddf..f32fbf5 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -40,6 +40,7 @@ are `JSON.parse`-coerced when possible and kept as raw strings otherwise. | `app_ldap__tlsOptions__ca` | `conf.ldap.tlsOptions.ca` | path to a CA cert for strict trust | | `app_auth__adminUsers` | `conf.auth.adminUsers` | local anti-lockout admin (uid) | | `app_auth__adminGroups` | `conf.auth.adminGroups` | SSO/LDAP groups that are global admin (JSON array) | +| `app_auth__localAdminPass` | `conf.auth.localAdminPass` | initial password for the local anti-lockout admin (used once, on first creation only — defaults to the username itself if unset) | | `app_redis__prefix` | `conf.redis.prefix` | default `proxy_` | See [`docs/docker.md`](docs/docker.md) for a shorter, container-focused version diff --git a/Dockerfile b/Dockerfile index dbdd6f0..942bf0d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,36 @@ # /usr/local/openresty/lualib (which is on OpenResty's package.path) — no manual # --lua-dir/--tree wrangling needed. +# ── Git commit hash (build-time only) ──────────────────────────────────────── +# The final image intentionally has no git binary and no .git directory (kept +# lean, per .dockerignore), so `git rev-parse` always fails at runtime and +# build_info.js silently fell back to "unknown". Resolve it here instead, +# where .git IS available (build context), and bake just the short hash into +# a file — this stage itself is discarded, only /commit.txt survives via the +# COPY --from below. Reuses the main base image (already pulled for the real +# build below) rather than a separate one, so this adds no extra image pull. +# +# GIT_COMMIT lets a caller override the resolved hash instead of computing it +# from .git in this build context. Needed when this repo is built as a git +# submodule (e.g. from theta-env): a submodule's .git is a pointer FILE, not +# a directory — the real object database lives in the superproject's +# .git/modules/, outside this repo's own directory and therefore outside +# Docker's build context entirely, so `git rev-parse` can never resolve it +# from in here no matter what. theta-env's setup.sh passes +# --build-arg GIT_COMMIT=$(git -C proxy rev-parse --short HEAD), computed on +# the host where the submodule resolves correctly. +ARG GIT_COMMIT="" +FROM openresty/openresty:1.31.1.1-2-bookworm-fat AS gitinfo +ARG GIT_COMMIT +WORKDIR /repo +COPY .git ./.git +RUN if [ -n "$GIT_COMMIT" ]; then \ + echo "$GIT_COMMIT" > /commit.txt; \ + else \ + { apt-get update && apt-get install -y --no-install-recommends git \ + && git rev-parse --short HEAD > /commit.txt; } 2>/dev/null || echo unknown > /commit.txt; \ + fi + FROM openresty/openresty:1.31.1.1-2-bookworm-fat # ── Tooling needed before adding apt repos ────────────────────────────────── @@ -77,6 +107,9 @@ COPY nodejs/utils ./utils COPY nodejs/views ./views COPY nodejs/public ./public +# Baked commit hash from the gitinfo stage (see build_info.js). +COPY --from=gitinfo /commit.txt ./.build_commit + # ── OpenResty config (mirrors ops/install.sh symlink targets) ───────────────── # The default OpenResty config lives at /usr/local/openresty/nginx/conf/nginx.conf # (the prefix conf dir); relative `include` directives resolve there. We place: diff --git a/nodejs/models/user_redis.js b/nodejs/models/user_redis.js index 8f66c11..90d64b8 100644 --- a/nodejs/models/user_redis.js +++ b/nodejs/models/user_redis.js @@ -3,6 +3,7 @@ const Table = require('.'); const bcrypt = require('bcrypt'); const crypto = require('crypto'); +const conf = require('@simpleworkjs/conf'); const saltRounds = 10; class User extends Table{ @@ -87,16 +88,22 @@ User.register(); (async function(){ var defaultUser = 'proxyadmin2' + // Optional: an orchestrator (e.g. theta-env's setup.sh) can set + // auth.localAdminPass in proxy-secrets.js to a generated password so this + // bootstrap account isn't left at the well-known default (username == + // password == "proxyadmin2"). Only used on first creation -- once the + // account exists this is never read again, so it's safe to leave set. + var defaultPass = (conf.auth && conf.auth.localAdminPass) || defaultUser; try{ let user = await User.get(defaultUser); }catch(error){ try{ let user = await User.create({ username:defaultUser, - password: defaultUser, + password: defaultPass, created_by: defaultUser }); - console.log(defaultUser, 'created', user); + console.log(defaultUser, 'created', user); }catch(error){ console.error(error) } diff --git a/nodejs/public/css/styles.css b/nodejs/public/css/styles.css index 7fa9bbc..804357f 100755 --- a/nodejs/public/css/styles.css +++ b/nodejs/public/css/styles.css @@ -3,9 +3,16 @@ nav.navbar{ padding-right: 1em; } +body { + display: flex; + flex-direction: column; + min-height: 100vh; +} + #spa-shell { margin-top: 4.5rem; padding-bottom: 1em; + flex-grow: 1; } .card-title{ diff --git a/nodejs/utils/build_info.js b/nodejs/utils/build_info.js index 933679b..66df054 100644 --- a/nodejs/utils/build_info.js +++ b/nodejs/utils/build_info.js @@ -1,15 +1,29 @@ 'use strict'; +const fs = require('fs'); +const path = require('path'); const { execSync } = require('child_process'); const { version: buildVersion } = require('../package.json'); -let buildHash = 'unknown'; -try { - buildHash = execSync('git rev-parse --short HEAD', { cwd: __dirname }).toString().trim(); -} catch (_) {} +// Docker builds bake the commit hash into ../.build_commit (see the gitinfo +// stage in Dockerfile) -- the final image has no git binary and no .git +// directory, so `git rev-parse` below always fails there. Bare-metal/dev +// runs have no baked file, so they fall back to asking git directly. +function readBuildHash() { + try { + const baked = fs.readFileSync(path.join(__dirname, '../.build_commit'), 'utf8').trim(); + if (baked) return baked; + } catch (_) {} + + try { + return execSync('git rev-parse --short HEAD', { cwd: __dirname }).toString().trim(); + } catch (_) { + return 'unknown'; + } +} module.exports = { buildVersion, - buildHash, + buildHash: readBuildHash(), buildYear: new Date().getFullYear(), }; diff --git a/secrets.js.example b/secrets.js.example index fcbff7e..b2f0157 100644 --- a/secrets.js.example +++ b/secrets.js.example @@ -60,6 +60,14 @@ module.exports = { adminGroups: [], adminUsers: ['proxyadmin'], groupRoleMap: {}, + // Optional: the local anti-lockout admin's initial password, used + // ONLY the first time that account is created. Leave unset and it + // defaults to the username itself ("proxyadmin2") — fine for a quick + // local test, but change it (or set this) before exposing the proxy + // publicly. Once the account exists, this key is never read again; + // change the password via the app itself (or delete the Redis user + // to force it to be re-bootstrapped with a new value here). + // localAdminPass: 'change-me', }, // ── Orchestrator-only (ignored by the app) ───────────────────────────────