Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7407f4bc00 | ||
|
|
635722089e |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm"
|
name = "tesm"
|
||||||
version = "2.0.5"
|
version = "2.0.7"
|
||||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -23,7 +23,20 @@ from .permissions import PERMISSIONS, ROLE_PRESETS
|
|||||||
from .schema import TESM_MIGRATIONS
|
from .schema import TESM_MIGRATIONS
|
||||||
from .settings_spec import TESM_SETTINGS
|
from .settings_spec import TESM_SETTINGS
|
||||||
|
|
||||||
__version__ = "2.0.2"
|
#: Version aus den Metadaten des installierten Pakets -- die einzige Quelle
|
||||||
|
#: ist ``pyproject.toml``. Vorher stand hier eine feste Zahl, und die
|
||||||
|
#: Fusszeile zeigte vier Releases lang 2.0.2.
|
||||||
|
#:
|
||||||
|
#: Im Quellbaum ohne ``pip install`` gibt es keine Metadaten; dann steht dort
|
||||||
|
#: "unbekannt" statt einer erfundenen Nummer. In einem editierbaren Install
|
||||||
|
#: zeigt sie den Stand des letzten ``pip install -e`` -- also die Version, die
|
||||||
|
#: tatsaechlich installiert ist.
|
||||||
|
try: # pragma: no cover - haengt an der Installationsart
|
||||||
|
from importlib.metadata import PackageNotFoundError, version as _paketversion
|
||||||
|
|
||||||
|
__version__ = _paketversion("tesm")
|
||||||
|
except (ImportError, PackageNotFoundError): # pragma: no cover
|
||||||
|
__version__ = "unbekannt"
|
||||||
|
|
||||||
APP_KEY = "tesm"
|
APP_KEY = "tesm"
|
||||||
APP_NAME = "TESM"
|
APP_NAME = "TESM"
|
||||||
@@ -32,10 +45,12 @@ APP_EDITION = "Geraeteverwaltung"
|
|||||||
#: Funktionszeile unter der Wortmarke auf der Anmeldeseite. Uebernommen vom
|
#: Funktionszeile unter der Wortmarke auf der Anmeldeseite. Uebernommen vom
|
||||||
#: Vorgaenger, wo sie Teil des Hintergrundmotivs war -- sie sagt in einer Zeile,
|
#: Vorgaenger, wo sie Teil des Hintergrundmotivs war -- sie sagt in einer Zeile,
|
||||||
#: was die Anwendung tut, und das gehoert auf die erste Seite.
|
#: was die Anwendung tut, und das gehoert auf die erste Seite.
|
||||||
AUTH_TAGLINE = (
|
#: Zeile unter der Wortmarke auf den Anmeldeseiten. Kurz gehalten: sie ist
|
||||||
"UEBERWACHEN · BOOTEN · ANBINDEN (DHCP) · "
|
#: eine Marke, keine Funktionsliste. "(PXE)" stand ausserdem fuer etwas, das
|
||||||
"BETRIEBSSYSTEM GEBEN (PXE)"
|
#: es noch nicht gibt -- das Anmeldebanner hat es aus demselben Grund nie
|
||||||
)
|
#: gezeigt. Wortgleich in ``deploy/tesm-motd``; ein Hygienetest haelt beides
|
||||||
|
#: zusammen.
|
||||||
|
AUTH_TAGLINE = "ÜBERWACHEN · BOOTEN · ANBINDEN"
|
||||||
ENV_PREFIX = "TESM_"
|
ENV_PREFIX = "TESM_"
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -0,0 +1,359 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# Benennt eine Installation um -- aus einer Instanz wird die Hauptinstallation
|
||||||
|
# (oder umgekehrt).
|
||||||
|
#
|
||||||
|
# sudo bash deploy/rename.sh --app tesm --from tesm-opus --to tesm
|
||||||
|
# sudo bash deploy/rename.sh --app tesm-license --from tesm-license-opus \
|
||||||
|
# --to tesm-license
|
||||||
|
#
|
||||||
|
# Der Name steckt an mehr Stellen, als man beim Verschieben eines Verzeichnisses
|
||||||
|
# erwartet, und drei davon sind leicht zu vergessen:
|
||||||
|
#
|
||||||
|
# * ``SITE_KEY`` in der Umgebungsdatei -- daraus entsteht der Name des
|
||||||
|
# Sitzungscookies. Bleibt er alt, heisst das Cookie weiter wie die alte
|
||||||
|
# Instanz.
|
||||||
|
# * ``web_cert_path``/``web_key_path``/``web_acme_webroot`` in der Datenbank.
|
||||||
|
# Sie enthalten den Namen; ein Zertifikat unter dem alten Pfad findet nginx
|
||||||
|
# nach dem Umzug nicht mehr.
|
||||||
|
# * ``server_name`` der nginx-Site. install.sh schreibt die Datei neu, sobald
|
||||||
|
# sich die alias-Pfade aendern -- und die aendern sich hier zwangslaeufig.
|
||||||
|
# Ohne den Namen aus der Datenbank stuenden danach zwei Sites mit ``_`` auf
|
||||||
|
# Port 80 und die erste gewinnt.
|
||||||
|
#
|
||||||
|
# Belegt der Zielname noch eine Installation der Fassung 1.x, wird sie beiseite
|
||||||
|
# geschoben: Verzeichnis nach ``<ziel>-<suffix>``, ihre systemd-Units und ihre
|
||||||
|
# nginx-Site als Kopie hinein. Danach ist der Name frei und nichts auf dem
|
||||||
|
# System zeigt mehr ins Leere.
|
||||||
|
#
|
||||||
|
# Alle angemeldeten Benutzer werden abgemeldet: das Sitzungscookie heisst
|
||||||
|
# danach anders.
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP=""
|
||||||
|
FROM=""
|
||||||
|
TO=""
|
||||||
|
SUFFIX="sonnet5"
|
||||||
|
ASSUME_YES=0
|
||||||
|
DRY_RUN=0
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
COLOR_OK=$'\033[32m'; COLOR_WARN=$'\033[33m'; COLOR_ERR=$'\033[31m'; COLOR_OFF=$'\033[0m'
|
||||||
|
step() { printf '\n%s==>%s %s\n' "$COLOR_OK" "$COLOR_OFF" "$*"; }
|
||||||
|
warn() { printf '%s !!%s %s\n' "$COLOR_WARN" "$COLOR_OFF" "$*"; }
|
||||||
|
fail() { printf '%s !!%s %s\n' "$COLOR_ERR" "$COLOR_OFF" "$*" >&2; exit 1; }
|
||||||
|
info() { printf ' %s\n' "$*"; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Verwendung: rename.sh --app <tesm|tesm-license> --from <alt> --to <neu> [Optionen]
|
||||||
|
|
||||||
|
--app <name> Welche Anwendung (Pflicht)
|
||||||
|
--from <name> Bisheriger Installationsname, z.B. tesm-opus (Pflicht)
|
||||||
|
--to <name> Neuer Installationsname, z.B. tesm (Pflicht)
|
||||||
|
--suffix <text> Namenszusatz fuer eine beiseite geschobene Installation
|
||||||
|
der Fassung 1.x (Vorgabe: sonnet5)
|
||||||
|
--dry-run Nur zeigen, was geschehen wuerde
|
||||||
|
--yes Keine Rueckfrage
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--app) APP="${2:?}"; shift 2 ;;
|
||||||
|
--from) FROM="${2:?}"; shift 2 ;;
|
||||||
|
--to) TO="${2:?}"; shift 2 ;;
|
||||||
|
--suffix) SUFFIX="${2:?}"; shift 2 ;;
|
||||||
|
--dry-run) DRY_RUN=1; shift ;;
|
||||||
|
--yes|-y) ASSUME_YES=1; shift ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) usage; fail "unbekannte Option: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || { usage; fail "--app fehlt oder ist unbekannt"; }
|
||||||
|
[[ -n "$FROM" && -n "$TO" ]] || { usage; fail "--from und --to sind Pflicht"; }
|
||||||
|
[[ "$FROM" != "$TO" ]] || fail "--from und --to sind gleich"
|
||||||
|
[[ "$TO" =~ ^[a-z0-9-]{1,32}$ ]] || fail "ungueltiger Zielname: $TO"
|
||||||
|
[[ "$TO" == "$APP" || "$TO" == "$APP-"* ]] || fail "der Zielname muss mit '$APP' beginnen"
|
||||||
|
[[ "$(id -u)" -eq 0 ]] || fail "bitte mit sudo ausfuehren"
|
||||||
|
[[ -f "$SCRIPT_DIR/install.sh" ]] || fail "install.sh fehlt neben diesem Skript"
|
||||||
|
[[ -d "/srv/$FROM" ]] || fail "/srv/$FROM gibt es nicht"
|
||||||
|
[[ -f "/srv/$FROM/data/app.db" ]] || fail "/srv/$FROM sieht nicht nach einer Installation dieser Fassung aus"
|
||||||
|
|
||||||
|
ENV_PREFIX="TESM_"
|
||||||
|
[[ "$APP" == "tesm-license" ]] && ENV_PREFIX="TESM_LICENSE_"
|
||||||
|
|
||||||
|
# Alles, was den Namen traegt. Reihenfolge: erst Daten, dann Konfiguration.
|
||||||
|
VERSCHIEBEN=(
|
||||||
|
"/srv/$FROM:/srv/$TO"
|
||||||
|
"/var/log/$FROM:/var/log/$TO"
|
||||||
|
"/etc/tesm/$FROM.env:/etc/tesm/$TO.env"
|
||||||
|
"/var/www/$FROM-acme:/var/www/$TO-acme"
|
||||||
|
"/etc/tesm/certs/$FROM:/etc/tesm/certs/$TO"
|
||||||
|
)
|
||||||
|
ENTFERNEN=(
|
||||||
|
"/etc/systemd/system/$FROM.service"
|
||||||
|
"/etc/systemd/system/$FROM-monitor.service"
|
||||||
|
"/etc/nginx/sites-enabled/$FROM"
|
||||||
|
"/etc/nginx/sites-available/$FROM"
|
||||||
|
"/usr/local/bin/$FROM-admin"
|
||||||
|
"/etc/logrotate.d/$FROM"
|
||||||
|
"/etc/update-motd.d/50-$FROM"
|
||||||
|
)
|
||||||
|
|
||||||
|
# -- Webeinstellungen auslesen, bevor irgendetwas umzieht -------------------- #
|
||||||
|
# Mit dem System-Python und der Standardbibliothek: das venv zieht gleich mit
|
||||||
|
# um, und ein zusaetzliches Werkzeug (sqlite3) ist auf einem schlanken Server
|
||||||
|
# nicht garantiert.
|
||||||
|
lese_einstellung() {
|
||||||
|
python3 - "$1" "/srv/$FROM/data/app.db" <<'PY'
|
||||||
|
import sqlite3, sys
|
||||||
|
schluessel, datenbank = sys.argv[1], sys.argv[2]
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(f"file:{datenbank}?mode=ro", uri=True)
|
||||||
|
row = conn.execute("SELECT value FROM settings WHERE key=?", (schluessel,)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
except sqlite3.Error:
|
||||||
|
row = None
|
||||||
|
print((row[0] if row and row[0] is not None else "").strip())
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
DOMAIN="$(lese_einstellung web_domain || true)"
|
||||||
|
HTTPS_AN="$(lese_einstellung web_https_enabled || true)"
|
||||||
|
HSTS_AN="$(lese_einstellung web_hsts_enabled || true)"
|
||||||
|
CERT_ALT="$(lese_einstellung web_cert_path || true)"
|
||||||
|
|
||||||
|
step "Vorhaben"
|
||||||
|
info "Anwendung : $APP"
|
||||||
|
info "Alt : $FROM"
|
||||||
|
info "Neu : $TO"
|
||||||
|
info "Domain : ${DOMAIN:-(keine)}"
|
||||||
|
info "HTTPS : $([[ "$HTTPS_AN" == "1" ]] && echo "ja${CERT_ALT:+, Zertifikat $CERT_ALT}" || echo nein)"
|
||||||
|
for paar in "${VERSCHIEBEN[@]}"; do
|
||||||
|
quelle="${paar%%:*}"; ziel="${paar##*:}"
|
||||||
|
[[ -e "$quelle" ]] && info "verschieben: $quelle -> $ziel"
|
||||||
|
done
|
||||||
|
for pfad in "${ENTFERNEN[@]}"; do
|
||||||
|
[[ -e "$pfad" ]] && info "entfernen : $pfad (install.sh legt es unter dem neuen Namen neu an)"
|
||||||
|
done
|
||||||
|
|
||||||
|
# -- Was belegt den Zielnamen? ----------------------------------------------- #
|
||||||
|
# Nicht nur das Verzeichnis unter /srv: eine Installation der Fassung 1.x
|
||||||
|
# hinterlaesst auch /var/log/<name>, /etc/logrotate.d/<name> und den
|
||||||
|
# ACME-Pfad. Der Umzug bricht sonst mitten drin ab -- live genau so passiert.
|
||||||
|
BELEGT=()
|
||||||
|
for paar in "${VERSCHIEBEN[@]}"; do
|
||||||
|
ziel="${paar##*:}"
|
||||||
|
[[ -e "$ziel" ]] && BELEGT+=("$ziel")
|
||||||
|
done
|
||||||
|
for weiteres in "/etc/logrotate.d/$TO" "/etc/update-motd.d/50-$TO"; do
|
||||||
|
[[ -e "$weiteres" ]] && BELEGT+=("$weiteres")
|
||||||
|
done
|
||||||
|
|
||||||
|
BEISEITE=""
|
||||||
|
if [[ ${#BELEGT[@]} -gt 0 ]]; then
|
||||||
|
if [[ -f "/srv/$TO/data/app.db" ]]; then
|
||||||
|
fail "/srv/$TO ist bereits eine Installation dieser Fassung. Welche gewinnt, entscheidet niemand automatisch."
|
||||||
|
fi
|
||||||
|
BEISEITE="/srv/$TO-$SUFFIX"
|
||||||
|
if [[ -e "$BEISEITE" ]]; then
|
||||||
|
# Ein vorhandenes Archiv derselben Verdraengung wird weiterbenutzt -- die
|
||||||
|
# Reste einer Installation gehoeren zusammen und nicht in ein zweites
|
||||||
|
# Verzeichnis. Alles andere unter diesem Namen bleibt unangetastet.
|
||||||
|
if [[ -f "$BEISEITE/sqlite.db" || -d "$BEISEITE/systemd-alt" ]]; then
|
||||||
|
info "Archiv $BEISEITE ist vorhanden und wird weiterbenutzt."
|
||||||
|
else
|
||||||
|
fail "$BEISEITE existiert und sieht nicht nach einem solchen Archiv aus -- bitte --suffix waehlen"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
info "Der Zielname ist belegt. Alles davon wandert nach $BEISEITE:"
|
||||||
|
for pfad in "${BELEGT[@]}"; do info " $pfad"; done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $DRY_RUN -eq 1 ]]; then
|
||||||
|
step "--dry-run: hier ist Schluss."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ $ASSUME_YES -eq 0 ]]; then
|
||||||
|
read -r -p "Fortfahren? Alle angemeldeten Benutzer werden abgemeldet. [j/N] " answer
|
||||||
|
[[ "$answer" =~ ^[jJyY]$ ]] || { echo "Abgebrochen."; exit 0; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- 1. Dienste anhalten ----------------------------------------------------- #
|
||||||
|
step "Dienste anhalten"
|
||||||
|
for unit in "$FROM.service" "$FROM-monitor.service"; do
|
||||||
|
if systemctl list-unit-files "$unit" --no-legend 2>/dev/null | grep -q .; then
|
||||||
|
systemctl disable --now "$unit" >/dev/null 2>&1 || true
|
||||||
|
info "$unit gestoppt und deaktiviert"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# -- 2. Zielnamen freimachen ------------------------------------------------- #
|
||||||
|
if [[ -n "$BEISEITE" ]]; then
|
||||||
|
step "Zielnamen freimachen"
|
||||||
|
# Alles wandert ins Archiv, nichts wird geloescht: unter /var/log/<name>
|
||||||
|
# liegen Protokolle, unter /srv/<name> im Zweifel eine ganze Installation.
|
||||||
|
# Die Units und die Site kommen mit -- sie zeigen auf das alte Verzeichnis
|
||||||
|
# und waeren auf dem System nur noch Fehlerquellen, im Archiv aber ein Beleg.
|
||||||
|
mkdir -p "$BEISEITE"
|
||||||
|
for pfad in "${BELEGT[@]}"; do
|
||||||
|
ziel="$BEISEITE/$(basename "$pfad")"
|
||||||
|
if [[ "$pfad" == "/srv/$TO" ]]; then
|
||||||
|
# Das Verzeichnis selbst wird das Archiv -- sein Inhalt bleibt, wie er ist.
|
||||||
|
for eintrag in "$pfad"/* "$pfad"/.[!.]*; do
|
||||||
|
[[ -e "$eintrag" ]] || continue
|
||||||
|
mv "$eintrag" "$BEISEITE/"
|
||||||
|
done
|
||||||
|
rmdir "$pfad"
|
||||||
|
info "/srv/$TO -> $BEISEITE/"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ -e "$ziel" ]]; then
|
||||||
|
ziel="$ziel-$(date +%Y%m%d-%H%M%S)"
|
||||||
|
warn "Im Archiv lag schon etwas mit diesem Namen -- ablegen als $(basename "$ziel")"
|
||||||
|
fi
|
||||||
|
mv "$pfad" "$ziel"
|
||||||
|
info "$pfad -> $ziel"
|
||||||
|
done
|
||||||
|
mkdir -p "$BEISEITE/systemd-alt"
|
||||||
|
for unit in "/etc/systemd/system/$TO.service" "/etc/systemd/system/$TO-check.service" \
|
||||||
|
"/etc/systemd/system/$TO-check.timer" "/etc/systemd/system/$TO-check-restart.service" \
|
||||||
|
"/etc/systemd/system/$TO-check-restart.timer"; do
|
||||||
|
[[ -f "$unit" ]] || continue
|
||||||
|
systemctl disable --now "$(basename "$unit")" >/dev/null 2>&1 || true
|
||||||
|
mv "$unit" "$BEISEITE/systemd-alt/"
|
||||||
|
info "$(basename "$unit") archiviert"
|
||||||
|
done
|
||||||
|
for site in "/etc/nginx/sites-enabled/$TO" "/etc/nginx/sites-available/$TO"; do
|
||||||
|
[[ -e "$site" ]] || continue
|
||||||
|
[[ -L "$site" ]] && { rm -f "$site"; continue; }
|
||||||
|
mv "$site" "$BEISEITE/systemd-alt/nginx-site"
|
||||||
|
info "nginx-Site archiviert"
|
||||||
|
done
|
||||||
|
for motd in "/etc/update-motd.d/50-$TO"; do
|
||||||
|
[[ -f "$motd" ]] && grep -q "motd_banner.py" "$motd" 2>/dev/null && rm -f "$motd" \
|
||||||
|
&& info "Banner der Fassung 1.x entfernt"
|
||||||
|
done
|
||||||
|
info "Alles Verdraengte liegt unter $BEISEITE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- 3. Umziehen ------------------------------------------------------------- #
|
||||||
|
step "Umziehen"
|
||||||
|
for paar in "${VERSCHIEBEN[@]}"; do
|
||||||
|
quelle="${paar%%:*}"; ziel="${paar##*:}"
|
||||||
|
[[ -e "$quelle" ]] || continue
|
||||||
|
[[ -e "$ziel" ]] && fail "$ziel existiert bereits -- Abbruch, damit nichts vermischt wird"
|
||||||
|
mkdir -p "$(dirname "$ziel")"
|
||||||
|
mv "$quelle" "$ziel"
|
||||||
|
info "$quelle -> $ziel"
|
||||||
|
done
|
||||||
|
|
||||||
|
# -- 4. Namen in den Dateien nachziehen -------------------------------------- #
|
||||||
|
step "Namen nachziehen"
|
||||||
|
ENV_FILE="/etc/tesm/$TO.env"
|
||||||
|
if [[ -f "$ENV_FILE" ]]; then
|
||||||
|
# Der SITE_KEY bestimmt den Namen des Sitzungscookies. Bleibt er alt, heisst
|
||||||
|
# das Cookie weiter wie die alte Instanz -- unauffaellig und falsch.
|
||||||
|
sed -i "s#^${ENV_PREFIX}SITE_KEY=.*#${ENV_PREFIX}SITE_KEY=$TO#" "$ENV_FILE"
|
||||||
|
info "SITE_KEY=$TO"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pfade in der Datenbank, die den Namen enthalten.
|
||||||
|
python3 - "$TO" "$FROM" "/srv/$TO/data/app.db" <<'PY'
|
||||||
|
import sqlite3, sys
|
||||||
|
neu, alt, datenbank = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||||
|
schluessel = ("web_cert_path", "web_key_path", "web_acme_webroot")
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(datenbank, timeout=5.0)
|
||||||
|
except sqlite3.Error as fehler:
|
||||||
|
print(f" Datenbank nicht schreibbar: {fehler}")
|
||||||
|
raise SystemExit(0)
|
||||||
|
try:
|
||||||
|
with conn:
|
||||||
|
for key in schluessel:
|
||||||
|
row = conn.execute("SELECT value FROM settings WHERE key=?", (key,)).fetchone()
|
||||||
|
if not row or not row[0]:
|
||||||
|
continue
|
||||||
|
wert = str(row[0])
|
||||||
|
ersetzt = wert.replace(alt, neu)
|
||||||
|
if ersetzt != wert:
|
||||||
|
conn.execute("UPDATE settings SET value=? WHERE key=?", (ersetzt, key))
|
||||||
|
print(f" {key}: {wert} -> {ersetzt}")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
PY
|
||||||
|
|
||||||
|
# -- 5. Alte Konfiguration entfernen ----------------------------------------- #
|
||||||
|
step "Alte Konfiguration entfernen"
|
||||||
|
for pfad in "${ENTFERNEN[@]}"; do
|
||||||
|
[[ -e "$pfad" ]] || continue
|
||||||
|
rm -f "$pfad"
|
||||||
|
info "$pfad"
|
||||||
|
done
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# -- 6. Unter dem neuen Namen einrichten ------------------------------------- #
|
||||||
|
# Das venv wird verworfen und neu gebaut. Es ist abgeleiteter Zustand, und
|
||||||
|
# seine Konsolenskripte tragen den absoluten Pfad im Shebang: nach dem Umzug
|
||||||
|
# zeigt "venv/bin/gunicorn" auf einen Interpreter, den es nicht mehr gibt, und
|
||||||
|
# systemd meldet nur "No such file or directory" -- fuer eine Datei, die
|
||||||
|
# sichtbar da ist. Live auf POETEST genau so passiert.
|
||||||
|
if [[ -d "/srv/$TO/venv" ]]; then
|
||||||
|
step "venv verwerfen (Shebangs tragen den alten Pfad)"
|
||||||
|
rm -rf "/srv/$TO/venv"
|
||||||
|
info "wird von install.sh neu gebaut"
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Unter dem neuen Namen einrichten"
|
||||||
|
INSTALL_ARGS=(--app "$APP" --yes)
|
||||||
|
[[ "$TO" != "$APP" ]] && INSTALL_ARGS+=(--instance "${TO#"$APP"-}")
|
||||||
|
bash "$SCRIPT_DIR/install.sh" "${INSTALL_ARGS[@]}"
|
||||||
|
|
||||||
|
# -- 7. nginx-Site aus den Einstellungen neu schreiben ----------------------- #
|
||||||
|
# install.sh schreibt die Site aus seiner Vorlage, sobald sich die alias-Pfade
|
||||||
|
# aendern -- und die aendern sich hier zwangslaeufig. Der Name der Site steht
|
||||||
|
# aber in der Datenbank, nicht in der Vorlage. Ohne diesen Schritt stuenden
|
||||||
|
# zwei Sites mit "_" auf Port 80 und die erste gewinnt.
|
||||||
|
if [[ -n "$DOMAIN" ]]; then
|
||||||
|
step "Webserver-Konfiguration erneuern"
|
||||||
|
# --apply nicht vergessen: ohne ihn speichert web-setup nur die Einstellungen
|
||||||
|
# und laesst die Site-Datei stehen, die install.sh aus seiner Vorlage
|
||||||
|
# geschrieben hat -- mit "server_name _" und ohne TLS.
|
||||||
|
WEB_ARGS=(web-setup --apply --domain "$DOMAIN")
|
||||||
|
if [[ "$HTTPS_AN" == "1" ]]; then
|
||||||
|
WEB_ARGS+=(--https)
|
||||||
|
[[ "$HSTS_AN" == "1" ]] && WEB_ARGS+=(--hsts)
|
||||||
|
# Das Zertifikat ist mitgezogen und sein Pfad in der Datenbank nachgefuehrt
|
||||||
|
# -- "web-setup --https" laesst es dann unangetastet. Ein neues erzeugen
|
||||||
|
# wuerde nur eine weitere Browserwarnung produzieren. Nur wenn nach dem
|
||||||
|
# Umzug wirklich keines da ist, wird eines erzeugt.
|
||||||
|
CERT_NEU="${CERT_ALT//$FROM/$TO}"
|
||||||
|
if [[ -n "$CERT_NEU" && -f "$CERT_NEU" ]]; then
|
||||||
|
info "HTTPS mit dem vorhandenen Zertifikat: $CERT_NEU"
|
||||||
|
else
|
||||||
|
WEB_ARGS+=(--self-signed)
|
||||||
|
warn "Zertifikat nicht gefunden (${CERT_NEU:-keins hinterlegt}) -- es wird ein neues"
|
||||||
|
warn "selbstsigniertes erzeugt. Der Browser warnt dann einmal erneut."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if sudo -u "$APP" "/usr/local/bin/$TO-admin" "${WEB_ARGS[@]}"; then
|
||||||
|
info "Site neu geschrieben: server_name $DOMAIN"
|
||||||
|
else
|
||||||
|
warn "web-setup ist fehlgeschlagen -- bitte in der Anwendung unter"
|
||||||
|
warn "'Verwaltung -> Webserver und TLS' die Domain eintragen und uebernehmen."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- 8. Ergebnis ------------------------------------------------------------- #
|
||||||
|
step "Fertig"
|
||||||
|
info "Verzeichnis : /srv/$TO"
|
||||||
|
info "Dienst : systemctl status $TO.service"
|
||||||
|
info "Werkzeug : $TO-admin"
|
||||||
|
info "Aktualisieren: sudo bash /srv/$TO/deploy/update.sh $APP$([[ "$TO" != "$APP" ]] && echo " latest --instance ${TO#"$APP"-}")"
|
||||||
|
[[ -n "$BEISEITE" ]] && info "Fassung 1.x : $BEISEITE (Units und Site darin unter systemd-alt/)"
|
||||||
|
echo
|
||||||
|
warn "Alle Benutzer sind abgemeldet: das Sitzungscookie heisst jetzt anders."
|
||||||
+5
-8
@@ -34,15 +34,12 @@ RESET = "\033[0m"
|
|||||||
#: Dieselbe Datei liest die Anwendung (``services/kea.py``: ``LEASE_FILE``).
|
#: Dieselbe Datei liest die Anwendung (``services/kea.py``: ``LEASE_FILE``).
|
||||||
LEASE_FILE = "/var/lib/kea/kea-leases4.csv"
|
LEASE_FILE = "/var/lib/kea/kea-leases4.csv"
|
||||||
|
|
||||||
#: Zeile unter der Wortmarke. Wortlaut wie in der Anwendung
|
#: Zeile unter der Wortmarke. Wortlaut wie in der jeweiligen Anwendung
|
||||||
#: (``apps/tesm/src/tesm/__init__.py``: ``AUTH_TAGLINE``) -- ein Hygienetest
|
#: (``AUTH_TAGLINE`` in deren ``__init__.py``) -- ein Hygienetest haelt beides
|
||||||
#: haelt beides zusammen.
|
#: zusammen.
|
||||||
#:
|
|
||||||
#: Der Lizenzserver hat bewusst keine: die Funktionsliste von TESM waere dort
|
|
||||||
#: falsch, und eine erfundene waere schlimmer als keine.
|
|
||||||
TAGLINE = {
|
TAGLINE = {
|
||||||
"tesm": "UEBERWACHEN · BOOTEN · ANBINDEN (DHCP) · BETRIEBSSYSTEM GEBEN (PXE)",
|
"tesm": "ÜBERWACHEN · BOOTEN · ANBINDEN",
|
||||||
"tesm-license": "",
|
"tesm-license": "LIZENZIEREN",
|
||||||
}
|
}
|
||||||
|
|
||||||
TITEL = {"tesm": "TimEShepManager", "tesm-license": "Lizenzserver"}
|
TITEL = {"tesm": "TimEShepManager", "tesm-license": "Lizenzserver"}
|
||||||
|
|||||||
@@ -283,6 +283,51 @@ entfernt -- nur dieses, nichts anderes in dem Verzeichnis.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 4b. Installation umbenennen
|
||||||
|
|
||||||
|
Aus einer Instanz die Hauptinstallation machen (oder umgekehrt):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash deploy/rename.sh --app tesm --from tesm-opus --to tesm
|
||||||
|
sudo bash deploy/rename.sh --app tesm-license --from tesm-license-opus --to tesm-license
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Name steckt an mehr Stellen als im Verzeichnisnamen, und drei davon sind
|
||||||
|
leicht zu übersehen:
|
||||||
|
|
||||||
|
* **`SITE_KEY`** in der Umgebungsdatei -- daraus entsteht der Name des
|
||||||
|
Sitzungscookies. Bleibt er alt, heisst das Cookie weiter wie die alte
|
||||||
|
Instanz.
|
||||||
|
* **`web_cert_path`, `web_key_path`, `web_acme_webroot`** in der Datenbank. Sie
|
||||||
|
enthalten den Namen; ein Zertifikat unter dem alten Pfad findet nginx nach
|
||||||
|
dem Umzug nicht mehr.
|
||||||
|
* **`server_name`** der nginx-Site. `install.sh` schreibt die Datei aus seiner
|
||||||
|
Vorlage neu, sobald sich die `alias`-Pfade ändern -- und die ändern sich hier
|
||||||
|
zwangsläufig. Ohne den Namen aus der Datenbank stünden danach zwei Sites mit
|
||||||
|
`server_name _` auf Port 80, und die erste gewinnt.
|
||||||
|
|
||||||
|
Ausserdem wird das **venv verworfen und neu gebaut**: seine Konsolenskripte
|
||||||
|
tragen den absoluten Pfad im Shebang, und nach dem Umzug zeigt
|
||||||
|
`venv/bin/gunicorn` auf einen Interpreter, den es nicht mehr gibt. systemd
|
||||||
|
meldet dann `No such file or directory` -- für eine Datei, die sichtbar da ist.
|
||||||
|
|
||||||
|
Belegt der Zielname noch eine Installation der Fassung 1.x, wandert **alles**,
|
||||||
|
was ihn belegt, in ein Archiv `/srv/<ziel>-sonnet5`: das Verzeichnis, die
|
||||||
|
Protokolle unter `/var/log/<ziel>`, der ACME-Pfad, die logrotate-Datei, und die
|
||||||
|
systemd-Units samt nginx-Site unter `systemd-alt/`. Gelöscht wird nichts.
|
||||||
|
|
||||||
|
Ports und HTTPS bleiben, wie sie waren; ein vorhandenes Zertifikat zieht mit
|
||||||
|
und wird weiterverwendet. **Alle angemeldeten Benutzer werden abgemeldet** --
|
||||||
|
das Sitzungscookie heisst danach anders.
|
||||||
|
|
||||||
|
Danach ist auch das Aktualisieren einfacher, weil der Instanzname wegfällt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash /srv/tesm/deploy/update.sh tesm
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 5. Dienste
|
## 5. Dienste
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-core"
|
name = "tesm-core"
|
||||||
version = "2.0.5"
|
version = "2.0.7"
|
||||||
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -8,7 +8,20 @@ Design-System.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
__version__ = "2.0.2"
|
#: Version aus den Metadaten des installierten Pakets -- die einzige Quelle
|
||||||
|
#: ist ``pyproject.toml``. Vorher stand hier eine feste Zahl, und die
|
||||||
|
#: Fusszeile zeigte vier Releases lang 2.0.2.
|
||||||
|
#:
|
||||||
|
#: Im Quellbaum ohne ``pip install`` gibt es keine Metadaten; dann steht dort
|
||||||
|
#: "unbekannt" statt einer erfundenen Nummer. In einem editierbaren Install
|
||||||
|
#: zeigt sie den Stand des letzten ``pip install -e`` -- also die Version, die
|
||||||
|
#: tatsaechlich installiert ist.
|
||||||
|
try: # pragma: no cover - haengt an der Installationsart
|
||||||
|
from importlib.metadata import PackageNotFoundError, version as _paketversion
|
||||||
|
|
||||||
|
__version__ = _paketversion("tesm-core")
|
||||||
|
except (ImportError, PackageNotFoundError): # pragma: no cover
|
||||||
|
__version__ = "unbekannt"
|
||||||
|
|
||||||
from .app_factory import create_base_app
|
from .app_factory import create_base_app
|
||||||
from .config import CoreConfig, SecurityConfig, load_core_config
|
from .config import CoreConfig, SecurityConfig, load_core_config
|
||||||
|
|||||||
@@ -130,11 +130,11 @@ def create_base_app(
|
|||||||
import_name: str = "tesm_core",
|
import_name: str = "tesm_core",
|
||||||
app_short: str = "TESM",
|
app_short: str = "TESM",
|
||||||
app_edition: str = "",
|
app_edition: str = "",
|
||||||
#: Zeile unter der Wortmarke auf den Anmeldeseiten. Leer = kein
|
#: Zeile unter der Wortmarke auf den Anmeldeseiten. Leer = nur die
|
||||||
#: Seitenhintergrund. Der Lizenzserver setzt sie bewusst nicht: die
|
#: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide
|
||||||
#: Funktionsliste von TESM waere dort schlicht falsch.
|
#: Anwendungen.
|
||||||
auth_tagline: str = "",
|
auth_tagline: str = "",
|
||||||
app_version: str = "2.0.5",
|
app_version: str = "2.0.7",
|
||||||
built_in_trusted_keys: tuple[str, ...] = (),
|
built_in_trusted_keys: tuple[str, ...] = (),
|
||||||
enable_license: bool = True,
|
enable_license: bool = True,
|
||||||
template_folder: str | None = None,
|
template_folder: str | None = None,
|
||||||
|
|||||||
@@ -200,7 +200,12 @@ def _deny(message: str) -> Any:
|
|||||||
abort(401, description="Sitzung abgelaufen.")
|
abort(401, description="Sitzung abgelaufen.")
|
||||||
if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html:
|
if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html:
|
||||||
abort(401, description="Anmeldung erforderlich.")
|
abort(401, description="Anmeldung erforderlich.")
|
||||||
flash("Bitte melden Sie sich an.", "info")
|
# "Bitte melden Sie sich an." stand auf einer Seite, die aus einem
|
||||||
|
# Anmeldefeld, einem Passwortfeld und einem Knopf "Anmelden" besteht --
|
||||||
|
# eine Meldung, die nichts hinzufuegt. Wer dagegen gerade noch
|
||||||
|
# angemeldet war, soll erfahren, warum er es nicht mehr ist.
|
||||||
|
if getattr(g, "session_expired", False):
|
||||||
|
flash("Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.", "info")
|
||||||
return redirect(url_for("auth.login"))
|
return redirect(url_for("auth.login"))
|
||||||
abort(403, description=message)
|
abort(403, description=message)
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import sqlite3
|
|||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from flask import Flask, Request, Response
|
from flask import Flask, Request, Response, g
|
||||||
from flask import request as aktuelle_anfrage
|
from flask import request as aktuelle_anfrage
|
||||||
from flask.sessions import SessionInterface, SessionMixin
|
from flask.sessions import SessionInterface, SessionMixin
|
||||||
from werkzeug.datastructures import CallbackDict
|
from werkzeug.datastructures import CallbackDict
|
||||||
@@ -209,6 +209,11 @@ class SqliteSessionInterface(SessionInterface):
|
|||||||
"WHERE id=?",
|
"WHERE id=?",
|
||||||
(_iso(now), sid),
|
(_iso(now), sid),
|
||||||
)
|
)
|
||||||
|
# Fuer die Anmeldeseite vermerken, **warum** sie erscheint. Ohne
|
||||||
|
# das steht dort entweder nichts -- dann wundert sich, wer
|
||||||
|
# gerade noch angemeldet war -- oder eine Meldung, die jeder
|
||||||
|
# Besucher zu sehen bekommt und die deshalb nichts sagt.
|
||||||
|
g.session_expired = True
|
||||||
return self.session_class(new=True)
|
return self.session_class(new=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -412,7 +412,10 @@
|
|||||||
und beide sind rot. */
|
und beide sind rot. */
|
||||||
.pill--critical {
|
.pill--critical {
|
||||||
background: var(--danger);
|
background: var(--danger);
|
||||||
color: var(--surface-0);
|
/* Weiss wie bei .btn--danger. Vorher stand hier ein Token, das es nicht
|
||||||
|
gibt -- die Deklaration war damit unwirksam, und die Schrift blieb in
|
||||||
|
der Textfarbe: dunkelgrau auf Rot. */
|
||||||
|
color: #fff;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
.pill--critical::before { background: currentColor; }
|
.pill--critical::before { background: currentColor; }
|
||||||
@@ -695,7 +698,9 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: min(70dvh, 760px);
|
height: min(70dvh, 760px);
|
||||||
border: 0;
|
border: 0;
|
||||||
background: var(--surface-0);
|
/* Die Vorschau zeigt fremde Dokumente; ein neutraler Grund gehoert
|
||||||
|
darunter. --surface-0 gibt es nicht, der Rahmen blieb durchsichtig. */
|
||||||
|
background: var(--surface-1);
|
||||||
}
|
}
|
||||||
/* Der Dateiname ist ein Knopf (er oeffnet die Vorschau), soll aber wie ein
|
/* Der Dateiname ist ein Knopf (er oeffnet die Vorschau), soll aber wie ein
|
||||||
Link aussehen -- ein Knopf mit Rahmen mitten in einer Tabellenzeile waere
|
Link aussehen -- ein Knopf mit Rahmen mitten in einer Tabellenzeile waere
|
||||||
@@ -849,7 +854,10 @@
|
|||||||
width: 17px;
|
width: 17px;
|
||||||
height: 17px;
|
height: 17px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: var(--surface-0);
|
/* Dunkler als die Karte -- genau das war die Absicht: "dunkler heisst,
|
||||||
|
es gibt das Recht nicht". Mit dem nicht existierenden --surface-0 war
|
||||||
|
das Feld durchsichtig und damit so hell wie die Karte. */
|
||||||
|
background: var(--canvas);
|
||||||
border: 1px solid var(--border-subtle);
|
border: 1px solid var(--border-subtle);
|
||||||
box-shadow: inset 0 1px 2px rgb(0 0 0 / 28%);
|
box-shadow: inset 0 1px 2px rgb(0 0 0 / 28%);
|
||||||
vertical-align: -3px;
|
vertical-align: -3px;
|
||||||
@@ -943,9 +951,15 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: var(--sp-6);
|
padding: var(--sp-6);
|
||||||
|
/* Zwei warme Kreise, Geometrie und Staerke aus der Fassung 1.x
|
||||||
|
uebernommen: oben links deutlich, unten rechts angedeutet. Die vorige
|
||||||
|
Fassung setzte den zweiten Kreis auf --info und 10 % -- ein kaltes
|
||||||
|
Blau, das man auf dunklem Grund nicht sah und das nicht zur Marke
|
||||||
|
gehoert. Ueber color-mix statt festem rgba, damit es im hellen Thema
|
||||||
|
nicht schmutzig wird. */
|
||||||
background:
|
background:
|
||||||
radial-gradient(1100px 520px at 12% -10%, color-mix(in srgb, var(--accent) 12%, transparent), transparent 70%),
|
radial-gradient(circle at 15% 15%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 45%),
|
||||||
radial-gradient(900px 480px at 100% 110%, color-mix(in srgb, var(--info) 10%, transparent), transparent 70%),
|
radial-gradient(circle at 85% 85%, color-mix(in srgb, var(--accent) 8%, transparent), transparent 40%),
|
||||||
var(--canvas);
|
var(--canvas);
|
||||||
}
|
}
|
||||||
/* Die Wortmarke liegt hinter allem (z-index 0), die Anmeldebox davor.
|
/* Die Wortmarke liegt hinter allem (z-index 0), die Anmeldebox davor.
|
||||||
@@ -1026,24 +1040,35 @@
|
|||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-page { position: relative; overflow: hidden; align-content: center; }
|
/* Marke und Karte stehen uebereinander im normalen Fluss. Vorher lag die
|
||||||
|
Marke absolut positioniert hinter der Karte -- auf einem flachen Fenster
|
||||||
|
schnitt die Karte mitten durch den Schriftzug und die Funktionszeile. Ein
|
||||||
|
Stapel kann nicht kollidieren, und die Seite darf notfalls scrollen. */
|
||||||
|
.auth-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-8);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.auth-backdrop {
|
.auth-backdrop {
|
||||||
position: absolute;
|
|
||||||
top: 4vh;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: min(72vw, 880px);
|
width: min(72vw, 880px);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 0;
|
/* Volle Deckung: solange die Marke hinter der Karte lag, war sie ein
|
||||||
opacity: 0.5;
|
Wasserzeichen und musste zurueckhaltend sein. Jetzt steht sie ueber der
|
||||||
|
Karte und ist das Erste, was man sieht -- halbtransparent sah sie dort
|
||||||
|
nur blass aus. Die Farben kommen ohnehin aus zurueckhaltenden Token. */
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.auth-backdrop svg { width: 100%; height: auto; display: block; }
|
.auth-backdrop svg { width: 100%; height: auto; display: block; }
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.auth-backdrop { width: min(90vw, 460px); opacity: 0.32; }
|
.auth-backdrop { width: min(90vw, 460px); }
|
||||||
}
|
}
|
||||||
/* Wer wenig Bewegung/Reize moechte, bekommt das Motiv dezenter. */
|
/* Wer wenig Bewegung/Reize moechte, bekommt das Motiv dezenter. */
|
||||||
|
/* Wer weniger Reize moechte, bekommt die Marke ruhiger -- aber nicht
|
||||||
|
unsichtbar: sie ist die Ueberschrift der Seite. */
|
||||||
@media (prefers-contrast: more) {
|
@media (prefers-contrast: more) {
|
||||||
.auth-backdrop { opacity: 0.18; }
|
.auth-backdrop { opacity: 0.75; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-card {
|
.auth-card {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
{% extends "tesm_core/standalone.html" %}
|
{% extends "tesm_core/standalone.html" %}
|
||||||
|
{# Die Ueberschrift bleibt fuer die Titelzeile des Browsers -- sichtbar ist sie
|
||||||
|
hier nicht: der leere ``brand``-Block laesst Kachel, Ueberschrift und
|
||||||
|
Unterzeile weg. Die Wortmarke darueber sagt schon, wo man ist. #}
|
||||||
{% block heading %}Anmelden{% endblock %}
|
{% block heading %}Anmelden{% endblock %}
|
||||||
{% block subheading %}{{ app_name }} – {{ app_edition }}{% endblock %}
|
{% block brand %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="post" class="stack" data-no-guard autocomplete="on">
|
<form method="post" class="stack" data-no-guard autocomplete="on">
|
||||||
|
|||||||
@@ -22,37 +22,51 @@
|
|||||||
</head>
|
</head>
|
||||||
<body data-csrf="{{ csrf_token() }}">
|
<body data-csrf="{{ csrf_token() }}">
|
||||||
<div class="auth-page">
|
<div class="auth-page">
|
||||||
{% if auth_tagline %}
|
<div class="auth-stack">
|
||||||
{# Wortmarke mit Funktionszeile als tatsächlicher Seitenhintergrund --
|
{# Wortmarke als Seitenhintergrund -- fuer beide Anwendungen. Bewusst inline
|
||||||
übernommen vom Vorgänger. Bewusst inline und mit Themen-Tokens statt
|
und mit Themen-Tokens statt zweier Dateien fuer hell und dunkel: bei der
|
||||||
zweier Dateien für hell und dunkel: bei der Einstellung "System" und
|
Einstellung "System" und beim Umschalten von Hand waere die versteckte
|
||||||
beim Umschalten von Hand wäre die versteckte Variante die falsche.
|
Variante die falsche. ``aria-hidden``, weil der Name in der Titelzeile
|
||||||
``aria-hidden``, weil der Name schon in der Überschrift steht. #}
|
steht.
|
||||||
<div class="auth-backdrop" aria-hidden="true">
|
|
||||||
<svg viewBox="0 0 620 190" xmlns="http://www.w3.org/2000/svg">
|
Die Geometrie ist symmetrisch um die Mitte (x=260) aufgebaut, damit die
|
||||||
<g transform="translate(150,15)">
|
Marke ohne Nachrechnen mittig sitzt: der Schriftzug haengt an
|
||||||
<circle cx="298" cy="70" r="15" fill="var(--logo-mark)" opacity="0.22"/>
|
``text-anchor="middle"``, die beiden Zacken liegen spiegelbildlich, und
|
||||||
<polyline points="0,70 90,70 100,70 108,20 116,120 124,70 134,70 200,70 210,70
|
die Linie beginnt links so weit vor der Mitte, wie der Punkt rechts nach
|
||||||
218,35 226,105 234,70 244,70 300,70"
|
ihr endet. Wer die Linie verlaengert, muss beide Enden anfassen.
|
||||||
fill="none" stroke="var(--logo-mark)" stroke-width="3.2"
|
|
||||||
stroke-linecap="round" stroke-linejoin="round" opacity="0.85"/>
|
Ohne Funktionszeile (Lizenzserver) ist der Kasten kuerzer -- sonst
|
||||||
<text x="14" y="98" font-family="var(--font-mono)" font-weight="800"
|
stuende unter der Marke ein leerer Streifen. #}
|
||||||
font-size="82" letter-spacing="2" fill="var(--logo-text)">TESM</text>
|
<div class="auth-backdrop" aria-hidden="true">
|
||||||
<circle cx="298" cy="70" r="8" fill="var(--logo-mark)"/>
|
<svg viewBox="0 0 520 {{ 150 if auth_tagline else 116 }}" xmlns="http://www.w3.org/2000/svg">
|
||||||
</g>
|
<circle cx="452" cy="62" r="15" fill="var(--logo-mark)" opacity="0.22"/>
|
||||||
<text x="310" y="178" text-anchor="middle" font-family="var(--font-sans)"
|
<polyline points="60,62 163,62 171,12 179,112 187,62 333,62 341,12 349,112 357,62 452,62"
|
||||||
|
fill="none" stroke="var(--logo-mark)" stroke-width="3.2"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round" opacity="0.85"/>
|
||||||
|
<text x="260" y="90" text-anchor="middle" font-family="var(--font-mono)"
|
||||||
|
font-weight="800" font-size="82" letter-spacing="2"
|
||||||
|
fill="var(--logo-text)">TESM</text>
|
||||||
|
<circle cx="452" cy="62" r="8" fill="var(--logo-mark)"/>
|
||||||
|
{% if auth_tagline %}
|
||||||
|
<text x="260" y="140" text-anchor="middle" font-family="var(--font-sans)"
|
||||||
font-weight="600" font-size="13" letter-spacing="1.5"
|
font-weight="600" font-size="13" letter-spacing="1.5"
|
||||||
fill="var(--logo-mark)">{{ auth_tagline }}</text>
|
fill="var(--logo-mark)">{{ auth_tagline }}</text>
|
||||||
</svg>
|
{% endif %}
|
||||||
</div>
|
</svg>
|
||||||
{% endif %}
|
</div>
|
||||||
<div class="auth-card {% block card_class %}{% endblock %}">
|
<div class="auth-card {% block card_class %}{% endblock %}">
|
||||||
<div class="auth-brand">
|
{# Die Anmeldeseite laesst diesen Kopf weg: die Wortmarke darueber sagt
|
||||||
<span class="auth-brand__mark" aria-hidden="true">{{ app_short }}</span>
|
schon, wo man ist, und "Anmelden" steht auf dem Knopf. Die uebrigen
|
||||||
<h1>{% block heading %}{{ app_name }}{% endblock %}</h1>
|
Standalone-Seiten behalten ihn -- "Zweiter Faktor" oder der Kundenname
|
||||||
{% set sub %}{% block subheading %}{% endblock %}{% endset %}
|
im Portal sind dort die Auskunft, um die es geht. #}
|
||||||
{% if sub | trim %}<p>{{ sub }}</p>{% endif %}
|
{% block brand %}
|
||||||
</div>
|
<div class="auth-brand">
|
||||||
|
<span class="auth-brand__mark" aria-hidden="true">{{ app_short }}</span>
|
||||||
|
<h1>{% block heading %}{{ app_name }}{% endblock %}</h1>
|
||||||
|
{% set sub %}{% block subheading %}{% endblock %}{% endset %}
|
||||||
|
{% if sub | trim %}<p>{{ sub }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
@@ -72,6 +86,7 @@
|
|||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
<p class="auth-footnote">{{ app_name }} {{ app_version }} · © {{ current_year }} {{ copyright_notice }}</p>
|
<p class="auth-footnote">{{ app_name }} {{ app_version }} · © {{ current_year }} {{ copyright_notice }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="{{ asset_url('js/app.js') }}" nonce="{{ csp_nonce }}"></script>
|
<script type="module" src="{{ asset_url('js/app.js') }}" nonce="{{ csp_nonce }}"></script>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-licensing"
|
name = "tesm-licensing"
|
||||||
version = "2.0.5"
|
version = "2.0.7"
|
||||||
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
|
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = ["cryptography>=42"]
|
dependencies = ["cryptography>=42"]
|
||||||
|
|||||||
Reference in New Issue
Block a user