Add registration toggle and password change; genericise deploy

Two things the running instance needed.

**Registration toggle.** Admin -> General, backed by a new settings table
group rather than the environment. LEMBAS_ALLOW_SIGNUP now seeds only the
initial value: once an administrator saves the setting, the stored value
wins. The alternative -- environment always winning -- means a toggle in
the UI silently reverts on the next restart, which is worse than not
offering one. Closing registration also removes the "Create one" link
from the sign-in page, so the link never leads somewhere that refuses.

**Password change**, on the user settings page. Changing a password
revokes every other session and immediately re-issues a cookie for the
current one: if the reason for the change is that somebody else knows
the password, leaving their session alive defeats the point, but signing
the user out of the tab they are standing in is merely rude.

**deploy/ is now host-agnostic.** This repository is public, so the unit
and vhost became templates with __PREFIX__ / __SITE_HOST__ / __APP_PORT__
substituted at install time, and every path, hostname and port moved to
environment variables. REPO_URL defaults to the checkout's own origin so
a fork deploys itself. Machine-specific values belong in private notes,
not here -- CLAUDE.md now says so.

83 tests, ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-07-21 11:14:33 +02:00
parent dd9e0e9440
commit ba2fb1e13d
17 changed files with 727 additions and 153 deletions
+67 -37
View File
@@ -1,28 +1,52 @@
#!/usr/bin/env bash
# Install LLeMbas as a system service behind nginx at https://chat.lan.
# Install LLeMbas as a system service behind nginx with a self-signed cert.
#
# Follows the conventions already used on this box for llama-swap and comfyui:
# a dedicated service user whose home lives on /home (the root LV is only
# 50 GB) and is bind-mounted to /srv/<name>, a system unit so it survives
# logout, and an nginx vhost with a self-signed cert.
# Creates a dedicated service user, a virtualenv, a systemd unit and an nginx
# vhost. Idempotent: safe to re-run. To deploy new code afterwards use
# update.sh, which is what a `git push` should be followed by.
#
# Idempotent: safe to re-run. To deploy new code afterwards use update.sh,
# which is what a `git push` should be followed by.
# Everything is configurable from the environment:
#
# SITE_HOST=chat.example ./deploy/install.sh # vhost name
# APP_PORT=8080 # loopback port
# PREFIX=/srv/lembas # install root
# HOME_DIR=/home/lembas # service user's home
# REPO_URL=... # defaults to this checkout's origin
#
# PREFIX defaults to a bind mount of HOME_DIR rather than living directly under
# /srv, because on many machines the root filesystem is small and the venv plus
# database belong on the larger /home volume. Set PREFIX=HOME_DIR to skip that.
set -euo pipefail
REPO_URL="${LEMBAS_REPO_URL:-https://git.houmeres.sk/Houmeres/LLeMbas.git}"
HERE="$(dirname "$(readlink -f "$0")")"
SITE_HOST="${SITE_HOST:-lembas.local}"
APP_PORT="${APP_PORT:-8080}"
SERVICE_USER="${SERVICE_USER:-lembas}"
HOME_DIR="${HOME_DIR:-/home/lembas}"
PREFIX="${PREFIX:-/srv/lembas}"
BRANCH="${LEMBAS_BRANCH:-main}"
SERVICE_USER=lembas
HOME_DIR=/home/lembas
PREFIX=/srv/lembas
# Default to wherever this checkout came from, so a fork deploys itself.
REPO_URL="${REPO_URL:-$(git -C "$HERE" remote get-url origin 2>/dev/null || true)}"
APP="$PREFIX/app"
VENV="$PREFIX/venv"
ENV_FILE="$PREFIX/lembas.env"
HERE="$(dirname "$(readlink -f "$0")")"
if [[ -z "$REPO_URL" ]]; then
echo "Could not determine REPO_URL. Set it explicitly." >&2
exit 1
fi
echo "== plan =="
echo " host : https://$SITE_HOST -> 127.0.0.1:$APP_PORT"
echo " user : $SERVICE_USER ($HOME_DIR)"
echo " prefix : $PREFIX"
echo " repo : $REPO_URL ($BRANCH)"
echo "== service user =="
# --system: no ageing, no mail spool. Home under /home, not /var/lib, so the
# venv and database sit on the big volume.
# venv and database sit on the larger volume.
if ! getent passwd "$SERVICE_USER" >/dev/null; then
sudo useradd --system --create-home --home-dir "$HOME_DIR" \
--shell /usr/bin/nologin --comment "LLeMbas" "$SERVICE_USER"
@@ -31,12 +55,14 @@ else
fi
sudo chmod 755 "$HOME_DIR"
echo "== /srv/lembas bind-mount onto /home =="
sudo mkdir -p "$PREFIX"
grep -q "^$HOME_DIR[[:space:]]" /etc/fstab \
|| echo "$HOME_DIR $PREFIX none bind 0 0" | sudo tee -a /etc/fstab >/dev/null
sudo systemctl daemon-reload
mountpoint -q "$PREFIX" || sudo mount "$PREFIX"
if [[ "$PREFIX" != "$HOME_DIR" ]]; then
echo "== $PREFIX bind-mount onto $HOME_DIR =="
sudo mkdir -p "$PREFIX"
grep -q "^$HOME_DIR[[:space:]]" /etc/fstab \
|| echo "$HOME_DIR $PREFIX none bind 0 0" | sudo tee -a /etc/fstab >/dev/null
sudo systemctl daemon-reload
mountpoint -q "$PREFIX" || sudo mount "$PREFIX"
fi
echo "== checkout =="
if [[ ! -d "$APP/.git" ]]; then
@@ -53,8 +79,8 @@ sudo -u "$SERVICE_USER" "$VENV/bin/pip" install --quiet --upgrade pip
sudo -u "$SERVICE_USER" "$VENV/bin/pip" install --quiet -e "$APP"
echo "== environment =="
# Generated once and never regenerated: rotating LEMBAS_SECRET_KEY would sign
# every user out and make the stored API keys unreadable.
# Generated once and never regenerated: rotating LEMBAS_SECRET_KEY signs every
# user out AND makes the stored upstream API keys unreadable.
if [[ ! -f "$ENV_FILE" ]]; then
KEY=$("$VENV/bin/python" -c "import secrets; print(secrets.token_urlsafe(48))")
sudo tee "$ENV_FILE" >/dev/null <<EOF
@@ -63,9 +89,9 @@ if [[ ! -f "$ENV_FILE" ]]; then
# Changing it signs everyone out and makes stored API keys unreadable.
LEMBAS_SECRET_KEY=$KEY
LEMBAS_DATA_DIR=$PREFIX/data
# Loopback only: reachable through the nginx chat.lan vhost, never direct.
# Loopback only: reachable through the nginx vhost, never directly.
LEMBAS_HOST=127.0.0.1
LEMBAS_PORT=8080
LEMBAS_PORT=$APP_PORT
LEMBAS_LOG_LEVEL=info
LEMBAS_ALLOW_SIGNUP=true
LEMBAS_DEFAULT_THEME=moria
@@ -80,30 +106,34 @@ fi
sudo install -d -o "$SERVICE_USER" -g "$SERVICE_USER" -m 750 "$PREFIX/data"
echo "== systemd unit =="
sudo install -Dm644 "$HERE/lembas.service" /etc/systemd/system/lembas.service
sed -e "s|__PREFIX__|$PREFIX|g" -e "s|__SERVICE_USER__|$SERVICE_USER|g" \
"$HERE/lembas.service" | sudo tee /etc/systemd/system/lembas.service >/dev/null
sudo systemctl daemon-reload
echo "== self-signed cert for chat.lan =="
echo "== self-signed cert for $SITE_HOST =="
sudo mkdir -p /etc/nginx/ssl
if [[ ! -f /etc/nginx/ssl/chat.lan.crt ]]; then
if [[ ! -f "/etc/nginx/ssl/$SITE_HOST.crt" ]]; then
sudo openssl req -x509 -newkey rsa:2048 -nodes \
-keyout /etc/nginx/ssl/chat.lan.key -out /etc/nginx/ssl/chat.lan.crt \
-days 3650 -subj "/CN=chat.lan" -addext "subjectAltName=DNS:chat.lan"
sudo chmod 600 /etc/nginx/ssl/chat.lan.key
sudo chmod 644 /etc/nginx/ssl/chat.lan.crt
-keyout "/etc/nginx/ssl/$SITE_HOST.key" -out "/etc/nginx/ssl/$SITE_HOST.crt" \
-days 3650 -subj "/CN=$SITE_HOST" -addext "subjectAltName=DNS:$SITE_HOST"
sudo chmod 600 "/etc/nginx/ssl/$SITE_HOST.key"
sudo chmod 644 "/etc/nginx/ssl/$SITE_HOST.crt"
fi
echo "== nginx vhost =="
sudo install -Dm644 "$HERE/chat.lan.nginx.conf" /etc/nginx/conf.d/chat.lan.conf
sed -e "s|__SITE_HOST__|$SITE_HOST|g" -e "s|__APP_PORT__|$APP_PORT|g" \
"$HERE/nginx-vhost.conf" | sudo tee "/etc/nginx/conf.d/$SITE_HOST.conf" >/dev/null
sudo nginx -t
sudo systemctl reload nginx
echo "== local name resolution =="
# chat.lan is in Pi-hole, but this box asks the router first and the router's
# dnsmasq is authoritative for .lan without forwarding it on -- same reason
# comfy.lan needs a hosts entry. Harmless if DNS already resolves it.
grep -q 'chat\.lan' /etc/hosts \
|| printf '127.0.0.1\tchat.lan\n::1\t\tchat.lan\n' | sudo tee -a /etc/hosts >/dev/null
# Only useful when the LAN's DNS does not already answer for this name.
if ! getent hosts "$SITE_HOST" >/dev/null; then
printf '127.0.0.1\t%s\n::1\t\t%s\n' "$SITE_HOST" "$SITE_HOST" | sudo tee -a /etc/hosts >/dev/null
echo " added $SITE_HOST to /etc/hosts"
else
echo " $SITE_HOST already resolves"
fi
echo "== enable service =="
sudo systemctl enable --now lembas
@@ -111,5 +141,5 @@ sleep 2
sudo systemctl --no-pager --lines=0 status lembas || true
echo
echo "LLeMbas is up at https://chat.lan (self-signed cert; accept the warning)"
echo "LLeMbas is up at https://$SITE_HOST (self-signed cert; accept the warning)"
echo "Create the first account -- it becomes the administrator."