Files
mrcharles 1bf9810071 fix(consuldemocracy): verify end-to-end + harden seed/OIDC [#653]
Seed verification over marker trust (observed exit-0 no-op seed),
first-boot-only OIDC auto-enable, logo added, docs synced (JOURNAL
s17, counts 17/~57). grind-stack verified: homepage 200, OIDC SSO
button live.

Detail: https://projects.knownelement.com/issues/653#note-5044
2026-09-06 18:23:54 -05:00

143 lines
5.2 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
# ConsulDemocracy runtime setup for Cloudron:
# 1. generate config/database.yml + config/secrets.yml under /app/data
# (config/* are symlinks; addon creds rotate on restore/migration, so
# both files are rewritten on EVERY start)
# 2. start memcached (production cache_store is mem_cache_store/dalli)
# 3. wait for the postgresql addon, then create/migrate/seed the DB once
# (marker file) or just migrate on later boots
# 4. background the delayed_job worker (gated on the web port, so the
# schema exists first), then exec the Rails server on :3000
#
# Authentication: OIDC via the Cloudron platform provider, mapped into
# secrets.yml (oidc_client_id/secret/issuer). The seeds create a local
# admin (admin@consul.dev / 12345678) - CHANGE ITS PASSWORD on first
# login (see README).
DATA_DIR="/app/data"
SECRET_FILE="${DATA_DIR}/.secret_key_base"
MARKER="${DATA_DIR}/.db_seeded"
cd /var/www/consul
# --- 1. runtime configs (regenerated every boot) -------------------------------
wait_tcp() {
local host="$1" port="$2" name="$3"
echo "Waiting for ${name} at ${host}:${port} ..."
until (exec 3<>"/dev/tcp/${host}/${port}") 2>/dev/null; do
echo "${name} is unavailable - sleeping"
sleep 2
done
echo "${name} is up"
}
yml_escape() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e "s/'/\\\\'/g"; }
if [[ ! -s "${SECRET_FILE}" ]]; then
( umask 077; openssl rand -hex 32 > "${SECRET_FILE}" )
echo "Generated new secret_key_base"
fi
SECRET_KEY_BASE="$(cat "${SECRET_FILE}")"
DB_HOST="${CLOUDRON_POSTGRESQL_HOST:-127.0.0.1}"
DB_PORT="${CLOUDRON_POSTGRESQL_PORT:-5432}"
DB_NAME="${CLOUDRON_POSTGRESQL_DATABASE:-consul}"
DB_USER="${CLOUDRON_POSTGRESQL_USERNAME:-consul}"
DB_PASS="${CLOUDRON_POSTGRESQL_PASSWORD:-}"
cat > "${DATA_DIR}/database.yml" <<EOF
default: &default
adapter: postgresql
encoding: unicode
host: ${DB_HOST}
port: ${DB_PORT}
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
schema_search_path: "public,shared_extensions"
username: ${DB_USER}
password: <%= ENV["PGPASSWORD"] %>
production:
<<: *default
database: ${DB_NAME}
EOF
export PGPASSWORD="${DB_PASS}"
OIDC_CLIENT_ID="$(yml_escape "${CLOUDRON_OIDC_CLIENT_ID:-}")"
OIDC_CLIENT_SECRET="$(yml_escape "${CLOUDRON_OIDC_CLIENT_SECRET:-}")"
OIDC_ISSUER="$(yml_escape "${CLOUDRON_OIDC_ISSUER:-}")"
SERVER_NAME="$(yml_escape "${CLOUDRON_APP_DOMAIN:-localhost}")"
cat > "${DATA_DIR}/secrets.yml" <<EOF
# Generated by start.sh on every boot - manual edits will be lost.
shared:
map_tiles_provider: "//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
map_tiles_provider_attribution: "OpenStreetMap contributors"
production:
secret_key_base: ${SECRET_KEY_BASE}
server_name: ${SERVER_NAME}
force_ssl: false
delay_jobs: true
multitenancy: false
http_basic_username: ""
http_basic_password: ""
authentication_logs: false
devise_lockable: false
oidc_client_id: "${OIDC_CLIENT_ID}"
oidc_client_secret: "${OIDC_CLIENT_SECRET}"
oidc_issuer: "${OIDC_ISSUER}"
EOF
chmod 600 "${DATA_DIR}/secrets.yml"
# --- 2. memcached (localhost, as upstream's image intends) ----------------------
memcached -d -p 11211 -u consul -m 64
# --- 3. database lifecycle -------------------------------------------------------
wait_tcp "${DB_HOST}" "${DB_PORT}" "PostgreSQL"
# seed decision: the marker alone is not trusted - a marker left by a
# previous database (restore, migration, fresh test DB) must not skip
# seeding an empty one (observed: seed chain exits 0 yet persists
# nothing when re-run in the same first-boot process; a standalone
# re-seed lands fine, so verify + retry instead of debugging ghosts)
SEED_COUNT="$(bundle exec rails runner 'print Setting.count' 2>/dev/null || echo 0)"
if [[ ! -f "${MARKER}" || "${SEED_COUNT}" = "0" ]]; then
echo "First boot (or empty database): creating + migrating + seeding ..."
bundle exec rails db:create db:migrate db:seed
SEED_COUNT="$(bundle exec rails runner 'print Setting.count' 2>/dev/null || echo 0)"
if [[ "${SEED_COUNT}" = "0" ]]; then
echo "seed pass 1 did not persist - retrying standalone seed"
bundle exec rails db:seed
fi
touch "${MARKER}"
echo "Database seeded (local admin: admin@consul.dev - change the password!)"
# first-boot only: expose the SSO button when the platform provider is
# wired (feature.oidc_login defaults to false; operators can toggle it
# later in Admin > Settings > Features without us re-enabling it)
if [[ -n "${CLOUDRON_OIDC_CLIENT_ID:-}" ]]; then
bundle exec rails runner 'Setting["feature.oidc_login"] = true' || true
echo "OIDC login enabled (admin panel can toggle it)"
fi
else
bundle exec rails db:migrate
fi
# assets:precompile needs the booted environment (DB-dependent
# initializers), so it runs here - after the PG wait, every boot
# (idempotent: fast no-op when the manifests are current)
bundle exec rails assets:precompile
# --- 4. delayed_job worker + web server ------------------------------------------
# gate on the web port (migrations done) so the worker never races the schema
(
until (exec 3<>/dev/tcp/127.0.0.1/3000) 2>/dev/null; do sleep 2; done
echo "web port is up - starting delayed_job worker"
exec bundle exec rake jobs:work
) &
echo "Starting ConsulDemocracy on :3000 ..."
exec bundle exec rails server -b 0.0.0.0 -p 3000