diff --git a/installer.sh b/installer.sh index 6f87695..fa7b19e 100644 --- a/installer.sh +++ b/installer.sh @@ -44,6 +44,10 @@ NODE_SETUP="${NODE_SETUP:-deb}" GREEN="\033[1;32m"; YELLOW="\033[1;33m"; RED="\033[1;31m"; CYAN="\033[1;36m"; GREY="\033[0;90m"; NC="\033[0m" BAR="──────────────────────────────────────────────────────────────────────────────" +# ===== Install-Log ===== +LOG_FILE="/var/log/mailwolt-install.log" +> "$LOG_FILE" + header() { echo -e "${CYAN}${BAR}${NC}" echo -e "${CYAN} 888b d888 d8b 888 888 888 888 888 ${NC}" @@ -81,9 +85,30 @@ footer_ok() { echo } -log() { echo -e "${GREEN}[+]${NC} $*"; } -warn() { echo -e "${YELLOW}[!]${NC} $*"; } -err() { echo -e "${RED}[x]${NC} $*"; } +_STEP_T=0 +step() { + local msg="$1" dur="${2:-}" + [ -n "$dur" ] && dur=" ${GREY}(~${dur})${NC}" || dur="" + printf "\n${CYAN} ▸${NC} %-42s%b\n" "$msg" "$dur" + _STEP_T=$SECONDS +} +ok() { + local t=$(( SECONDS - _STEP_T )) + [ $t -gt 1 ] && printf " ${GREEN}✔${NC} ${GREY}%ds${NC}\n" $t || printf " ${GREEN}✔${NC}\n" +} +warn() { printf " ${YELLOW}⚠${NC} %s\n" "$*"; } +err() { printf " ${RED}✗${NC} %s\n" "$*"; } + +quietly() { + if ! "$@" >> "$LOG_FILE" 2>&1; then + printf "\n ${RED}✗${NC} Fehlgeschlagen. Letzte Log-Zeilen:\n\n" + tail -20 "$LOG_FILE" | sed 's/^/ /' + printf "\n ${GREY}Vollständiges Log: %s${NC}\n\n" "$LOG_FILE" + exit 1 + fi +} +try_quiet() { "$@" >> "$LOG_FILE" 2>&1 || true; } +log() { :; } # Kompatibilität — stille Ausgabe require_root() { [ "$(id -u)" -eq 0 ] || { err "Bitte als root ausführen."; exit 1; }; } # ===== IP ermitteln ===== @@ -118,22 +143,19 @@ APP_PW="${APP_PW:-$(pw)}" MAIL_HOSTNAME="${MAIL_HOSTNAME:-"bootstrap.local"}" # Wizard setzt später FQDN TZ="${TZ:-""}" # leer; Wizard setzt final -echo -e "${GREY}Server-IP erkannt: ${SERVER_IP}${NC}" +echo -e "\n ${GREY}Server-IP: ${SERVER_IP} Log: ${LOG_FILE}${NC}" [ -n "$TZ" ] && { ln -fs "/usr/share/zoneinfo/${TZ}" /etc/localtime || true; } -log "Paketquellen aktualisieren…" +step "Paketquellen aktualisieren" "10 Sek" export DEBIAN_FRONTEND=noninteractive -apt-get update -y +quietly apt-get update -y +ok -# ---- MariaDB-Workaround (fix für mariadb-common prompt) ---- -log "MariaDB-Workaround vorbereiten…" mkdir -p /etc/mysql /etc/mysql/mariadb.conf.d [ -f /etc/mysql/mariadb.cnf ] || echo '!include /etc/mysql/mariadb.conf.d/*.cnf' > /etc/mysql/mariadb.cnf -# ---- Basis-Pakete installieren ---- -log "Pakete installieren… (dies kann einige Minuten dauern)" -#apt-get install -y \ -apt-get -y -o Dpkg::Options::="--force-confdef" \ +step "Pakete installieren" "2–5 Min" +quietly apt-get -y -o Dpkg::Options::="--force-confdef" \ -o Dpkg::Options::="--force-confold" install \ postfix postfix-mysql \ dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-mysql \ @@ -147,15 +169,15 @@ apt-get -y -o Dpkg::Options::="--force-confdef" \ certbot python3-certbot-nginx \ fail2ban \ ca-certificates rsyslog sudo openssl netcat-openbsd monit git +ok -# ===== Verzeichnisse / User ===== -log "Verzeichnisse und Benutzer anlegen…" +step "Benutzer & Verzeichnisse anlegen" "5 Sek" mkdir -p ${CERT_DIR} /etc/postfix /etc/dovecot/conf.d /etc/rspamd/local.d /var/mail/vhosts -id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail +quietly bash -c "id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail" chown -R vmail:vmail /var/mail - -id "$APP_USER" >/dev/null 2>&1 || adduser --disabled-password --gecos "" "$APP_USER" -usermod -a -G www-data "$APP_USER" +quietly bash -c "id '$APP_USER' >/dev/null 2>&1 || adduser --disabled-password --gecos '' '$APP_USER'" +quietly usermod -a -G www-data "$APP_USER" +ok # ===== Self-signed TLS (SAN = IP) ===== CERT="${CERT_DIR}/cert.pem" @@ -163,7 +185,7 @@ KEY="${CERT_DIR}/key.pem" OSSL_CFG="${CERT_DIR}/openssl.cnf" if [ ! -s "$CERT" ] || [ ! -s "$KEY" ]; then - log "Erzeuge Self-Signed TLS Zertifikat (SAN=IP:${SERVER_IP})…" + step "Self-Signed Zertifikat erstellen" "5 Sek" cat > "$OSSL_CFG" < /etc/dovecot/dovecot.conf <<'CONF' !include_try /etc/dovecot/conf.d/*.conf CONF @@ -358,15 +381,14 @@ ssl_cert = <${CERT} ssl_key = <${KEY} CONF -systemctl enable --now dovecot || true +try_quiet systemctl enable --now dovecot # ===== Rspamd & OpenDKIM ===== -log "Rspamd + OpenDKIM aktivieren…" cat > /etc/rspamd/local.d/worker-controller.inc <<'CONF' password = "admin"; bind_socket = "127.0.0.1:11334"; CONF -systemctl enable --now rspamd || true +try_quiet systemctl enable --now rspamd cat > /etc/opendkim.conf <<'CONF' Syslog yes @@ -382,13 +404,12 @@ LogWhy yes OversignHeaders From # KeyTable / SigningTable später im Wizard CONF -systemctl enable --now opendkim || true - -# ===== Redis ===== -systemctl enable --now redis-server || true +try_quiet systemctl enable --now opendkim +try_quiet systemctl enable --now redis-server +ok # ===== Nginx: Laravel vHost (80/443) ===== -log "Nginx konfigurieren…" +step "Webserver konfigurieren (Nginx)" "5 Sek" rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default || true PHPV=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;') @@ -455,33 +476,23 @@ server { } CONF ln -sf ${NGINX_SITE} ${NGINX_SITE_LINK} -nginx -t && systemctl enable --now nginx || true +try_quiet nginx -t +try_quiet systemctl enable --now nginx +ok -# ===== Node/NPM installieren (für Vite/Tailwind Build) ===== -log "Node/NPM installieren…" -if [ "$NODE_SETUP" = "nodesource" ]; then - curl -fsSL https://deb.nodesource.com/setup_22.x -o /tmp/nodesource_setup.sh - bash /tmp/nodesource_setup.sh - rm -f /tmp/nodesource_setup.sh - apt-get install -y nodejs -else - apt-get install -y nodejs npm -fi - -# ===== Projekt aus Git holen ===== -log "Projekt aus Git klonen…" +step "Projekt herunterladen (Git)" "15 Sek" mkdir -p "$(dirname "$APP_DIR")" chown "$APP_USER":$APP_GROUP "$(dirname "$APP_DIR")" if [ ! -d "${APP_DIR}/.git" ]; then rm -rf "${APP_DIR}" - sudo -u "$APP_USER" -H bash -lc "git clone --depth=1 -b ${GIT_BRANCH} ${GIT_REPO} ${APP_DIR}" + quietly sudo -u "$APP_USER" -H bash -lc "git clone --depth=1 -b ${GIT_BRANCH} ${GIT_REPO} ${APP_DIR}" else - sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && git fetch --depth=1 origin ${GIT_BRANCH} && git checkout ${GIT_BRANCH} && git pull --ff-only" + quietly sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && git fetch --depth=1 origin ${GIT_BRANCH} && git checkout ${GIT_BRANCH} && git pull --ff-only" fi +ok # ===== .env erstellen und befüllen ===== -log ".env konfigurieren…" APP_URL="http://${SERVER_IP}" sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && cp -n .env.example .env || true" @@ -534,34 +545,43 @@ grep -q '^BOOTSTRAP_ADMIN_PASSWORD_HASH=' "${APP_DIR}/.env" \ || echo "BOOTSTRAP_ADMIN_PASSWORD_HASH=${BOOTSTRAP_HASH}" >> "${APP_DIR}/.env" sed -i "s|^BOOTSTRAP_ADMIN_PASSWORD_HASH=.*|BOOTSTRAP_ADMIN_PASSWORD_HASH=${BOOTSTRAP_HASH}|g" "${APP_DIR}/.env" -# ===== Composer Dependencies ===== -log "Composer install…" -sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-dev --optimize-autoloader --no-interaction" +step "PHP-Abhängigkeiten installieren (Composer)" "1–2 Min" +quietly sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-dev --optimize-autoloader --no-interaction" +ok -# ===== App-Key, Migrations & Caches ===== -log "App-Key generieren, Datenbank migrieren, Caches bauen…" -sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan key:generate --force" -sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan migrate --force" -sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan storage:link --force || true" -sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan config:cache" || true -sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan route:cache" || true -sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan view:cache" || true +step "Datenbank migrieren" "15 Sek" +quietly sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan key:generate --force" +quietly sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan migrate --force" +try_quiet sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan storage:link --force" +try_quiet sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan config:cache" +try_quiet sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan route:cache" +try_quiet sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan view:cache" +ok -# ===== Frontend Build ===== if [ -f "${APP_DIR}/package.json" ]; then - log "Frontend bauen…" - sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm ci --no-audit --no-fund || npm install" - sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build" || warn "npm run build fehlgeschlagen – manuell nachholen: cd ${APP_DIR} && npm run build" + step "Frontend bauen (npm)" "1–3 Min" + # Node/npm falls noch nicht installiert + if ! command -v node >/dev/null 2>&1; then + if [ "$NODE_SETUP" = "nodesource" ]; then + quietly bash -c "curl -fsSL https://deb.nodesource.com/setup_22.x -o /tmp/nodesource_setup.sh && bash /tmp/nodesource_setup.sh && rm -f /tmp/nodesource_setup.sh" + quietly apt-get install -y nodejs + else + quietly apt-get install -y nodejs npm + fi + fi + quietly sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm ci --no-audit --no-fund || npm install" + if ! sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build" >> "$LOG_FILE" 2>&1; then + warn "npm run build fehlgeschlagen — manuell nachholen: cd ${APP_DIR} && npm run build" + else + ok + fi fi - -# ===== Wizard State-Verzeichnis ===== mkdir -p /var/lib/mailwolt/wizard chown www-data:www-data /var/lib/mailwolt/wizard chmod 775 /var/lib/mailwolt/wizard -# ===== mailwolt-apply-domains Helper ===== -log "mailwolt-apply-domains Helper installieren…" +step "Hilfsskripte & Konfiguration installieren" "5 Sek" cat > /usr/local/sbin/mailwolt-apply-domains <<'HELPER' #!/usr/bin/env bash set -euo pipefail @@ -758,67 +778,51 @@ GIT_TAG="$(sudo -u "$APP_USER" -H bash -lc "git -C ${APP_DIR} describe --tags -- if [ -n "$GIT_TAG" ]; then echo "${GIT_TAG#v}" > /var/lib/mailwolt/version echo "$GIT_TAG" > /var/lib/mailwolt/version_raw - log "Version: ${GIT_TAG}" else warn "Kein Git-Tag gefunden — Version-Datei wird nicht geschrieben" fi +ok -# ===== App-User/Gruppen & Rechte (am ENDE ausführen) ===== -# User anlegen (nur falls noch nicht vorhanden) + Passwort setzen + Gruppe +step "Berechtigungen setzen & Dienste starten" "10 Sek" if ! id -u "$APP_USER" >/dev/null 2>&1; then - adduser --disabled-password --gecos "" "$APP_USER" + quietly adduser --disabled-password --gecos "" "$APP_USER" fi -echo "${APP_USER}:${APP_PW}" | chpasswd -usermod -a -G "$APP_GROUP" "$APP_USER" +echo "${APP_USER}:${APP_PW}" | quietly chpasswd +quietly usermod -a -G "$APP_GROUP" "$APP_USER" -# Besitz & Rechte chown -R "$APP_USER":"$APP_GROUP" "$APP_DIR" -find "$APP_DIR" -type d -exec chmod 775 {} \; -find "$APP_DIR" -type f -exec chmod 664 {} \; +find "$APP_DIR" -type d -exec chmod 775 {} \; 2>>"$LOG_FILE" +find "$APP_DIR" -type f -exec chmod 664 {} \; 2>>"$LOG_FILE" chmod -R 775 "$APP_DIR"/storage "$APP_DIR"/bootstrap/cache if command -v setfacl >/dev/null 2>&1; then - setfacl -R -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX \ - "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true - setfacl -dR -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX \ - "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true + try_quiet setfacl -R -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" + try_quiet setfacl -dR -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" fi grep -q 'umask 002' /home/${APP_USER}/.profile 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.profile grep -q 'umask 002' /home/${APP_USER}/.bashrc 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.bashrc +try_quiet sudo -u "$APP_USER" -H bash -lc "npm config set umask 0002" -# 7) npm respektiert umask – zur Sicherheit direkt setzen (für APP_USER) -sudo -u "$APP_USER" -H bash -lc "npm config set umask 0002" >/dev/null 2>&1 || true - -# PHP-FPM-Socket group-writable machen FPM_POOL="/etc/php/${PHPV}/fpm/pool.d/www.conf" if [ -f "$FPM_POOL" ]; then sed -i 's/^;*listen\.owner.*/listen.owner = www-data/' "$FPM_POOL" sed -i 's/^;*listen\.group.*/listen.group = www-data/' "$FPM_POOL" sed -i 's/^;*listen\.mode.*/listen.mode = 0660/' "$FPM_POOL" - systemctl restart php${PHPV}-fpm || true + try_quiet systemctl restart php${PHPV}-fpm fi -# 9) Optional: deinem Shell-/IDE-User ebenfalls Schreibrechte geben IDE_USER="${SUDO_USER:-}" if [ -n "$IDE_USER" ] && id "$IDE_USER" >/dev/null 2>&1 && command -v setfacl >/dev/null 2>&1; then - usermod -a -G "$APP_GROUP" "$IDE_USER" || true - setfacl -R -m u:${IDE_USER}:rwX "$APP_DIR" - setfacl -dR -m u:${IDE_USER}:rwX "$APP_DIR" - echo -e "${YELLOW}[i]${NC} Benutzer '${IDE_USER}' wurde für Schreibzugriff freigeschaltet (ACL + Gruppe ${APP_GROUP})." + try_quiet usermod -a -G "$APP_GROUP" "$IDE_USER" + try_quiet setfacl -R -m u:${IDE_USER}:rwX "$APP_DIR" + try_quiet setfacl -dR -m u:${IDE_USER}:rwX "$APP_DIR" fi -# Webstack neu laden -systemctl reload nginx || true -systemctl restart php*-fpm || true +try_quiet systemctl reload nginx +try_quiet systemctl restart php*-fpm +ok -# Hinweis zur neuen Gruppenzugehörigkeit - -echo -e "${YELLOW}[i]${NC} SHELL: Du kannst dich nun als Benutzer '${APP_USER}' mit dem Passwort '${APP_PW}' anmelden." -echo -e "${YELLOW}[i]${NC} Hinweis: Nach dem ersten Login solltest du das Passwort mit 'passwd ${APP_USER}' ändern." -echo -e "${YELLOW}[i]${NC} Damit die Gruppenrechte (${APP_GROUP}) aktiv werden, bitte einmal ab- und wieder anmelden." - -# ===== Monit (Watchdog) – installiert, aber NICHT aktiviert ===== -log "Monit (Watchdog) installieren (deaktiviert)" +# ===== Monit ===== cat > /etc/monit/monitrc <<'EOF' set daemon 60 set logfile syslog facility log_daemon @@ -862,26 +866,37 @@ check process nginx with pidfile /run/nginx.pid if failed port 443 type tcp ssl then restart EOF chmod 600 /etc/monit/monitrc -systemctl enable --now monit || true +try_quiet systemctl enable --now monit -# ===== Smoke-Test (alle Ports, mit Timeouts) ===== -log "Smoke-Test (Ports & Banner):" +# ===== Smoke-Test ===== +step "Port-Check" "30 Sek" set +e -printf "[25] " && timeout 6s bash -lc 'printf "EHLO localhost\r\nQUIT\r\n" | nc -v -w 4 127.0.0.1 25 2>&1' || true -printf "[465] " && timeout 6s openssl s_client -connect 127.0.0.1:465 -brief -quiet &1' || true -printf "[995] " && timeout 6s openssl s_client -connect 127.0.0.1:995 -brief -quiet &1' || true -printf "[993] " && timeout 6s openssl s_client -connect 127.0.0.1:993 -brief -quiet &1) + if echo "$result" | grep -qiE '(220|OK|\+OK|CAPABILITY)'; then + printf " ${GREEN}✔${NC} Port %-5s erreichbar\n" "$port" + (( _smoke_ok++ )) || true + else + printf " ${YELLOW}⚠${NC} Port %-5s nicht erreichbar\n" "$port" + (( _smoke_fail++ )) || true + fi +} +smoke_port 25 "timeout 5s bash -c 'printf \"EHLO localhost\r\nQUIT\r\n\" | nc -w3 127.0.0.1 25'" +smoke_port 465 "timeout 5s openssl s_client -connect 127.0.0.1:465 -brief -quiet