#!/usr/bin/env bash # ============================================================ # Psono Passwortmanager — Migrations-Script # Von: altem Host (z.B. Synology NAS) → neuer Debian 12/13 # ============================================================ set -euo pipefail # ── Farben & Symbole ───────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m' OK=" ${GREEN}[ OK ]${RESET}" ERR=" ${RED}[ ERROR ]${RESET}" WARN="${YELLOW}[ WARNING ]${RESET}" INFO="${CYAN}[ INFO ]${RESET}" # ── Hilfsfunktionen ────────────────────────────────────────── print_header() { echo "" echo -e "${BOLD}${BLUE}╔══════════════════════════════════════════════════════════╗${RESET}" echo -e "${BOLD}${BLUE}║ $1${RESET}" echo -e "${BOLD}${BLUE}╚══════════════════════════════════════════════════════════╝${RESET}" echo "" } print_phase() { echo "" echo -e "${BOLD}${CYAN}┌──────────────────────────────────────────────────────────┐${RESET}" echo -e "${BOLD}${CYAN}│ Phase $1: $2${RESET}" echo -e "${BOLD}${CYAN}└──────────────────────────────────────────────────────────┘${RESET}" echo "" } print_step() { echo -e "${DIM}──────────────────────────────────────────────────────────${RESET}" echo -e "${BOLD} Schritt $1${RESET}" echo "" } # Befehl ausführen mit OK/ERROR-Ausgabe run() { local desc="$1"; shift echo -ne " ${DIM}→${RESET} ${desc} ... " if output=$("$@" 2>&1); then echo -e "$OK" return 0 else echo -e "$ERR" echo -e " ${RED}Ausgabe: ${output}${RESET}" return 1 fi } # Befehl ausführen, Fehler nur als Warning run_warn() { local desc="$1"; shift echo -ne " ${DIM}→${RESET} ${desc} ... " if output=$("$@" 2>&1); then echo -e "$OK" else echo -e "$WARN" echo -e " ${YELLOW}Ausgabe: ${output}${RESET}" fi } # Befehl mit sichtbarer Ausgabe (z.B. für pg_restore) run_visible() { local desc="$1"; shift echo -e " ${DIM}→${RESET} ${desc}" echo -e "${DIM} ────────────────────────────────────────${RESET}" if "$@" 2>&1 | sed 's/^/ /'; then echo -e "${DIM} ────────────────────────────────────────${RESET}" echo -e "$OK ${desc}" return 0 else echo -e "${DIM} ────────────────────────────────────────${RESET}" echo -e "$ERR ${desc}" return 1 fi } # Fehlerbehandlung: fragen ob weiter oder abbrechen handle_error() { local msg="${1:-Ein Fehler ist aufgetreten.}" echo "" echo -e "$ERR ${RED}${BOLD}${msg}${RESET}" echo "" echo -e "${YELLOW} Wie möchtest du fortfahren?${RESET}" echo -e " ${BOLD}[C]${RESET} Fortsetzen (auf eigene Gefahr)" echo -e " ${BOLD}[Q]${RESET} Script abbrechen" echo "" read -rp " Eingabe [C/Q]: " choice case "${choice^^}" in C) echo -e "$WARN Fortgesetzt trotz Fehler." ;; *) echo -e "$ERR Script abgebrochen."; exit 1 ;; esac } # Nach jeder Phase pausieren pause_phase() { echo "" echo -e "${DIM}──────────────────────────────────────────────────────────${RESET}" echo -e "${GREEN} Phase $1 abgeschlossen.${RESET}" echo -e "${DIM}──────────────────────────────────────────────────────────${RESET}" echo "" echo -e " ${BOLD}[ENTER]${RESET} Weiter mit Phase $2" echo -e " ${BOLD}[Ctrl+C]${RESET} Script abbrechen" echo "" read -rp " → " } # Passwort einlesen (ohne Echo) read_password() { local prompt="$1" local var_name="$2" local pw1 pw2 while true; do read -rsp " ${prompt}: " pw1; echo "" read -rsp " Wiederholen: " pw2; echo "" if [[ "$pw1" == "$pw2" ]]; then if [[ ${#pw1} -lt 12 ]]; then echo -e "$WARN Passwort zu kurz (min. 12 Zeichen). Bitte nochmal." else eval "$var_name='$pw1'" break fi else echo -e "$ERR Passwörter stimmen nicht überein. Bitte nochmal." fi done } # ── Trap für Ctrl+C ────────────────────────────────────────── trap 'echo -e "\n$ERR Script durch Benutzer abgebrochen (Ctrl+C)."; exit 130' INT # ════════════════════════════════════════════════════════════ # START # ════════════════════════════════════════════════════════════ clear print_header "Psono Passwortmanager — Migration" echo -e " Dieses Script migriert Psono von einem alten Host" echo -e " auf diesen Debian 12/13 Server." echo "" echo -e " ${YELLOW}Voraussetzungen:${RESET}" echo -e " • Docker installiert" echo -e " • PostgreSQL 17 installiert" echo -e " • Nginx installiert" echo -e " • Dehydrated mit gültigem Zertifikat für deine Domain" echo -e " • Datenbank-Backup (.tar) lokal vorhanden" echo -e " • settings.yaml vom alten psono-combo Container gesichert" echo "" echo -e "${DIM}──────────────────────────────────────────────────────────${RESET}" # ── Parameter einlesen ─────────────────────────────────────── print_header "Parameter eingeben" echo -e "${BOLD} 1/6 Domain${RESET}" read -rp " Psono-Domain (z.B. psono.example.com): " PSONO_DOMAIN echo "" echo -e "${BOLD} 2/6 Server-IP${RESET}" read -rp " Öffentliche IP dieses Servers: " SERVER_IP echo "" echo -e "${BOLD} 3/6 Datenbank-Passwort${RESET}" echo -e "${DIM} Passwort für den PostgreSQL-Benutzer 'psono'${RESET}" read_password "Passwort" DB_PASSWORD echo "" echo -e "${BOLD} 4/6 Pfad zum Datenbank-Backup${RESET}" read -rp " Vollständiger Pfad zur .tar-Datei: " BACKUP_PATH while [[ ! -f "$BACKUP_PATH" ]]; do echo -e "$ERR Datei nicht gefunden: $BACKUP_PATH" read -rp " Pfad zur .tar-Datei: " BACKUP_PATH done echo "" echo -e "${BOLD} 5/6 Pfad zur settings.yaml${RESET}" read -rp " Vollständiger Pfad zur settings.yaml: " SETTINGS_PATH while [[ ! -f "$SETTINGS_PATH" ]]; do echo -e "$ERR Datei nicht gefunden: $SETTINGS_PATH" read -rp " Pfad zur settings.yaml: " SETTINGS_PATH done echo "" echo -e "${BOLD} 6/6 E-Mail-Adresse${RESET}" read -rp " E-Mail für Psono-Benachrichtigungen: " PSONO_EMAIL echo "" # ── Zusammenfassung ────────────────────────────────────────── print_header "Zusammenfassung" echo -e " ${BOLD}Domain:${RESET} ${CYAN}${PSONO_DOMAIN}${RESET}" echo -e " ${BOLD}Server-IP:${RESET} ${CYAN}${SERVER_IP}${RESET}" echo -e " ${BOLD}DB-Passwort:${RESET} ${CYAN}(gesetzt, ${#DB_PASSWORD} Zeichen)${RESET}" echo -e " ${BOLD}Backup-Datei:${RESET} ${CYAN}${BACKUP_PATH}${RESET}" echo -e " ${BOLD}settings.yaml:${RESET} ${CYAN}${SETTINGS_PATH}${RESET}" echo -e " ${BOLD}E-Mail:${RESET} ${CYAN}${PSONO_EMAIL}${RESET}" echo "" echo -e " ${BOLD}Zertifikat erwartet unter:${RESET}" echo -e " ${DIM}/var/lib/dehydrated/certs/${PSONO_DOMAIN}/fullchain.pem${RESET}" echo "" echo -e "${DIM}──────────────────────────────────────────────────────────${RESET}" echo "" echo -e " ${YELLOW}${BOLD}Bitte prüfe alle Angaben sorgfältig.${RESET}" echo -e " Gib ${BOLD}YES${RESET} (Großbuchstaben) ein um das Script zu starten." echo -e " Alles andere bricht ab." echo "" read -rp " Bestätigung: " CONFIRM if [[ "$CONFIRM" != "YES" ]]; then echo -e "$ERR Abgebrochen. Keine Änderungen vorgenommen." exit 0 fi # ════════════════════════════════════════════════════════════ # PHASE 1 — PostgreSQL vorbereiten # ════════════════════════════════════════════════════════════ print_phase "1" "PostgreSQL vorbereiten" print_step "1.1 — Datenbank anlegen" if run "Datenbank 'psono' anlegen" sudo -u postgres psql -c "CREATE DATABASE psono;"; then : # ok else handle_error "Datenbank konnte nicht angelegt werden." fi print_step "1.2 — Benutzer anlegen" if run "Benutzer 'psono' anlegen" sudo -u postgres psql -c "CREATE USER psono WITH PASSWORD '${DB_PASSWORD}';"; then : # ok else handle_error "Benutzer konnte nicht angelegt werden." fi print_step "1.3 — Rechte vergeben" run "GRANT auf Datenbank" sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE psono TO psono;" \ || handle_error "GRANT fehlgeschlagen." run "GRANT auf Schema" sudo -u postgres psql -d psono -c "GRANT ALL ON SCHEMA public TO psono;" \ || handle_error "GRANT auf Schema fehlgeschlagen." print_step "1.4 — Erweiterungen installieren" run "Extension ltree" sudo -u postgres psql -d psono -c "CREATE EXTENSION IF NOT EXISTS ltree;" \ || handle_error "Extension ltree konnte nicht installiert werden." run "Extension pgcrypto" sudo -u postgres psql -d psono -c "CREATE EXTENSION IF NOT EXISTS pgcrypto;" \ || handle_error "Extension pgcrypto konnte nicht installiert werden." print_step "1.5 — pg_hba.conf anpassen" HBA_FILE=$(sudo -u postgres psql -tAc "SHOW hba_file;") echo -e " ${INFO} pg_hba.conf: ${HBA_FILE}" run "Eintrag für lokalen psono-Benutzer" sudo sed -i \ '/^local all all/i local all psono md5' \ "$HBA_FILE" || handle_error "pg_hba.conf konnte nicht angepasst werden." echo "host psono psono 172.17.0.0/16 md5" | sudo tee -a "$HBA_FILE" > /dev/null \ && echo -e "$OK Docker-Netzwerk 172.17.0.0/16 eingetragen" \ || handle_error "pg_hba.conf Eintrag für 172.17.0.0/16 fehlgeschlagen." echo "host psono psono 172.18.0.0/16 md5" | sudo tee -a "$HBA_FILE" > /dev/null \ && echo -e "$OK Docker-Netzwerk 172.18.0.0/16 eingetragen" \ || handle_error "pg_hba.conf Eintrag für 172.18.0.0/16 fehlgeschlagen." print_step "1.6 — listen_addresses anpassen" run "listen_addresses setzen" sudo -u postgres psql -c \ "ALTER SYSTEM SET listen_addresses = 'localhost,172.17.0.1,172.18.0.1';" \ || handle_error "listen_addresses konnte nicht gesetzt werden." run "PostgreSQL neu starten" sudo systemctl restart postgresql \ || handle_error "PostgreSQL-Neustart fehlgeschlagen." print_step "1.7 — Prüfung" if run "Verbindung als psono-Benutzer testen" sudo -u postgres psql -U psono -d psono \ -c "SELECT count(*) FROM pg_extension WHERE extname IN ('ltree','pgcrypto');" 2>/dev/null; then echo -e " ${INFO} Erweiterungen vorhanden" else handle_error "Verbindungstest fehlgeschlagen — Erweiterungen oder Benutzer nicht korrekt." fi LISTEN=$(sudo -u postgres psql -tAc "SHOW listen_addresses;" 2>/dev/null | tr -d ' ') echo -e " ${INFO} listen_addresses = ${CYAN}${LISTEN}${RESET}" pause_phase "1" "2" # ════════════════════════════════════════════════════════════ # PHASE 2 — Backup einspielen # ════════════════════════════════════════════════════════════ print_phase "2" "Backup einspielen" print_step "2.1 — Backup entpacken" DUMP_DIR="/root/psono-backup/dump" run "Verzeichnis anlegen" mkdir -p "$DUMP_DIR" \ || handle_error "Verzeichnis $DUMP_DIR konnte nicht angelegt werden." run "Backup entpacken" tar -xf "$BACKUP_PATH" -C "$DUMP_DIR" \ || handle_error "Backup konnte nicht entpackt werden." echo -e " ${INFO} Inhalt des Dumps:" ls "$DUMP_DIR" | head -5 | sed 's/^/ /' echo -e " ${DIM}... ($(ls "$DUMP_DIR" | wc -l) Dateien gesamt)${RESET}" print_step "2.2 — Backup einspielen" echo -e " ${INFO} Das kann einige Sekunden dauern ..." echo "" PGPASSWORD="$DB_PASSWORD" pg_restore \ -U psono \ -d psono \ -Fd \ --no-owner \ --no-privileges \ "$DUMP_DIR" 2>&1 | while IFS= read -r line; do if echo "$line" | grep -qi "error:"; then echo -e " $ERR ${line}" elif echo "$line" | grep -qi "warning:"; then echo -e " $WARN ${DIM}${line}${RESET}" else echo -e " ${DIM} ${line}${RESET}" fi done PG_EXIT=${PIPESTATUS[0]} if [[ $PG_EXIT -eq 0 ]]; then echo -e "$OK pg_restore abgeschlossen" else echo -e "$WARN pg_restore mit Warnungen/Fehlern beendet (Exit: $PG_EXIT)" handle_error "pg_restore meldete Fehler. Prüfe die Ausgabe oben." fi print_step "2.3 — Prüfung" USER_COUNT=$(PGPASSWORD="$DB_PASSWORD" psql -U psono -d psono -tAc \ "SELECT count(*) FROM restapi_user;" 2>/dev/null || echo "0") echo -e " ${INFO} Importierte Psono-Benutzer: ${CYAN}${USER_COUNT}${RESET}" if [[ "$USER_COUNT" -gt 0 ]]; then echo -e "$OK Daten erfolgreich importiert" else handle_error "Keine Benutzer gefunden — Import möglicherweise fehlgeschlagen." fi pause_phase "2" "3" # ════════════════════════════════════════════════════════════ # PHASE 3 — Psono-Konfiguration wiederherstellen # ════════════════════════════════════════════════════════════ print_phase "3" "Psono-Konfiguration wiederherstellen" print_step "3.1 — Verzeichnisse anlegen" run "Verzeichnis /opt/docker/psono" mkdir -p /opt/docker/psono \ || handle_error "Verzeichnis konnte nicht angelegt werden." run "Verzeichnis /opt/docker/psono-client" mkdir -p /opt/docker/psono-client \ || handle_error "Verzeichnis konnte nicht angelegt werden." print_step "3.2 — settings.yaml kopieren" run "settings.yaml kopieren" cp "$SETTINGS_PATH" /opt/docker/psono/settings.yaml \ || handle_error "settings.yaml konnte nicht kopiert werden." print_step "3.3 — settings.yaml anpassen" run "HOST auf host.docker.internal setzen" sed -i \ "s/'HOST': 'psono-database'/'HOST': 'host.docker.internal'/" \ /opt/docker/psono/settings.yaml || handle_error "HOST konnte nicht ersetzt werden." run "DB-Passwort setzen" sed -i \ "s/'PASSWORD': '.*'/'PASSWORD': '${DB_PASSWORD}'/" \ /opt/docker/psono/settings.yaml || handle_error "Passwort konnte nicht gesetzt werden." # Cache deaktivieren (nur wenn vorhanden) if grep -q "^CACHE_ENABLE: TRUE" /opt/docker/psono/settings.yaml 2>/dev/null; then run "CACHE_ENABLE deaktivieren" sed -i \ "s/^CACHE_ENABLE: TRUE/CACHE_ENABLE: FALSE/" /opt/docker/psono/settings.yaml \ || handle_error "CACHE_ENABLE konnte nicht deaktiviert werden." run "CACHE_REDIS deaktivieren" sed -i \ "s/^CACHE_REDIS: TRUE/CACHE_REDIS: FALSE/" /opt/docker/psono/settings.yaml \ || true run "CACHE_REDIS_LOCATION auskommentieren" sed -i \ "s/^CACHE_REDIS_LOCATION:/#CACHE_REDIS_LOCATION:/" /opt/docker/psono/settings.yaml \ || true else echo -e " ${INFO} Cache war bereits deaktiviert — keine Änderung nötig" fi print_step "3.4 — config.json erstellen" cat > /opt/docker/psono-client/config.json << EOF { "backend_servers": [{ "title": "Psono Server", "url": "https://${PSONO_DOMAIN}/server" }], "base_url": "https://${PSONO_DOMAIN}/", "allow_custom_server": true, "allow_registration": true, "allow_lost_password": true, "disable_download_bar": false, "remember_me_default": false, "trust_device_default": false, "authentication_methods": ["AUTHKEY"] } EOF echo -e "$OK config.json erstellt" print_step "3.5 — Prüfung" echo -e " ${INFO} Aktuelle Datenbankverbindung in settings.yaml:" grep -E "'HOST'|'PASSWORD'|'NAME'" /opt/docker/psono/settings.yaml 2>/dev/null | sed 's/^/ /' CACHE_STATE=$(grep "^CACHE_ENABLE" /opt/docker/psono/settings.yaml 2>/dev/null || echo "nicht gefunden") echo -e " ${INFO} Cache: ${CYAN}${CACHE_STATE}${RESET}" pause_phase "3" "4" # ════════════════════════════════════════════════════════════ # PHASE 4 — Docker Container starten # ════════════════════════════════════════════════════════════ print_phase "4" "Psono-Container starten" print_step "4.1 — docker-compose.yml erstellen" cat > /opt/docker/psono/docker-compose.yml << 'EOF' services: psono-combo: image: psono/psono-combo:latest restart: unless-stopped ports: - "127.0.0.1:10200:80" volumes: - /opt/docker/psono/settings.yaml:/root/.psono_server/settings.yaml:ro - /opt/docker/psono-client/config.json:/usr/share/nginx/html/config.json:ro extra_hosts: - "host.docker.internal:host-gateway" EOF echo -e "$OK docker-compose.yml erstellt" print_step "4.2 — Container starten" run "Docker Image pullen" docker compose -f /opt/docker/psono/docker-compose.yml pull \ || handle_error "Docker Image konnte nicht geladen werden." run "Container starten" docker compose -f /opt/docker/psono/docker-compose.yml up -d \ || handle_error "Container konnte nicht gestartet werden." print_step "4.3 — Warten und Logs prüfen" echo -e " ${INFO} Warte 10 Sekunden auf Container-Start ..." sleep 10 echo -e " ${INFO} Letzte Log-Einträge:" docker compose -f /opt/docker/psono/docker-compose.yml logs psono-combo 2>&1 | tail -8 | sed 's/^/ /' print_step "4.4 — Prüfung" if run "HTTP-Test auf Port 10200" curl -sf http://localhost:10200/server/info/ > /dev/null; then PSONO_VERSION=$(curl -s http://localhost:10200/server/info/ | grep -o '"version": "[^"]*"' | head -1) echo -e " ${INFO} ${CYAN}${PSONO_VERSION}${RESET}" else handle_error "Psono antwortet nicht auf Port 10200. Logs prüfen." fi pause_phase "4" "5" # ════════════════════════════════════════════════════════════ # PHASE 5 — Nginx einrichten # ════════════════════════════════════════════════════════════ print_phase "5" "Nginx Reverse Proxy einrichten" print_step "5.1 — Zertifikat prüfen" CERT_PATH="/var/lib/dehydrated/certs/${PSONO_DOMAIN}" if [[ -f "${CERT_PATH}/fullchain.pem" && -f "${CERT_PATH}/privkey.pem" ]]; then echo -e "$OK Zertifikat gefunden: ${CERT_PATH}" EXPIRY=$(openssl x509 -enddate -noout -in "${CERT_PATH}/fullchain.pem" 2>/dev/null | cut -d= -f2) echo -e " ${INFO} Gültig bis: ${CYAN}${EXPIRY}${RESET}" else handle_error "Zertifikat nicht gefunden unter ${CERT_PATH}. Dehydrated zuerst ausführen." fi print_step "5.2 — Nginx-Konfiguration erstellen" NGINX_CONF="/etc/nginx/sites-available/${PSONO_DOMAIN}.conf" cat > "$NGINX_CONF" << EOF server { listen 80; listen [::]:80; server_name ${PSONO_DOMAIN}; server_tokens off; return 301 https://\$host\$request_uri; } server { listen 443 ssl default_server; listen [::]:443 ssl default_server; server_name ${PSONO_DOMAIN}; include snippets/letsencrypt-acme-challenge.conf; ssl_certificate /var/lib/dehydrated/certs/${PSONO_DOMAIN}/fullchain.pem; ssl_certificate_key /var/lib/dehydrated/certs/${PSONO_DOMAIN}/privkey.pem; ssl_dhparam /etc/nginx/ssl/dhparam.pem; ssl_session_cache shared:MozSSL:50m; ssl_session_timeout 1d; ssl_session_tickets off; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; server_tokens off; add_header Strict-Transport-Security "max-age=15768000; includeSubDomains;" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "same-origin" always; add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always; client_max_body_size 256m; location / { proxy_pass http://127.0.0.1:10200; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; } } EOF echo -e "$OK Nginx-Konfiguration erstellt: $NGINX_CONF" print_step "5.3 — Konfiguration aktivieren" if [[ ! -L "/etc/nginx/sites-enabled/${PSONO_DOMAIN}.conf" ]]; then run "Symlink erstellen" ln -s "$NGINX_CONF" "/etc/nginx/sites-enabled/${PSONO_DOMAIN}.conf" \ || handle_error "Symlink konnte nicht erstellt werden." else echo -e " ${INFO} Symlink bereits vorhanden" fi run "Nginx-Konfiguration testen" nginx -t \ || handle_error "Nginx-Konfiguration fehlerhaft — bitte manuell prüfen." run "Nginx neu laden" systemctl reload nginx \ || handle_error "Nginx konnte nicht neu geladen werden." print_step "5.4 — Prüfung" if run "HTTPS-Test" curl -sf "https://${PSONO_DOMAIN}/server/info/" > /dev/null; then echo -e " ${INFO} ${GREEN}Psono ist über HTTPS erreichbar!${RESET}" else echo -e "$WARN HTTPS-Test fehlgeschlagen — DNS noch nicht umgestellt?" echo -e " ${INFO} Test mit direkter IP-Auflösung:" if curl -sf --resolve "${PSONO_DOMAIN}:443:127.0.0.1" \ "https://${PSONO_DOMAIN}/server/info/" > /dev/null 2>&1; then echo -e "$OK Lokal erreichbar — DNS-Umstellung ausstehend" else handle_error "Psono nicht erreichbar. Nginx-Logs prüfen." fi fi pause_phase "5" "6" # ════════════════════════════════════════════════════════════ # PHASE 6 — Abschluss & Backups # ════════════════════════════════════════════════════════════ print_phase "6" "Abschluss & Backups einrichten" print_step "6.1 — Backup-Verzeichnis anlegen" run "Verzeichnis /opt/backups/psono anlegen" mkdir -p /opt/backups/psono \ || handle_error "Backup-Verzeichnis konnte nicht angelegt werden." print_step "6.2 — Cronjobs einrichten" CRON_BACKUP="0 2 * * * PGPASSWORD='${DB_PASSWORD}' pg_dump -U psono -Ft psono > /opt/backups/psono/psono-\$(date +\\%Y-\\%m-\\%d).tar" CRON_CLEANUP="0 3 * * * find /opt/backups/psono -name '*.tar' -mtime +30 -delete" (crontab -l 2>/dev/null | grep -v "psono"; echo "$CRON_BACKUP"; echo "$CRON_CLEANUP") | crontab - \ && echo -e "$OK Cronjobs eingerichtet" \ || echo -e "$WARN Cronjobs konnten nicht eingerichtet werden — bitte manuell eintragen" echo -e " ${INFO} Backup täglich um 02:00, Aufbewahrung 30 Tage" # ── Abschlussbericht ───────────────────────────────────────── echo "" echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════════════════════╗${RESET}" echo -e "${BOLD}${GREEN}║ Migration erfolgreich abgeschlossen! ║${RESET}" echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════════════╝${RESET}" echo "" echo -e " ${BOLD}Psono läuft unter:${RESET} ${CYAN}https://${PSONO_DOMAIN}${RESET}" echo -e " ${BOLD}Admin-Panel:${RESET} ${CYAN}https://${PSONO_DOMAIN}/portal/login${RESET}" echo -e " ${BOLD}Backups:${RESET} ${CYAN}/opt/backups/psono/${RESET}" echo "" echo -e " ${YELLOW}Nächste Schritte:${RESET}" echo -e " • Login mit bekanntem Benutzer testen" echo -e " • Passwörter in Tresoren prüfen" echo -e " • Alten psono-combo Container auf der NAS stoppen" echo -e " • Nach 1–2 Wochen stabilem Betrieb: alte Container löschen" echo ""