Files
Behavision/run-local.sh
Suriyakumarvijayanayagam 4c750cb2ac Opening a shop is an API call; the broker learns of it in the same request
The last step of onboarding that needed a shell: provision site printed
a broker password and a person typed it into Mosquitto's passwd file on
the host - mounted read-only in the container, so the first attempt
failed silently and the password was re-rolled. No tenant could open a
second branch without us.

The server now drives Mosquitto's dynamic-security plugin over its own
broker login: POST /api/sites (owner) writes the row and the sealed
password, registers the login and a per-site role with literal topics
(the 2.0 plugin does not substitute %u - measured), and removes the row
again if the broker refuses, so a shop cannot exist in the database and
not on the broker. provision site goes through the same path. The
head-office Shops screen gets 'Open a new shop'.

broker-init converts the existing passwd file into the plugin's store
with every hash intact - PBKDF2-SHA512 both sides - so the cutover
re-claims no shop PC. Rehearsed locally: old logins keep working,
isolation holds, the health probe works, and a PC claiming a shop opened
through the API connects as that shop. run-local.sh now brings the
broker up the same way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
2026-09-19 11:55:26 +05:30

167 lines
7.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# Bring the whole platform up locally, from nothing, in one command.
#
# Everything runs on this machine and touches no production system: Postgres and
# Mosquitto in containers, the server as a local binary with the web app built
# into it. Re-running it is safe - it reuses the containers and the database.
#
# Ports are deliberately odd. Docker Desktop itself listens on 127.0.0.1:8080,
# which is how an earlier run of this ended up talking to something that was not
# the server at all.
set -euo pipefail
cd "$(dirname "$0")"
PORT=${PORT:-8088}
PG_PORT=${PG_PORT:-55432}
MQTT_PORT=${MQTT_PORT:-51883}
STATE=${STATE:-.local}
export DATABASE_URL="postgres://postgres:test@127.0.0.1:${PG_PORT}/behavision"
mkdir -p "$STATE/mosquitto"
step() { printf '\n\033[1m%s\033[0m\n' "$*"; }
# Checked up front rather than discovered in the middle of step 2, where the
# failure is a bare "go: command not found" after two minutes of npm install.
for tool in docker go npm; do
command -v "$tool" >/dev/null 2>&1 || {
printf 'need %s on PATH.\n' "$tool" >&2
[ "$tool" = go ] && printf 'go is often installed outside the default PATH; try: export PATH="$HOME/go/bin:$PATH"\n' >&2
exit 1
}
done
step "1. Postgres (pgvector - migration 001 needs the extension)"
docker inspect bv-pg >/dev/null 2>&1 || docker run -d --name bv-pg \
-p "${PG_PORT}:5432" -e POSTGRES_PASSWORD=test -e POSTGRES_DB=behavision \
pgvector/pgvector:pg16 >/dev/null
docker start bv-pg >/dev/null 2>&1 || true
until docker exec bv-pg pg_isready -U postgres >/dev/null 2>&1; do sleep 1; done
echo " ready (the server applies the schema itself on start)"
step "2. Build (the web app builds INTO the Go module, so it goes first)"
(cd web && npm install --silent && npm run build >/dev/null)
(cd server && go build -o "../$STATE/bv-server" ./cmd/behavision-server)
echo " built $STATE/bv-server"
step "3. Encryption key (camera and broker passwords are sealed with it)"
if [ ! -f "$STATE/env.sh" ]; then
KEY=$("./$STATE/bv-server" provision key -raw 2>/dev/null)
cat > "$STATE/env.sh" <<EOF
export DATABASE_URL='${DATABASE_URL}'
export BEHAVISION_SECRET_KEY='${KEY}'
export LISTEN_ADDR=127.0.0.1:${PORT}
export MQTT_URL='tcp://127.0.0.1:${MQTT_PORT}'
export MQTT_USERNAME='behavision-server'
export MQTT_PASSWORD='server-broker-2026'
export AGENT_MQTT_URL='tcp://127.0.0.1:${MQTT_PORT}'
export BEHAVISION_ALLOW_PLAINTEXT_MQTT=1
EOF
chmod 600 "$STATE/env.sh"
echo " new key written to $STATE/env.sh (keep it: without it, sealed passwords are lost)"
else
echo " reusing $STATE/env.sh"
fi
# shellcheck disable=SC1090
. "$STATE/env.sh"
step "3b. Schema"
# The server would do this itself on start, but provisioning below runs BEFORE
# it does and needs the tables to exist. One command either way, and it is the
# same code path the server uses.
"./$STATE/bv-server" migrate
step "4. Mosquitto"
# Dynamic security, not a passwd file - the same shape as production. The
# server registers each site's broker login itself over the control topic, so
# there is no per-site password to type here and nothing to restart. The store
# is seeded once with the server's own login as the plugin admin; after that
# the plugin owns the file.
mkdir -p "$STATE/mosquitto/data"
# Rewritten when it is the pre-plugin shape, so a checkout that ran the old
# script comes up in the new one rather than half of each.
if ! grep -q mosquitto_dynamic_security "$STATE/mosquitto/mosquitto.conf" 2>/dev/null; then
rm -f "$STATE/mosquitto/passwd" "$STATE/mosquitto/acl"
docker rm -f bv-mqtt >/dev/null 2>&1 || true
cat > "$STATE/mosquitto/mosquitto.conf" <<EOF
per_listener_settings false
listener 1883
allow_anonymous false
plugin /usr/lib/mosquitto_dynamic_security.so
plugin_opt_config_file /mosquitto/data/dynamic-security.json
EOF
fi
if [ ! -f "$STATE/mosquitto/data/dynamic-security.json" ]; then
: > "$STATE/mosquitto/passwd.seed"
docker run --rm -v "$PWD/$STATE/mosquitto:/m" eclipse-mosquitto:2 \
mosquitto_passwd -b /m/passwd.seed behavision-server "$MQTT_PASSWORD" 2>/dev/null
"./$STATE/bv-server" broker-init -passwd "$STATE/mosquitto/passwd.seed" \
-out "$STATE/mosquitto/data/dynamic-security.json" -backend-user behavision-server >/dev/null
rm -f "$STATE/mosquitto/passwd.seed"
# The plugin rewrites this file, so the broker's user (1883) must own it.
chmod 666 "$STATE/mosquitto/data/dynamic-security.json"
fi
# A container is reused only if its config mount still points HERE. The bind
# source is baked in when the container is created, so one made while the
# checkout lived somewhere else - or by a run from another directory - comes
# back up with an empty /mosquitto/config and dies with "Unable to open config
# file", which the old `|| true` below then hid completely.
MQTT_CONF="$PWD/$STATE/mosquitto"
if docker inspect bv-mqtt >/dev/null 2>&1; then
MOUNTED=$(docker inspect bv-mqtt \
--format '{{range .Mounts}}{{if eq .Destination "/mosquitto/config"}}{{.Source}}{{end}}{{end}}')
if [ "$MOUNTED" != "$MQTT_CONF" ]; then
echo " recreating bv-mqtt (its config was mounted from ${MOUNTED:-nowhere})"
docker rm -f bv-mqtt >/dev/null
fi
fi
docker inspect bv-mqtt >/dev/null 2>&1 || docker run -d --name bv-mqtt \
-p "${MQTT_PORT}:1883" -v "$MQTT_CONF:/mosquitto/config" \
-v "$MQTT_CONF/data:/mosquitto/data" \
eclipse-mosquitto:2 >/dev/null
docker start bv-mqtt >/dev/null 2>&1 || true
# Wait for it, and say so if it never arrives. `docker start` returning 0 only
# means the container was launched; mosquitto exits a moment later if it cannot
# read its config, and every `docker exec` after that fails for a reason that
# has nothing to do with what it was asked to do.
for _ in $(seq 1 20); do
docker exec bv-mqtt sh -c 'exit 0' >/dev/null 2>&1 && break
sleep 1
done
if ! docker exec bv-mqtt sh -c 'exit 0' >/dev/null 2>&1; then
echo " broker will not stay up:" >&2
docker logs --tail 5 bv-mqtt >&2
exit 1
fi
echo " broker on ${MQTT_PORT} (dynamic security)"
step "5. First accounts"
# Idempotent throughout: every provision subcommand upserts, so re-running this
# resets these passwords rather than failing.
"./$STATE/bv-server" provision user -email admin@loyaly.ai -role admin \
-name "Loyaly Platform" -password 'loyaly-platform-2026' >/dev/null
# A tenant to sign in as. In the real flow a platform admin creates this from
# Companies -> New company; it is seeded here so a fresh database has a working
# login without seven steps first. Creating another one through the UI still
# exercises the real path.
"./$STATE/bv-server" provision client -slug tenext-retail -name "TeNext Retail" >/dev/null
"./$STATE/bv-server" provision user -client tenext-retail -email suriya@tenext.in \
-role owner -name "Suriya" -password 'tenext-2026' >/dev/null
# The shop. Its broker password is re-rolled on every run - it is sealed and
# never readable again - so it is pushed into Mosquitto here in the same breath.
# A shop PC enrolled on an earlier run therefore has to be claimed again, which
# is the right trade locally and is why this is not how production works.
MQTT_URL="tcp://127.0.0.1:${MQTT_PORT}" MQTT_USERNAME=behavision-server MQTT_PASSWORD="$MQTT_PASSWORD" \
"./$STATE/bv-server" provision site -client tenext-retail -slug chennai \
-name "TeNext Chennai" -tz Asia/Kolkata | sed 's/^/ /'
printf ' platform admin admin@loyaly.ai / loyaly-platform-2026 (Companies only)\n'
printf ' TeNext owner suriya@tenext.in / tenext-2026 (Shops, Live, Cameras, Customers, Reports)\n'
step "6. Run"
echo " http://127.0.0.1:${PORT}"
exec "./$STATE/bv-server"