Files
JakeBreath 1c6735cde8 Include the desktop origin in generated CORS settings
The backend only allows app://j621 through CORS_ALLOWED_ORIGINS, so
gen_env.sh now always writes that origin (plus the split-deploy frontend
when one is given) and --update keeps hand-added origins instead of
overwriting the list.
2026-09-20 19:44:56 -05:00

164 lines
5.4 KiB
Bash
Executable File

#!/bin/bash
# Generate deploy/.env from .env.example with fresh secrets.
#
# ./gen_env.sh # interactive: asks for the auth key/hostnames
# ./gen_env.sh --no-prompt # secrets + defaults only
# ./gen_env.sh --update # refresh hostnames/authkey, KEEP existing secrets
# ./gen_env.sh --force # regenerate everything (new SECRET_KEY!)
#
# SECRET_KEY and the database passwords come from openssl. Rotating
# SECRET_KEY invalidates signed media URLs and stored e621 API keys, which is
# why --update keeps it.
set -euo pipefail
cd "$(dirname "$0")"
FORCE=0
UPDATE=0
NO_PROMPT=0
TS_AUTHKEY=""
FQDN=""
FRONTEND=""
DEFAULT_FQDN="j621.rainbow-herring.ts.net"
usage() {
sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'
}
while [ $# -gt 0 ]; do
case "$1" in
--force) FORCE=1 ;;
--update) UPDATE=1 ;;
--no-prompt) NO_PROMPT=1 ;;
--ts-authkey) TS_AUTHKEY="${2:?missing value}"; shift ;;
--hostname) FQDN="${2:?missing value}"; shift ;;
--frontend) FRONTEND="${2:?missing value}"; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
esac
shift
done
command -v openssl >/dev/null || { echo "openssl is required." >&2; exit 1; }
command -v python3 >/dev/null || { echo "python3 is required." >&2; exit 1; }
[ -f .env.example ] || { echo "Run me from the deploy/ directory." >&2; exit 1; }
if [ -f .env ] && [ "$FORCE" -eq 0 ] && [ "$UPDATE" -eq 0 ]; then
echo "deploy/.env already exists."
echo " --update keeps the existing secrets and just refreshes the rest"
echo " --force regenerates everything, including SECRET_KEY and DB passwords"
exit 1
fi
existing() { grep -E "^$1=" .env 2>/dev/null | head -1 | cut -d= -f2- || true; }
gen_key() { openssl rand -base64 48 | tr -d '\n'; }
gen_password() { openssl rand -hex 24; }
if [ "$UPDATE" -eq 1 ] && [ -f .env ]; then
SECRET_KEY="$(existing SECRET_KEY)"
DB_PASSWORD="$(existing DB_PASSWORD)"
DB_ROOT_PASSWORD="$(existing DB_ROOT_PASSWORD)"
TS_AUTHKEY="${TS_AUTHKEY:-$(existing TS_AUTHKEY)}"
# TS_HOSTNAME is the short label; the full FQDN lives in ALLOWED_HOSTS.
EXISTING_HOSTS="$(existing ALLOWED_HOSTS)"
FQDN="${FQDN:-${EXISTING_HOSTS%%,*}}"
fi
# Fill whatever is still missing.
SECRET_KEY="${SECRET_KEY:-$(gen_key)}"
DB_PASSWORD="${DB_PASSWORD:-$(gen_password)}"
DB_ROOT_PASSWORD="${DB_ROOT_PASSWORD:-$(gen_password)}"
if [ "$NO_PROMPT" -eq 0 ]; then
if [ -z "$TS_AUTHKEY" ]; then
read -rp "Tailscale auth key (tskey-..., Enter to fill in later): " TS_AUTHKEY
fi
if [ -z "$FQDN" ]; then
read -rp "Tailnet hostname of this deployment [$DEFAULT_FQDN]: " FQDN
fi
FQDN="${FQDN:-$DEFAULT_FQDN}"
if [ -z "$FRONTEND" ]; then
read -rp "Frontend hostname for the split deploys (optional, Enter to skip): " FRONTEND
fi
fi
FQDN="${FQDN:-$DEFAULT_FQDN}"
# Derive the rest from the tailnet hostname.
TS_HOSTNAME="${FQDN%%.*}"
ALLOWED_HOSTS="$FQDN,localhost,127.0.0.1"
# Cross-origin access: the optional split-deploy frontend plus the desktop
# shell, which is always a different origin from the backend. On --update the
# existing list is kept, so hand-added origins survive.
CORS_ALLOWED_ORIGINS=""
add_origin() {
[ -n "${1:-}" ] || return 0
case ",$CORS_ALLOWED_ORIGINS," in
*",$1,"*) ;;
*) CORS_ALLOWED_ORIGINS="${CORS_ALLOWED_ORIGINS:+$CORS_ALLOWED_ORIGINS,}$1" ;;
esac
}
if [ "$UPDATE" -eq 1 ]; then
while IFS= read -r origin; do
add_origin "$origin"
done < <(existing CORS_ALLOWED_ORIGINS | tr ',' '\n')
fi
[ -n "$FRONTEND" ] && add_origin "https://$FRONTEND"
add_origin "app://j621"
CSRF_TRUSTED_ORIGINS="${FRONTEND:+https://$FRONTEND}"
J621_SECRET_KEY="$SECRET_KEY" \
J621_DB_PASSWORD="$DB_PASSWORD" \
J621_DB_ROOT_PASSWORD="$DB_ROOT_PASSWORD" \
J621_TS_AUTHKEY="$TS_AUTHKEY" \
J621_TS_HOSTNAME="$TS_HOSTNAME" \
J621_ALLOWED_HOSTS="$ALLOWED_HOSTS" \
J621_CORS_ALLOWED_ORIGINS="$CORS_ALLOWED_ORIGINS" \
J621_CSRF_TRUSTED_ORIGINS="$CSRF_TRUSTED_ORIGINS" \
python3 - <<'PY'
import os
import re
from pathlib import Path
text = Path(".env.example").read_text()
def apply(name):
value = os.environ.get(f"J621_{name}")
if not value:
return text
line = f"{name}={value}"
pattern = re.compile(rf"^(?:# )?{re.escape(name)}=.*$", re.M)
if pattern.search(text):
return pattern.sub(line, text, count=1)
return text + f"\n{line}\n"
for name in (
"SECRET_KEY",
"TS_AUTHKEY",
"TS_HOSTNAME",
"ALLOWED_HOSTS",
"CORS_ALLOWED_ORIGINS",
"CSRF_TRUSTED_ORIGINS",
"DB_PASSWORD",
"DB_ROOT_PASSWORD",
):
text = apply(name)
text = re.sub(r"^DEBUG=.*$", "DEBUG=False", text, count=1, flags=re.M)
Path(".env").write_text(text)
PY
chmod 600 .env
echo
echo "Wrote deploy/.env (mode 600):"
echo " SECRET_KEY $([ "$UPDATE" -eq 1 ] && echo 'kept from the existing file' || echo 'generated with openssl')"
echo " DB passwords $([ "$UPDATE" -eq 1 ] && echo 'kept from the existing file' || echo 'generated with openssl')"
echo " TS_HOSTNAME $TS_HOSTNAME"
echo " ALLOWED_HOSTS $ALLOWED_HOSTS"
[ -n "$CORS_ALLOWED_ORIGINS" ] && echo " cross-origin $CORS_ALLOWED_ORIGINS"
[ -z "$TS_AUTHKEY" ] && echo " TS_AUTHKEY still empty - paste your Tailscale auth key before starting"
echo
echo "Next: docker compose -f compose.yml up -d (or a compose.tailnet*.yml variant)"