diff --git a/deploy-production.sh b/deploy-production.sh index 04218f8..40cceec 100755 --- a/deploy-production.sh +++ b/deploy-production.sh @@ -293,8 +293,20 @@ if [ ! -d "$BACKUP_DIR/server-data" ]; then exit 1 fi -mkdir -p server/data -cp -a "$BACKUP_DIR/server-data/." server/data/ +# WICHTIG: Prüfe, ob server/data ein Symlink ist (z.B. auf Backup-Verzeichnis) +# Falls ja, entferne den Symlink, damit wir in das echte Verzeichnis kopieren können +if [ -L "server/data" ]; then + echo " WARNING: server/data ist ein Symlink. Entferne Symlink..." + rm "server/data" +fi + +# Stelle sicher, dass server/data ein echtes Verzeichnis ist +if [ ! -d "server/data" ]; then + mkdir -p server/data +fi + +# Kopiere Daten vom Backup (verwende -L, um Symlinks zu folgen, falls nötig) +cp -aL "$BACKUP_DIR/server-data/." server/data/ echo " Restored server/data from backup ($BACKUP_DIR/server-data)." # Stelle alle CSVs wieder her diff --git a/deploy-test.sh b/deploy-test.sh new file mode 100755 index 0000000..390c9dc --- /dev/null +++ b/deploy-test.sh @@ -0,0 +1,432 @@ +#!/bin/bash +set -euo pipefail + +# Deployment Script für Harheimer TC Test-Instanz +# Verwendet separate Backup- und Data-Verzeichnisse + +# Immer im Repo-Verzeichnis arbeiten (wichtig für Backup/Restore mit relativen Pfaden) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +echo "=== Harheimer TC Test-Instanz Deployment ===" +echo "" +echo "Working directory: $(pwd)" +echo "" + +if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo "ERROR: Dieses Script muss im Git-Repository ausgeführt werden (kein .git gefunden)." + exit 1 +fi + +# Separate Verzeichnisse für Test-Instanz +DEPLOY_HOME="${DEPLOY_HOME:-${HOME:-/tmp}}" +DATA_ROOT="${DATA_ROOT:-$DEPLOY_HOME/harheimertc.test-data}" +BACKUP_ROOT="${BACKUP_ROOT:-$DEPLOY_HOME/harheimertc.test-backups}" + +mkdir -p "$DATA_ROOT" "$BACKUP_ROOT" + +ensure_symlink_dir() { + local src="$1" # z.B. server/data + local target="$2" # z.B. /var/lib/harheimertc.test/server-data + + mkdir -p "$(dirname "$src")" + mkdir -p "$target" + + if [ -L "$src" ]; then + return 0 + fi + + if [ -d "$src" ]; then + echo " Moving $src -> $target (first-time migration)" + # Merge existing content into target + cp -a "$src/." "$target/" || true + rm -rf "$src" + fi + + ln -s "$target" "$src" + echo " Linked $src -> $target" +} + +has_tracked_files_under() { + local prefix="$1" # e.g. public/data + # If any file is tracked under this path, symlinking the directory will break git operations + git ls-files "$prefix" | head -n 1 | grep -q . +} + +echo "0. Ensuring persistent data directories (recommended)..." +# IMPORTANT: Only symlink server/data if it's not tracked by git. +if has_tracked_files_under "server/data"; then + echo " Skipping symlink for server/data (tracked files detected in git)." + echo " Recommendation: remove server/data/** from git history and keep them only as production data." +else + ensure_symlink_dir "server/data" "$DATA_ROOT/server-data" +fi + +# IMPORTANT: Only symlink public/data if it's not tracked by git. +# Otherwise git will error with "path is beyond a symbolic link". +if has_tracked_files_under "public/data"; then + echo " Skipping symlink for public/data (tracked files detected in git)." + echo " Recommendation: remove public/data/*.csv from git history and keep them only as production data." +else + ensure_symlink_dir "public/data" "$DATA_ROOT/public-data" +fi + +ensure_symlink_dir "public/uploads" "$DATA_ROOT/public-uploads" +echo "" + +# 1. BACKUP: Laufende Produktivdaten VOR allen Git-Operationen sichern +echo "1. Backing up current production data (pre-git)..." + +# Human readable timestamp (lokal) +BACKUP_TS="$(date +"%Y-%m-%d_%H-%M-%S")" +BACKUP_DIR="$BACKUP_ROOT/backup_$BACKUP_TS" +mkdir -p "$BACKUP_DIR" +echo " Backup directory: $BACKUP_DIR" + +# Backup server data (JSON) und CSVs immer vom Dateisystem, nicht aus 'stash' +if [ -d server/data ]; then + cp -a server/data "$BACKUP_DIR/server-data" + echo " Backed up server/data -> $BACKUP_DIR/server-data" +else + echo "ERROR: server/data existiert nicht. Abbruch, damit wir keine Repo-Defaults ausrollen." + exit 1 +fi + +if ls public/data/*.csv >/dev/null 2>&1; then + mkdir -p "$BACKUP_DIR/public-data" + cp -a public/data/*.csv "$BACKUP_DIR/public-data/" + echo " Backed up public/data/*.csv -> $BACKUP_DIR/public-data/" +else + echo " No public CSVs to backup (public/data/*.csv not found)" +fi + +# 2. Handle local changes and Git Pull +echo "2. Handling local changes and pulling latest from git..." + +# Check if there are merge conflicts first +if [ -n "$(git status --porcelain | grep '^UU\|^AA\|^DD')" ]; then + echo " Resolving existing merge conflicts..." + git reset --hard HEAD +fi + +# Ensure git operations can run even if we have untracked local artifacts (e.g. backups/) +# We avoid `git stash` (breaks with symlinked tracked paths). Instead, hard-reset and clean safely. +echo " Resetting working tree to HEAD (production data will be restored from backup)..." +git reset --hard HEAD + +echo " Cleaning untracked files (excluding data dirs/backups/.env)..." +git clean -fd \ + -e server/data \ + -e public/data \ + -e public/uploads \ + -e backups \ + -e .env || true + +# Pull latest changes +echo " Pulling latest changes..." +git pull + +# Reset any accidental changes from stash restore (should be none now) +git reset --hard HEAD >/dev/null 2>&1 + +# WICHTIG: Entferne public/data Dateien aus Git-Index, falls sie getrackt sind +# (Sie sollten in .gitignore sein, aber falls sie historisch getrackt wurden) +if git ls-files --error-unmatch public/data/*.csv >/dev/null 2>&1; then + echo " WARNING: public/data/*.csv Dateien sind im Git getrackt!" + echo " Entferne sie aus dem Git-Index (Dateien bleiben erhalten)..." + git rm --cached public/data/*.csv 2>/dev/null || true +fi + +# 3. Install dependencies +echo "" +echo "3. Installing dependencies..." +npm install + +# 4. Remove old build (but keep data!) +echo "" +echo "4. Cleaning build artifacts..." +# Sicherstellen, dass .output vollständig gelöscht wird +if [ -d ".output" ]; then + echo " Removing .output directory..." + rm -rf .output + # Prüfen, ob wirklich gelöscht wurde + if [ -d ".output" ]; then + echo "WARNING: .output konnte nicht vollständig gelöscht werden. Versuche erneut..." + sleep 2 + rm -rf .output + if [ -d ".output" ]; then + echo "ERROR: .output konnte auch nach erneutem Versuch nicht gelöscht werden!" + echo "Bitte manuell prüfen und löschen: rm -rf .output" + exit 1 + fi + fi + echo " ✓ .output gelöscht" +fi + +# Auch .nuxt Cache löschen für sauberen Build +if [ -d ".nuxt" ]; then + echo " Removing .nuxt cache..." + rm -rf .nuxt + echo " ✓ .nuxt gelöscht" +fi + +# Prüfe, ob node_modules vorhanden ist (für npm run build) +if [ ! -d "node_modules" ]; then + echo "" + echo "WARNING: node_modules fehlt. Installiere Dependencies..." + npm install +fi + +# 5. Build +echo "" +echo "5. Building application..." +echo " Running: npm run build" +echo " (This may take a few minutes...)" + +# Clean-Build: Stelle sicher, dass node_modules aktuell ist +echo " Checking dependencies..." +if [ ! -f "node_modules/.package-lock.json" ] && [ ! -f "package-lock.json" ]; then + echo " WARNING: package-lock.json fehlt. Führe npm install aus..." + npm install +fi + +# Build mit expliziter Fehlerbehandlung und Output-Capture +BUILD_OUTPUT=$(npm run build 2>&1) +BUILD_EXIT_CODE=$? + +# Zeige Build-Output +echo "$BUILD_OUTPUT" + +if [ "$BUILD_EXIT_CODE" -ne 0 ]; then + echo "" + echo "ERROR: Build fehlgeschlagen mit Exit-Code $BUILD_EXIT_CODE" + echo "Bitte prüfen Sie die Build-Ausgabe oben auf Fehler." + exit 1 +fi + +# Prüfe auf Warnungen im Build-Output, die auf Probleme hinweisen +if echo "$BUILD_OUTPUT" | grep -qi "error\|failed\|missing"; then + echo "" + echo "WARNING: Build-Output enthält möglicherweise Fehler oder Warnungen." + echo "Bitte prüfen Sie die Ausgabe oben." +fi + +# Prüfe, ob der Build erfolgreich war - mehrere Checks +echo "" +echo " Verifying build output..." + +BUILD_FAILED=0 + +# Check 1: _nuxt Verzeichnis +if [ ! -d ".output/public/_nuxt" ]; then + echo "ERROR: .output/public/_nuxt Verzeichnis fehlt!" + BUILD_FAILED=1 +else + NUXT_FILES=$(find .output/public/_nuxt -type f 2>/dev/null | wc -l) + echo " ✓ .output/public/_nuxt vorhanden ($NUXT_FILES Dateien)" + if [ "$NUXT_FILES" -eq 0 ]; then + echo "ERROR: .output/public/_nuxt ist leer!" + BUILD_FAILED=1 + else + # Prüfe, ob wichtige Dateien vorhanden sind + JS_FILES=$(find .output/public/_nuxt -name "*.js" 2>/dev/null | wc -l) + CSS_FILES=$(find .output/public/_nuxt -name "*.css" 2>/dev/null | wc -l) + echo " ✓ JS-Dateien: $JS_FILES, CSS-Dateien: $CSS_FILES" + if [ "$JS_FILES" -eq 0 ]; then + echo "ERROR: Keine JS-Dateien in .output/public/_nuxt gefunden!" + BUILD_FAILED=1 + fi + fi +fi + +# Check 1b: _nuxt/builds Verzeichnis (für Meta-Dateien) +if [ -d ".output/public/_nuxt/builds" ]; then + BUILD_META_FILES=$(find .output/public/_nuxt/builds -type f 2>/dev/null | wc -l) + echo " ✓ .output/public/_nuxt/builds vorhanden ($BUILD_META_FILES Meta-Dateien)" +else + echo "WARNING: .output/public/_nuxt/builds fehlt (kann in manchen Nuxt-Versionen normal sein)" +fi + +# Check 2: Server index.mjs +if [ ! -f ".output/server/index.mjs" ]; then + echo "ERROR: .output/server/index.mjs fehlt!" + BUILD_FAILED=1 +else + echo " ✓ .output/server/index.mjs vorhanden" +fi + +# Check 3: Public Verzeichnis +if [ ! -d ".output/public" ]; then + echo "ERROR: .output/public Verzeichnis fehlt!" + BUILD_FAILED=1 +else + echo " ✓ .output/public vorhanden" +fi + +# Check 4: Server Verzeichnis +if [ ! -d ".output/server" ]; then + echo "ERROR: .output/server Verzeichnis fehlt!" + BUILD_FAILED=1 +else + echo " ✓ .output/server vorhanden" +fi + +if [ "$BUILD_FAILED" -eq 1 ]; then + echo "" + echo "ERROR: Build-Verifikation fehlgeschlagen!" + echo "Bitte führen Sie manuell aus:" + echo " rm -rf .output .nuxt" + echo " npm run build" + exit 1 +fi + +echo " ✓ Build erfolgreich verifiziert" + +# 6. Restore Production Data (überschreibe Repo-Defaults mit Backup) +echo "" +echo "6. Restoring production data..." + +# Stelle server/data vollständig wieder her (inkl. config.json, users.json, news.json, sessions.json, members.json, membership-applications) +if [ ! -d "$BACKUP_DIR/server-data" ]; then + echo "ERROR: Backup-Verzeichnis $BACKUP_DIR/server-data fehlt. Abbruch." + exit 1 +fi + +# WICHTIG: Prüfe, ob server/data ein Symlink ist (z.B. auf Backup-Verzeichnis) +# Falls ja, entferne den Symlink, damit wir in das echte Verzeichnis kopieren können +if [ -L "server/data" ]; then + echo " WARNING: server/data ist ein Symlink. Entferne Symlink..." + rm "server/data" +fi + +# Stelle sicher, dass server/data ein echtes Verzeichnis ist +if [ ! -d "server/data" ]; then + mkdir -p server/data +fi + +# Kopiere Daten vom Backup (verwende -L, um Symlinks zu folgen, falls nötig) +cp -aL "$BACKUP_DIR/server-data/." server/data/ +echo " Restored server/data from backup ($BACKUP_DIR/server-data)." + +# Stelle alle CSVs wieder her +if ls "$BACKUP_DIR/public-data"/*.csv >/dev/null 2>&1; then + mkdir -p public/data + + # WICHTIG: Überschreibe auch Dateien, die aus dem Git-Repository kommen + # Verwende cp mit -f (force) um sicherzustellen, dass Backup-Dateien Vorrang haben + for csv_file in "$BACKUP_DIR/public-data"/*.csv; do + filename=$(basename "$csv_file") + # Überschreibe explizit, auch wenn Datei bereits existiert + cp -f "$csv_file" "public/data/$filename" + # Stelle sicher, dass die Datei wirklich überschrieben wurde + if [ -f "public/data/$filename" ]; then + # Prüfe, ob die Datei wirklich vom Backup kommt (Größenvergleich) + backup_size=$(stat -f%z "$csv_file" 2>/dev/null || stat -c%s "$csv_file" 2>/dev/null || echo "0") + restored_size=$(stat -f%z "public/data/$filename" 2>/dev/null || stat -c%s "public/data/$filename" 2>/dev/null || echo "0") + if [ "$backup_size" = "$restored_size" ] && [ "$backup_size" != "0" ]; then + echo " ✓ Restored public/data/$filename from backup ($backup_size bytes)" + else + echo " ⚠ WARNING: public/data/$filename Größe stimmt nicht überein (Backup: $backup_size, Restored: $restored_size)" + fi + else + echo " ❌ ERROR: Konnte public/data/$filename nicht wiederherstellen!" + fi + done + + echo " ✓ All public/data/*.csv files restored from backup ($BACKUP_DIR/public-data)." + + # Zusätzliche Sicherheit: Entferne public/data Dateien aus Git-Index, falls sie getrackt sind + # (nach dem Restore, damit sie nicht beim nächsten git reset überschrieben werden) + if git ls-files --error-unmatch public/data/*.csv >/dev/null 2>&1; then + echo " WARNING: public/data/*.csv Dateien sind noch im Git getrackt!" + echo " Entferne sie aus dem Git-Index (Dateien bleiben erhalten)..." + git rm --cached public/data/*.csv 2>/dev/null || true + echo " ✓ public/data/*.csv aus Git-Index entfernt" + fi +else + echo " No public CSVs to restore" +fi + +# Sanity Check: users.json muss existieren und darf nicht leer sein +if [ ! -s server/data/users.json ]; then + echo "ERROR: server/data/users.json fehlt oder ist leer nach Restore. Abbruch." + exit 1 +fi + +# 7. Cleanup (Backups werden bewusst behalten) +echo "" +echo "7. Keeping backups in $BACKUP_ROOT (no git stash used)." + +# 8. Restart PM2 +echo "" +echo "8. Restarting PM2..." + +# Prüfe, ob PM2 installiert ist +if ! command -v pm2 &> /dev/null; then + echo "ERROR: PM2 ist nicht installiert oder nicht im PATH!" + echo "Bitte installieren Sie PM2: npm install -g pm2" + exit 1 +fi + +# Funktion zum Starten/Neustarten einer PM2-Instanz +restart_pm2_instance() { + local instance_name=$1 + local config_file="$SCRIPT_DIR/harheimertc.test.config.cjs" + + # Prüfe, ob die Konfigurationsdatei existiert + if [ ! -f "$config_file" ]; then + echo " ERROR: PM2-Konfigurationsdatei nicht gefunden: $config_file" + return 1 + fi + + if ! pm2 describe "$instance_name" &> /dev/null; then + echo " WARNING: PM2-Prozess '$instance_name' existiert nicht." + echo " Versuche, den Prozess zu starten..." + echo " Verwende Konfiguration: $config_file" + if pm2 start "$config_file" --update-env; then + echo " ✓ PM2-Prozess '$instance_name' gestartet" + return 0 + else + echo " ERROR: Konnte PM2-Prozess '$instance_name' nicht starten." + echo " Bitte manuell prüfen: pm2 logs $instance_name" + return 1 + fi + else + echo " Restarting $instance_name with --update-env..." + if pm2 restart "$instance_name" --update-env; then + echo " ✓ PM2-Prozess '$instance_name' neu gestartet" + return 0 + else + echo " ERROR: PM2-Restart für '$instance_name' fehlgeschlagen!" + return 1 + fi + fi +} + +# Starte/Neustarte Test-Instanz +if ! restart_pm2_instance "harheimertc.test"; then + echo "" + echo "WARNING: PM2-Instanz konnte nicht gestartet werden. Bitte manuell prüfen:" + echo " pm2 status" + echo " pm2 logs harheimertc.test" +fi + +# Prüfe, ob der Prozess läuft +sleep 2 +echo "" +echo " Checking PM2 instance status..." + +if pm2 describe harheimertc.test | grep -q "online"; then + echo " ✓ PM2-Prozess 'harheimertc.test' läuft (online)" +else + echo " WARNING: PM2-Prozess 'harheimertc.test' ist nicht online. Prüfe Logs: pm2 logs harheimertc.test" +fi + +echo "" +echo "=== Test-Instanz Deployment completed successfully! ===" +echo "The test application is now running with the latest code and your test data preserved." +echo "" +echo "Useful commands:" +echo " pm2 logs harheimertc.test # View logs" +echo " pm2 status # View status" +echo " pm2 restart harheimertc.test # Restart manually" diff --git a/harheimertc.test.config.cjs b/harheimertc.test.config.cjs new file mode 100644 index 0000000..a659fa3 --- /dev/null +++ b/harheimertc.test.config.cjs @@ -0,0 +1,62 @@ +// Load environment variables from .env (production secrets) +// Für Test-Instanz: /var/www/harheimertc.test/.env +try { + // eslint-disable-next-line global-require + require('dotenv').config({ path: '/var/www/harheimertc.test/.env' }) +} catch (_e) { + // If dotenv isn't available or .env missing, continue (process.env may be set elsewhere) +} + +// Helper function to create env object +function createEnv(port) { + return { + NODE_ENV: 'production', + PORT: port, + // Secrets/Config (loaded from .env above, if present) + ENCRYPTION_KEY: process.env.ENCRYPTION_KEY, + JWT_SECRET: process.env.JWT_SECRET, + SMTP_HOST: process.env.SMTP_HOST, + SMTP_PORT: process.env.SMTP_PORT, + SMTP_USER: process.env.SMTP_USER, + SMTP_PASS: process.env.SMTP_PASS, + SMTP_FROM: process.env.SMTP_FROM, + SMTP_ADMIN: process.env.SMTP_ADMIN, + NUXT_PUBLIC_BASE_URL: process.env.NUXT_PUBLIC_BASE_URL, + COOKIE_SECURE: process.env.COOKIE_SECURE, + COOKIE_SAMESITE: process.env.COOKIE_SAMESITE, + CSP_ENABLED: process.env.CSP_ENABLED, + CSP_REPORT_ONLY: process.env.CSP_REPORT_ONLY, + CSP_VALUE: process.env.CSP_VALUE, + HIBP_ENABLED: process.env.HIBP_ENABLED, + HIBP_USER_AGENT: process.env.HIBP_USER_AGENT, + HIBP_TIMEOUT_MS: process.env.HIBP_TIMEOUT_MS, + HIBP_CACHE_TTL_MS: process.env.HIBP_CACHE_TTL_MS, + HIBP_FAIL_CLOSED: process.env.HIBP_FAIL_CLOSED, + AUDIT_LOG_ENABLED: process.env.AUDIT_LOG_ENABLED, + WEBAUTHN_ORIGIN: process.env.WEBAUTHN_ORIGIN, + WEBAUTHN_RP_ID: process.env.WEBAUTHN_RP_ID, + WEBAUTHN_RP_NAME: process.env.WEBAUTHN_RP_NAME, + WEBAUTHN_REQUIRE_UV: process.env.WEBAUTHN_REQUIRE_UV + } +} + +module.exports = { + apps: [ + { + name: 'harheimertc.test', + // Nuxt 4 production build: direkt den Node-Server starten (kein "preview mode") + script: 'node', + args: '.output/server/index.mjs', + cwd: '/var/www/harheimertc.test', + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '1G', + env: createEnv(process.env.PORT || 3102), + error_file: '/var/log/pm2/harheimertc.test-error.log', + out_file: '/var/log/pm2/harheimertc.test-out.log', + log_file: '/var/log/pm2/harheimertc.test-combined.log', + time: true + } + ] +}