Files
psono/psono-neuinstallation.sh
T
2026-06-13 18:53:52 +02:00

543 lines
24 KiB
Bash
Executable File

#!/usr/bin/env bash
# ============================================================
# Psono Passwortmanager — Neuinstallations-Script
# Debian 12/13 · PostgreSQL 17 · Docker · Nginx
# ============================================================
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 ""
}
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
}
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
}
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
}
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 " → "
}
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 'echo -e "\n$ERR Script durch Benutzer abgebrochen (Ctrl+C)."; exit 130' INT
# ════════════════════════════════════════════════════════════
# START
# ════════════════════════════════════════════════════════════
clear
print_header "Psono Passwortmanager — Neuinstallation"
echo -e " Dieses Script installiert Psono Community Edition"
echo -e " auf diesem Debian 12/13 Server."
echo ""
echo -e " ${YELLOW}Voraussetzungen:${RESET}"
echo -e " • Docker installiert (download.docker.com)"
echo -e " • PostgreSQL 17 installiert"
echo -e " • Nginx installiert"
echo -e " • Dehydrated mit gültigem Zertifikat für deine Domain"
echo -e " • DNS-Eintrag für deine Domain zeigt auf diesen Server"
echo -e " • SMTP-Zugangsdaten für den E-Mail-Versand"
echo ""
echo -e "${DIM}──────────────────────────────────────────────────────────${RESET}"
# ── Parameter einlesen ───────────────────────────────────────
print_header "Parameter eingeben"
echo -e "${BOLD} 1/7 Domain${RESET}"
read -rp " Psono-Domain (z.B. psono.example.com): " PSONO_DOMAIN
echo ""
echo -e "${BOLD} 2/7 Server-IP${RESET}"
read -rp " Öffentliche IP dieses Servers: " SERVER_IP
echo ""
echo -e "${BOLD} 3/7 Datenbank-Passwort${RESET}"
echo -e "${DIM} Passwort für den PostgreSQL-Benutzer 'psono'${RESET}"
read_password "Passwort" DB_PASSWORD
echo ""
echo -e "${BOLD} 4/7 E-Mail-Absender${RESET}"
read -rp " E-Mail-Adresse für Psono (z.B. psono@example.com): " PSONO_EMAIL
echo ""
echo -e "${BOLD} 5/7 SMTP-Server${RESET}"
read -rp " SMTP-Host (z.B. smtp.example.com): " SMTP_HOST
read -rp " SMTP-Port [587]: " SMTP_PORT
SMTP_PORT="${SMTP_PORT:-587}"
echo ""
echo -e "${BOLD} 6/7 SMTP-Zugangsdaten${RESET}"
read -rp " SMTP-Benutzername: " SMTP_USER
read_password "SMTP-Passwort" SMTP_PASSWORD
echo ""
echo -e "${BOLD} 7/7 Registrierung${RESET}"
echo -e "${DIM} Sollen sich neue Benutzer selbst registrieren können?${RESET}"
read -rp " Registrierung erlauben? [j/N]: " REG_CHOICE
case "${REG_CHOICE,,}" in
j|ja|y|yes) ALLOW_REGISTRATION="True" ;;
*) ALLOW_REGISTRATION="False" ;;
esac
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}E-Mail:${RESET} ${CYAN}${PSONO_EMAIL}${RESET}"
echo -e " ${BOLD}SMTP-Host:${RESET} ${CYAN}${SMTP_HOST}:${SMTP_PORT}${RESET}"
echo -e " ${BOLD}SMTP-Benutzer:${RESET} ${CYAN}${SMTP_USER}${RESET}"
echo -e " ${BOLD}SMTP-Passwort:${RESET} ${CYAN}(gesetzt, ${#SMTP_PASSWORD} Zeichen)${RESET}"
echo -e " ${BOLD}Registrierung:${RESET} ${CYAN}${ALLOW_REGISTRATION}${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"
run "Datenbank 'psono' anlegen" sudo -u postgres psql -c "CREATE DATABASE psono;" \
|| handle_error "Datenbank konnte nicht angelegt werden."
print_step "1.2 — Benutzer anlegen"
run "Benutzer 'psono' anlegen" sudo -u postgres psql \
-c "CREATE USER psono WITH PASSWORD '${DB_PASSWORD}';" \
|| handle_error "Benutzer konnte nicht angelegt werden."
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" \
bash -c "PGPASSWORD='${DB_PASSWORD}' psql -U psono -d psono -c 'SELECT 1;' > /dev/null"; then
echo -e " ${INFO} Verbindung erfolgreich"
else
handle_error "Verbindungstest fehlgeschlagen."
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 — Psono-Konfiguration erstellen
# ════════════════════════════════════════════════════════════
print_phase "2" "Psono-Konfiguration erstellen"
print_step "2.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 "2.2 — Kryptographische Schlüssel generieren"
echo -e " ${INFO} Generiere Schlüssel (Docker-Image wird ggf. heruntergeladen) ..."
echo ""
KEYS_RAW=$(docker run --rm psono/psono-combo:latest \
python3 ./psono/manage.py generateserverkeys 2>/dev/null)
if [[ -z "$KEYS_RAW" ]]; then
handle_error "Schlüssel konnten nicht generiert werden."
fi
SECRET_KEY=$(echo "$KEYS_RAW" | grep "^SECRET_KEY:" | head -1 | sed "s/SECRET_KEY: '//;s/'$//")
ACT_SECRET=$(echo "$KEYS_RAW" | grep "^ACTIVATION_LINK_SECRET:" | head -1 | sed "s/ACTIVATION_LINK_SECRET: '//;s/'$//")
DB_SECRET=$(echo "$KEYS_RAW" | grep "^DB_SECRET:" | head -1 | sed "s/DB_SECRET: '//;s/'$//")
EMAIL_SALT=$(echo "$KEYS_RAW" | grep "^EMAIL_SECRET_SALT:" | head -1 | sed "s/EMAIL_SECRET_SALT: '//;s/'$//")
PRIVATE_KEY=$(echo "$KEYS_RAW" | grep "^PRIVATE_KEY:" | head -1 | sed "s/PRIVATE_KEY: '//;s/'$//")
PUBLIC_KEY=$(echo "$KEYS_RAW" | grep "^PUBLIC_KEY:" | head -1 | sed "s/PUBLIC_KEY: '//;s/'$//")
echo -e "$OK Schlüssel generiert"
echo ""
echo -e " ${YELLOW}${BOLD}WICHTIG: Diese Schlüssel jetzt sichern!${RESET}"
echo -e " ${DIM}Ohne diese Schlüssel sind alle Passwörter verloren.${RESET}"
echo ""
echo -e " ${BOLD}PUBLIC_KEY:${RESET} ${CYAN}${PUBLIC_KEY}${RESET}"
echo -e " ${BOLD}PRIVATE_KEY:${RESET} ${CYAN}${PRIVATE_KEY:0:20}...${RESET} ${DIM}(vollständig in settings.yaml)${RESET}"
echo ""
print_step "2.3 — settings.yaml erstellen"
cat > /opt/docker/psono/settings.yaml << EOF
SECRET_KEY: '${SECRET_KEY}'
ACTIVATION_LINK_SECRET: '${ACT_SECRET}'
DB_SECRET: '${DB_SECRET}'
EMAIL_SECRET_SALT: '${EMAIL_SALT}'
PRIVATE_KEY: '${PRIVATE_KEY}'
PUBLIC_KEY: '${PUBLIC_KEY}'
DEBUG: False
ALLOWED_HOSTS: ['${PSONO_DOMAIN}']
HOST_URL: 'https://${PSONO_DOMAIN}/server'
WEB_CLIENT_URL: 'https://${PSONO_DOMAIN}'
DATABASES:
default:
ENGINE: 'django.db.backends.postgresql_psycopg2'
NAME: 'psono'
USER: 'psono'
PASSWORD: '${DB_PASSWORD}'
HOST: 'host.docker.internal'
PORT: '5432'
CACHE_ENABLE: FALSE
EMAIL_FROM: 'Psono <${PSONO_EMAIL}>'
EMAIL_HOST: '${SMTP_HOST}'
EMAIL_HOST_USER: '${SMTP_USER}'
EMAIL_HOST_PASSWORD: '${SMTP_PASSWORD}'
EMAIL_PORT: ${SMTP_PORT}
EMAIL_USE_TLS: True
ALLOW_REGISTRATION: ${ALLOW_REGISTRATION}
ALLOW_LOST_PASSWORD: True
EOF
echo -e "$OK settings.yaml erstellt: /opt/docker/psono/settings.yaml"
print_step "2.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": ${ALLOW_REGISTRATION,,},
"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: /opt/docker/psono-client/config.json"
pause_phase "2" "3"
# ════════════════════════════════════════════════════════════
# PHASE 3 — Docker Container starten
# ════════════════════════════════════════════════════════════
print_phase "3" "Psono-Container starten"
print_step "3.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 "3.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 "3.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 "3.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 "3" "4"
# ════════════════════════════════════════════════════════════
# PHASE 4 — Nginx einrichten
# ════════════════════════════════════════════════════════════
print_phase "4" "Nginx Reverse Proxy einrichten"
print_step "4.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 "4.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 "4.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 "4.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 propagiert?"
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-Propagierung abwarten"
else
handle_error "Psono nicht erreichbar. Nginx-Logs prüfen."
fi
fi
pause_phase "4" "5"
# ════════════════════════════════════════════════════════════
# PHASE 5 — Ersteinrichtung & Backups
# ════════════════════════════════════════════════════════════
print_phase "5" "Ersteinrichtung & Backups einrichten"
print_step "5.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 "5.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}║ Installation 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 -e " ${BOLD}settings.yaml:${RESET} ${CYAN}/opt/docker/psono/settings.yaml${RESET}"
echo ""
echo -e " ${YELLOW}Nächste Schritte:${RESET}"
echo -e " 1. https://${PSONO_DOMAIN} öffnen und ersten Benutzer registrieren"
echo -e " 2. Admin-Rechte vergeben:"
echo -e " ${DIM}docker exec -ti psono-psono-combo-1 \\"
echo -e " python3 ./psono/manage.py promoteuser BENUTZER@${PSONO_DOMAIN} superuser${RESET}"
echo -e " 3. settings.yaml zusätzlich sichern:"
echo -e " ${DIM}cp /opt/docker/psono/settings.yaml /opt/backups/psono/settings.yaml.bak${RESET}"
echo ""