Files
Vorrania/deploy/install.sh
T
ScarriffleandClaude Opus 5 3d0ff54bf5 Anmeldung wahlweise ueber OpenID Connect (Authentik)
Neben dem Passwort-Login gibt es jetzt einen zweiten, gleichberechtigten Weg:
OpenID Connect gegen einen selbst gehosteten Anmeldedienst. Ohne Konfiguration
aendert sich nichts - keine SSO-Knoepfe, Passwort-Login wie bisher.

Der Anmeldedienst ist NUR fuer die Anmeldung zustaendig. Danach stellt Vorrania
sein eigenes Token aus wie beim Passwort-Login, deshalb bleiben deps.py, die
Rollenpruefung und die langlebigen API-Tokens unveraendert.

Backend
- Anbieter kommen aus der Umgebung (OIDC_PROVIDERS + OIDC_<KUERZEL>_*). Ein
  zweiter Anbieter ist damit reine Konfiguration, ohne Codeaenderung.
- Browser-Weg (vertraulicher Client) und App-Weg (oeffentlicher Client, PKCE)
  enden beide im selben Token-Schema.
- state/nonce/PKCE-Verifier liegen in einer kurzlebigen Tabelle statt in einem
  Cookie: Starlettes CORS-Schicht spiegelt bei allow_origins=["*"] zusammen mit
  allow_credentials die Herkunft zurueck, sobald ein Request ein Cookie traegt.
  Ohne Cookie entsteht diese Wechselwirkung erst gar nicht - und der Ablauf
  bleibt mit der db-Fixture pruefbar.
- Zuordnung ueber die sub des Anbieters; die Erstverknuepfung laeuft ueber die
  BESTAETIGTE E-Mail-Adresse. Rollen kommen nie aus dem Anbieter, und ein
  gleichlautender preferred_username uebernimmt nie ein bestehendes Konto.
- users.password_hash darf jetzt leer sein (reine SSO-Konten). verify_password
  faengt das ab, sonst antwortete der Login mit 500 statt 401.

Verknuepfen bestehender Konten
- Neue Seite "Mein Konto" (fuer alle Benutzer, nicht nur Admins): verknuepfen
  und wieder loesen. Die Berechtigung stammt aus der laufenden Sitzung, es
  braucht also weder eine hinterlegte Adresse noch Admin-Rechte.
- Der Start laeuft ueber POST, weil eine Seitennavigation das Bearer-Token
  nicht mitschicken kann.
- Die letzte Anmeldemoeglichkeit eines Kontos laesst sich nicht loesen.

iOS
- AppAuth (erste Paketabhaengigkeit) mit PKCE im System-Browser, nie in einem
  eingebetteten WebView, und ohne Client-Secret.
- offline_access nur im App-Weg: der Browser-Weg braucht kein Refresh-Token und
  bekaeme sonst einen ueberfluessigen Zustimmungsdialog.
- Anmeldezustand je Serverprofil im Keychain; abgelaufene Token erneuert die App
  still im Hintergrund. Gleichzeitige 401er teilen sich eine Erneuerung.

Deployment
- docker-compose reicht die ganze .env durch. Einzeln aufzuzaehlen ginge nur mit
  fest verdrahteten Kuerzeln - genau das soll ein weiterer Anbieter nicht
  erfordern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 21:55:38 +02:00

278 lines
9.0 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
#
# Vorrania – Installer für selfhostbare Lebensmittel-Lagerverwaltung.
# Installiert bei Bedarf Docker und startet den kompletten Stack via Docker Compose.
#
# Nutzung:
# ./install.sh interaktiv (fragt Admin-Passwort etc.)
# ./install.sh --yes nicht-interaktiv (erzeugt Zufalls-Admin-Passwort)
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="$SCRIPT_DIR/.env"
EXAMPLE_FILE="$SCRIPT_DIR/.env.example"
ASSUME_YES=0
[[ "${1:-}" == "--yes" || "${1:-}" == "-y" ]] && ASSUME_YES=1
info() { printf '\033[0;32m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[0;33m[!]\033[0m %s\n' "$*"; }
error() { printf '\033[0;31m[x]\033[0m %s\n' "$*" >&2; }
rand() { LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c "${1:-32}"; }
# root/sudo bestimmen
if [[ "$(id -u)" -eq 0 ]]; then
SUDO=""
elif command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
else
SUDO=""
fi
# --- Paketinstallation (paketmanager-unabhängig) ------------------------------
pkg_install() {
local pkg="$1"
if command -v apt-get >/dev/null 2>&1; then
$SUDO apt-get update -y && $SUDO apt-get install -y "$pkg"
elif command -v dnf >/dev/null 2>&1; then
$SUDO dnf install -y "$pkg"
elif command -v yum >/dev/null 2>&1; then
$SUDO yum install -y "$pkg"
elif command -v apk >/dev/null 2>&1; then
$SUDO apk add --no-cache "$pkg"
elif command -v pacman >/dev/null 2>&1; then
$SUDO pacman -Sy --noconfirm "$pkg"
elif command -v zypper >/dev/null 2>&1; then
$SUDO zypper install -y "$pkg"
else
return 1
fi
}
# --- curl sicherstellen (wird für die Docker-Installation gebraucht) ----------
ensure_curl() {
if command -v curl >/dev/null 2>&1; then
return 0
fi
warn "curl ist nicht installiert – wird jetzt automatisch installiert …"
if ! pkg_install curl; then
error "curl konnte nicht installiert werden (kein bekannter Paketmanager gefunden). Bitte curl manuell installieren und erneut ausführen."
exit 1
fi
command -v curl >/dev/null 2>&1 || { error "curl-Installation fehlgeschlagen."; exit 1; }
info "curl installiert."
}
# --- Docker prüfen / installieren ---------------------------------------------
ensure_docker() {
if command -v docker >/dev/null 2>&1; then
info "Docker gefunden: $(docker --version)"
else
warn "Docker ist nicht installiert."
if [[ "$ASSUME_YES" -eq 1 ]]; then
reply="j"
else
read -r -p "Soll Docker jetzt automatisch installiert werden? [J/n] " reply
reply="${reply:-j}"
fi
case "$reply" in
[JjYy]*)
ensure_curl
info "Installiere Docker über get.docker.com …"
curl -fsSL https://get.docker.com | sh
;;
*)
error "Docker wird benötigt. Bitte manuell installieren und erneut ausführen."
exit 1
;;
esac
fi
# docker compose (v2) oder docker-compose (v1) ermitteln
if docker compose version >/dev/null 2>&1; then
COMPOSE="docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE="docker-compose"
else
error "Docker Compose wurde nicht gefunden. Bitte Docker Compose v2 installieren."
exit 1
fi
info "Verwende: $COMPOSE"
if ! docker info >/dev/null 2>&1; then
error "Docker-Daemon nicht erreichbar. Läuft Docker? Ggf. 'sudo' nötig oder Nutzer zur 'docker'-Gruppe hinzufügen."
exit 1
fi
}
# --- Beispiel-.env sicherstellen ----------------------------------------------
ensure_example() {
if [[ -f "$EXAMPLE_FILE" ]]; then
return
fi
info "Lege Beispiel-Konfiguration an: $EXAMPLE_FILE"
cat > "$EXAMPLE_FILE" <<'EOF'
# Kopiere diese Datei zu ".env" und passe die Werte an.
# install.sh erzeugt eine .env automatisch mit sicheren Zufallswerten.
# Port, unter dem die Web-UI erreichbar ist
WEB_PORT=8080
# Datenbank
POSTGRES_USER=vorrania
POSTGRES_PASSWORD=vorrania
POSTGRES_DB=vorrania
# Sicherheit – im Betrieb unbedingt ändern!
JWT_SECRET=change-me-please
# Erster Admin-Benutzer (wird nur beim allerersten Start angelegt)
ADMIN_USERNAME=admin
ADMIN_PASSWORD=changeme
# CORS (bei Betrieb hinter dem mitgelieferten nginx nicht nötig)
CORS_ORIGINS=*
# Oeffentliche Adresse dieser Instanz, z. B. https://vorrania.example.com
# Wird nur fuer Single Sign-on gebraucht (Rueckleit-Adresse) - sonst leer lassen.
PUBLIC_BASE_URL=
# ---------------------------------------------------------------------------
# Single Sign-on (OpenID Connect), z. B. gegen Authentik. Leer = aus; dann
# bleibt es beim Passwort-Login und nichts aendert sich.
#
# Ein weiterer Anbieter ist reine Konfiguration: Kuerzel in OIDC_PROVIDERS
# ergaenzen (kommagetrennt) und einen zweiten Block mit dessen Praefix anlegen.
# ---------------------------------------------------------------------------
OIDC_PROVIDERS=
#OIDC_PROVIDERS=authentik
#OIDC_AUTHENTIK_DISPLAY_NAME=Authentik
#OIDC_AUTHENTIK_DISCOVERY_URL=https://authentik.example.com/application/o/vorrania/.well-known/openid-configuration
# Vertraulicher Client (Web-Oberflaeche) - mit Secret:
#OIDC_AUTHENTIK_CLIENT_ID=
#OIDC_AUTHENTIK_CLIENT_SECRET=
# Oeffentlicher Client (iOS-App) - ohne Secret, mit PKCE:
#OIDC_AUTHENTIK_NATIVE_CLIENT_IDS=vorrania-ios
# Neue Konten automatisch anlegen (Rolle "Benutzer"); false = nur bereits
# vorhandene Konten duerfen sich per SSO anmelden.
#OIDC_AUTHENTIK_AUTO_CREATE=true
# Nur bei abweichendem Reverse-Proxy-Pfad noetig. Ohne diese Zeile bildet der
# Server die Adresse als <PUBLIC_BASE_URL>/api/auth/oidc/authentik/callback.
#OIDC_AUTHENTIK_REDIRECT_URI=
EOF
}
# --- Hinweis auf die Konfigurationsdatei --------------------------------------
env_hint() {
echo
info "Konfiguration liegt in: $ENV_FILE"
echo " Vorlage/Beispiel: $EXAMPLE_FILE"
warn "Dort kannst du bei Bedarf Werte anpassen: WEB_PORT (Port der Web-UI),"
warn "ADMIN_USERNAME/ADMIN_PASSWORD sowie die Datenbank- und JWT-Secrets."
warn "Fuer Single Sign-on (z. B. Authentik): PUBLIC_BASE_URL und OIDC_* -"
warn "die Vorlage in $EXAMPLE_FILE zeigt einen vollstaendigen Beispielblock."
warn "Nach Änderungen an der .env neu starten: $COMPOSE --env-file .env up -d"
}
# --- .env erzeugen ------------------------------------------------------------
create_env() {
ensure_example
if [[ -f "$ENV_FILE" ]]; then
info ".env existiert bereits – bestehende Konfiguration wird verwendet."
env_hint
return
fi
info "Erzeuge Konfiguration (.env) aus den Beispiel-Vorgaben …"
local web_port="8080"
local admin_user="admin"
local admin_pass
admin_pass="$(rand 16)"
if [[ "$ASSUME_YES" -eq 0 ]]; then
read -r -p "Port für die Web-UI [8080]: " input; web_port="${input:-8080}"
read -r -p "Admin-Benutzername [admin]: " input; admin_user="${input:-admin}"
read -r -s -p "Admin-Passwort [leer = zufällig generieren]: " input; echo
[[ -n "$input" ]] && admin_pass="$input"
fi
cat > "$ENV_FILE" <<EOF
WEB_PORT=$web_port
POSTGRES_USER=vorrania
POSTGRES_PASSWORD=$(rand 24)
POSTGRES_DB=vorrania
JWT_SECRET=$(rand 48)
ADMIN_USERNAME=$admin_user
ADMIN_PASSWORD=$admin_pass
CORS_ORIGINS=*
# Fuer Single Sign-on ausfuellen (siehe .env.example). Leer = Passwort-Login.
PUBLIC_BASE_URL=
OIDC_PROVIDERS=
EOF
chmod 600 "$ENV_FILE"
GENERATED_ADMIN_USER="$admin_user"
GENERATED_ADMIN_PASS="$admin_pass"
GENERATED_WEB_PORT="$web_port"
env_hint
}
# --- Warten bis das Backend über die Web-UI erreichbar ist --------------------
wait_for_backend() {
local port="$1" tries=30 i
ensure_curl
info "Warte darauf, dass das Backend bereit ist …"
for ((i = 1; i <= tries; i++)); do
if curl -fsS "http://localhost:${port}/api/health" >/dev/null 2>&1; then
info "Backend ist erreichbar. ✅"
return 0
fi
sleep 2
done
warn "Backend antwortet nach ~$((tries * 2))s noch nicht – das erklärt einen 502 in der Web-UI."
warn "Bitte die Logs prüfen: ( cd \"$SCRIPT_DIR\" && $COMPOSE logs backend )"
return 1
}
# --- Start --------------------------------------------------------------------
main() {
info "Vorrania-Installer startet …"
ensure_docker
create_env
info "Baue Container (das kann beim ersten Mal einige Minuten dauern) …"
( cd "$SCRIPT_DIR" && $COMPOSE --env-file "$ENV_FILE" build )
info "Starte Dienste …"
( cd "$SCRIPT_DIR" && $COMPOSE --env-file "$ENV_FILE" up -d )
# Werte aus .env lesen (auch bei bestehender .env)
# shellcheck disable=SC1090
local web_port admin_user
web_port="$(grep -E '^WEB_PORT=' "$ENV_FILE" | cut -d= -f2-)"
admin_user="$(grep -E '^ADMIN_USERNAME=' "$ENV_FILE" | cut -d= -f2-)"
wait_for_backend "$web_port" || true
echo
info "Fertig! 🎉"
echo " Web-UI: http://localhost:${web_port} (bzw. http://<server-ip>:${web_port})"
echo " Admin-Login: ${admin_user}"
if [[ -n "${GENERATED_ADMIN_PASS:-}" ]]; then
echo " Admin-Passwort: ${GENERATED_ADMIN_PASS}"
warn "Bitte dieses Passwort notieren – es wird nicht erneut angezeigt."
else
echo " Admin-Passwort: (wie in .env gesetzt)"
fi
echo
echo " Logs: ( cd $SCRIPT_DIR && $COMPOSE logs -f )"
echo " Stoppen: ( cd $SCRIPT_DIR && $COMPOSE down )"
}
main "$@"