setup.ps1

#Requires -RunAsAdministrator
<#
.SYNOPSIS
    Webhook Receiver — Setup & Dienst-Verwaltung via pm2
 
.DESCRIPTION
    Installiert Node.js-Abhängigkeiten, richtet pm2 als Windows-Dienst ein,
    erstellt den IIS-Unterordner "webhook" und konfiguriert ARR/URL Rewrite.
    PM2_HOME wird auf das App-Verzeichnis gesetzt damit alle pm2-Daten
    zusammen mit dem Projekt liegen.
    Autostart via Scheduled Task (AtStartup) — zuverlässiger als Registry-Login-Hook.
 
.PARAMETER Action
    setup — Erstinstallation (npm install, pm2, IIS webhook-Ordner einrichten)
    start — Receiver + Worker starten
    stop — Receiver + Worker stoppen
    restart — Beide Prozesse neu starten
    status — Aktuellen Status anzeigen
    logs — Live-Logs anzeigen (Ctrl+C zum Beenden)
    uninstall — pm2-Dienst und alle Prozesse entfernen
 
.PARAMETER IisSiteName
    Name der IIS-Website in der der webhook-Unterordner erstellt wird.
    Default: "Default Web Site"
 
.PARAMETER IisWebRoot
    Physischer Pfad der IIS-Website.
    Default: C:\inetpub\wwwroot
 
.PARAMETER AutostartUser
    Windows-Account unter dem der pm2-Autostart-Task läuft.
    Muss SQL Server Zugriff haben wenn Windows Auth verwendet wird.
    Default: aktuell eingeloggter User
 
.EXAMPLE
    .\setup.ps1 -Action setup
    .\setup.ps1 -Action setup -AutostartUser "DOMAIN\SvcAccount"
    .\setup.ps1 -Action setup -IisSiteName "MeineSeite" -IisWebRoot "D:\wwwroot"
    pm2 save # aktuellen Stand als Dump speichern
    .\setup.ps1 -Action setup -AutostartUser "aptservice"
    .\setup.ps1 -Action start
    .\setup.ps1 -Action status
#>


param(
    [Parameter(Mandatory = $true)]
    [ValidateSet('setup', 'start', 'stop', 'restart', 'status', 'logs', 'uninstall')]
    [string] $Action,

    [string] $IisSiteName    = 'Default Web Site',
    [string] $IisWebRoot     = 'C:\inetpub\wwwroot',
    [string] $AutostartUser  = $env:USERNAME
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

# ------------------------------------------------------------
# Konfiguration
# ------------------------------------------------------------
$AppDir       = $PSScriptRoot
$ReceiverName = 'webhook-receiver'
$WorkerName   = 'webhook-worker'
$NodeExe      = 'node'
$Pm2Exe       = 'pm2'
$WebhookDir   = Join-Path $IisWebRoot 'webhook'
$Pm2Home      = Join-Path $AppDir '.pm2'
$TaskName     = 'PM2-Autostart-WebhookReceiver'
$TaskPath     = '\Apteco\'

# PM2_HOME für die aktuelle Session setzen
$env:PM2_HOME = $Pm2Home

# ------------------------------------------------------------
# Hilfsfunktionen
# ------------------------------------------------------------
function Write-Step([string]$msg) {
    Write-Host "`n▶ $msg" -ForegroundColor Cyan
}

function Write-Ok([string]$msg) {
    Write-Host " ✓ $msg" -ForegroundColor Green
}

function Write-Warn([string]$msg) {
    Write-Host " ! $msg" -ForegroundColor Yellow
}

function Assert-Command([string]$cmd) {
    if (-not (Get-Command $cmd -ErrorAction SilentlyContinue)) {
        throw "Befehl '$cmd' nicht gefunden. Bitte installieren und PATH prüfen."
    }
}

function Invoke-Pm2([string[]]$pmArgs) {
    & $Pm2Exe @pmArgs
    if ($LASTEXITCODE -ne 0) {
        throw "pm2 beendete mit Exit-Code $LASTEXITCODE"
    }
}

# ------------------------------------------------------------
# PM2_HOME dauerhaft als Systemvariable setzen
# ------------------------------------------------------------
function Set-Pm2HomeEnv {
    Write-Step "PM2_HOME als Systemvariable setzen"
    $current = [System.Environment]::GetEnvironmentVariable('PM2_HOME', 'Machine')
    if ($current -ne $Pm2Home) {
        [System.Environment]::SetEnvironmentVariable('PM2_HOME', $Pm2Home, 'Machine')
        Write-Ok "PM2_HOME = $Pm2Home"
    } else {
        Write-Ok "PM2_HOME bereits korrekt: $Pm2Home"
    }
}

# ------------------------------------------------------------
# Autostart via Scheduled Task (AtStartup + pm2 resurrect)
# Zuverlässiger als pm2-windows-startup (Registry/Login-Hook),
# da der Task auch ohne interaktiven Login beim Systemstart läuft.
# ------------------------------------------------------------
function Install-AutostartTask {
    Write-Step "Autostart Scheduled Task einrichten (Account: $AutostartUser)"

    # Task-Ordner anlegen
    $scheduler = New-Object -ComObject Schedule.Service
    $scheduler.Connect()
    $rootFolder = $scheduler.GetFolder('\')
    try   { $rootFolder.GetFolder('Apteco') | Out-Null }
    catch { $rootFolder.CreateFolder('Apteco') | Out-Null }

    $pm2Path = (Get-Command $Pm2Exe -ErrorAction Stop).Source

    # Aktion: pm2 resurrect — lädt gespeicherten Prozess-Dump
    $action = New-ScheduledTaskAction `
        -Execute          $pm2Path `
        -Argument         'resurrect' `
        -WorkingDirectory $AppDir

    # Trigger: beim Systemstart
    $trigger = New-ScheduledTaskTrigger -AtStartup

    # Einstellungen
    $settings = New-ScheduledTaskSettingsSet `
        -ExecutionTimeLimit  (New-TimeSpan -Minutes 2) `
        -RestartCount        3 `
        -RestartInterval     (New-TimeSpan -Minutes 1) `
        -MultipleInstances   IgnoreNew `
        -StartWhenAvailable

    # Passwort abfragen
    $cred = Get-Credential -UserName $AutostartUser `
        -Message "Passwort für Scheduled Task Account '$AutostartUser'"

    $principal = New-ScheduledTaskPrincipal `
        -UserId    $AutostartUser `
        -LogonType Password `
        -RunLevel  Highest

    Register-ScheduledTask `
        -TaskName    $TaskName `
        -TaskPath    $TaskPath `
        -Action      $action `
        -Trigger     $trigger `
        -Settings    $settings `
        -Principal   $principal `
        -Password    $cred.GetNetworkCredential().Password `
        -Force | Out-Null

    Write-Ok "Task '$TaskPath$TaskName' registriert"
    Write-Ok "Läuft als: $AutostartUser"
    Write-Ok "Trigger: AtStartup → pm2 resurrect"
    Write-Warn "Hinweis: Account muss SQL Server Zugriff haben wenn Windows Auth aktiv ist."
}

function Remove-AutostartTask {
    try {
        Unregister-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -Confirm:$false -ErrorAction Stop
        Write-Ok "Autostart Task '$TaskName' entfernt"
    } catch {
        Write-Warn "Task nicht gefunden oder Fehler beim Entfernen: $_"
    }
}

# ------------------------------------------------------------
# IIS webhook-Verzeichnis und web.config einrichten
# ------------------------------------------------------------
function Install-IisWebhook {
    Write-Step 'IIS: WebAdministration Modul laden'
    Import-Module WebAdministration -ErrorAction Stop
    Write-Ok 'WebAdministration geladen'

    Write-Step "IIS: Unterverzeichnis '$WebhookDir' anlegen"
    if (-not (Test-Path $WebhookDir)) {
        New-Item -ItemType Directory -Path $WebhookDir | Out-Null
        Write-Ok "Verzeichnis erstellt: $WebhookDir"
    } else {
        Write-Ok "Verzeichnis existiert bereits: $WebhookDir"
    }

    Write-Step 'IIS: web.config in webhook-Verzeichnis kopieren'
    $srcConfig = Join-Path $AppDir 'web.config'
    $dstConfig = Join-Path $WebhookDir 'web.config'
    Copy-Item -Path $srcConfig -Destination $dstConfig -Force
    Write-Ok "web.config kopiert nach $dstConfig"

    Write-Step "IIS: Virtual Directory 'webhook' unter '$IisSiteName' einrichten"
    $vdPath = "IIS:\Sites\$IisSiteName\webhook"
    if (-not (Test-Path $vdPath)) {
        New-WebVirtualDirectory -Site $IisSiteName -Name 'webhook' -PhysicalPath $WebhookDir | Out-Null
        Write-Ok "Virtual Directory 'webhook' erstellt"
    } else {
        Write-Ok "Virtual Directory 'webhook' existiert bereits"
    }

    Write-Step 'IIS: HTTP_X_FORWARDED_FOR als allowed Server Variable registrieren'
    $filterPath = 'system.webServer/rewrite/allowedServerVariables'
    $existing = Get-WebConfigurationProperty `
        -PSPath 'MACHINE/WEBROOT/APPHOST' `
        -Filter $filterPath `
        -Name '.' `
        -ErrorAction SilentlyContinue |
        Where-Object { $_.name -eq 'HTTP_X_FORWARDED_FOR' }

    if (-not $existing) {
        Add-WebConfigurationProperty `
            -PSPath 'MACHINE/WEBROOT/APPHOST' `
            -Filter $filterPath `
            -Name '.' `
            -Value @{ name = 'HTTP_X_FORWARDED_FOR' }
        Write-Ok 'HTTP_X_FORWARDED_FOR registriert'
    } else {
        Write-Ok 'HTTP_X_FORWARDED_FOR bereits registriert'
    }
}

# ------------------------------------------------------------
# Actions
# ------------------------------------------------------------
switch ($Action) {

    'setup' {
        # --- Node.js Version prüfen ---
        Write-Step 'Voraussetzungen prüfen'
        Assert-Command $NodeExe
        $nodeVersion = & $NodeExe --version
        $verMatch = $nodeVersion -match 'v(\d+)\.(\d+)'
        if ($verMatch) {
            $major = [int]$Matches[1]
            $minor = [int]$Matches[2]
            if ($major -lt 22 -or ($major -eq 22 -and $minor -lt 5)) {
                throw "Node.js $nodeVersion zu alt. Bitte Node.js 22.5 oder neuer installieren (https://nodejs.org)."
            }
        }
        Write-Ok "Node.js $nodeVersion"

        # --- PM2_HOME dauerhaft setzen ---
        Set-Pm2HomeEnv

        # --- npm install ---
        Write-Step 'npm install'
        Push-Location $AppDir
        npm install --omit=dev
        if ($LASTEXITCODE -ne 0) { throw 'npm install fehlgeschlagen' }
        Write-Ok 'Abhängigkeiten installiert'
        Pop-Location

        # --- .env prüfen ---
        Write-Step '.env prüfen'
        $envPath = Join-Path $AppDir '.env'
        if (-not (Test-Path $envPath)) {
            Copy-Item (Join-Path $AppDir '.env.example') $envPath
            Write-Warn '.env aus .env.example erstellt — bitte Werte eintragen!'
        } else {
            Write-Ok '.env vorhanden'
        }

        # --- pm2 installieren ---
        Write-Step 'pm2 global installieren'
        $pm2Version = & npm list -g pm2 --depth=0 2>$null
        if ($pm2Version -notmatch 'pm2@') {
            npm install -g pm2
            if ($LASTEXITCODE -ne 0) { throw 'pm2 Installation fehlgeschlagen' }
            Write-Ok 'pm2 installiert'
        } else {
            Write-Ok "pm2 bereits vorhanden ($($pm2Version.Trim()))"
        }

        # --- IIS einrichten ---
        Install-IisWebhook

        # --- pm2 Prozesse registrieren ---
        Write-Step 'Prozesse in pm2 registrieren'
        Push-Location $AppDir

        $receiverScript = Join-Path $AppDir 'src\server.js'
        $workerScript   = Join-Path $AppDir 'src\worker.js'

        # Alte Einträge entfernen falls vorhanden
        & $Pm2Exe delete $ReceiverName 2>$null
        & $Pm2Exe delete $WorkerName   2>$null

        Invoke-Pm2 @('start', $receiverScript,
            '--name', $ReceiverName,
            '--interpreter', $NodeExe
        )
        Invoke-Pm2 @('start', $workerScript,
            '--name', $WorkerName,
            '--interpreter', $NodeExe
        )

        # Dump speichern (wird von "pm2 resurrect" beim Autostart geladen)
        Invoke-Pm2 @('save')
        Write-Ok 'pm2 Prozess-Dump gespeichert'

        Pop-Location

        # --- Autostart Scheduled Task einrichten ---
        Install-AutostartTask

        Write-Host ''
        Write-Ok 'Setup abgeschlossen!'
        Write-Host ''
        Write-Host ' Nächste Schritte:' -ForegroundColor White
        Write-Host " 1. .env anpassen: notepad $AppDir\.env" -ForegroundColor White
        Write-Host " 2. endpoints.yaml: notepad $AppDir\endpoints.yaml" -ForegroundColor White
        Write-Host ' 3. Neu starten: .\setup.ps1 -Action restart' -ForegroundColor White
        Write-Host " 4. pm2-Daten liegen in: $Pm2Home" -ForegroundColor White
        Write-Host " 5. Autostart Task: $TaskPath$TaskName" -ForegroundColor White
    }

    'start' {
        Write-Step "Starte $ReceiverName und $WorkerName"
        Push-Location $AppDir
        Invoke-Pm2 @('start', $ReceiverName)
        Invoke-Pm2 @('start', $WorkerName)
        Invoke-Pm2 @('save')
        Pop-Location
        Write-Ok 'Gestartet'
        & $Pm2Exe list
    }

    'stop' {
        Write-Step "Stoppe $ReceiverName und $WorkerName"
        Invoke-Pm2 @('stop', $ReceiverName)
        Invoke-Pm2 @('stop', $WorkerName)
        Invoke-Pm2 @('save')
        Write-Ok 'Gestoppt'
    }

    'restart' {
        Write-Step "Starte neu: $ReceiverName und $WorkerName"
        Invoke-Pm2 @('restart', $ReceiverName)
        Invoke-Pm2 @('restart', $WorkerName)
        Write-Ok 'Neustart abgeschlossen'
        & $Pm2Exe list
    }

    'status' {
        Write-Step 'pm2 Prozessliste'
        & $Pm2Exe list
        Write-Step 'Autostart Task Status'
        try {
            $task = Get-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -ErrorAction Stop
            $info = Get-ScheduledTaskInfo -TaskName $TaskName -TaskPath $TaskPath
            Write-Host " Task State: $($task.State)" -ForegroundColor White
            Write-Host " Letzter Run: $($info.LastRunTime)" -ForegroundColor White
            Write-Host " Letztes Ergebnis: 0x$($info.LastTaskResult.ToString('X'))" -ForegroundColor White
            Write-Host " Nächster Run: $($info.NextRunTime)" -ForegroundColor White
        } catch {
            Write-Warn "Autostart Task nicht gefunden — setup ausführen?"
        }
        Write-Step 'Queue-Statistik (HTTP)'
        try {
            $port = 3000
            $envFile = Join-Path $AppDir '.env'
            if (Test-Path $envFile) {
                $portLine = Select-String -Path $envFile -Pattern '^PORT=(\d+)'
                if ($portLine) { $port = [int]$portLine.Matches[0].Groups[1].Value }
            }
            $stats = Invoke-RestMethod "http://127.0.0.1:$port/admin/queue-stats" -TimeoutSec 3
            $stats.stats | ConvertTo-Json -Depth 3
        } catch {
            Write-Warn "Queue-Stats nicht erreichbar (Receiver läuft?): $_"
        }
    }

    'logs' {
        Write-Step "Live-Logs ($ReceiverName + $WorkerName) — Ctrl+C zum Beenden"
        & $Pm2Exe logs $ReceiverName $WorkerName --lines 50
    }

    'uninstall' {
        Write-Step 'pm2 Prozesse entfernen'
        & $Pm2Exe delete $ReceiverName 2>$null
        & $Pm2Exe delete $WorkerName   2>$null
        & $Pm2Exe save --force 2>$null

        Write-Step 'Autostart Task entfernen'
        Remove-AutostartTask

        Write-Step 'PM2_HOME Systemvariable entfernen'
        [System.Environment]::SetEnvironmentVariable('PM2_HOME', $null, 'Machine')
        Write-Ok 'PM2_HOME entfernt'

        Write-Step 'IIS Virtual Directory entfernen'
        try {
            Import-Module WebAdministration -ErrorAction Stop
            $vdPath = "IIS:\Sites\$IisSiteName\webhook"
            if (Test-Path $vdPath) {
                Remove-Item $vdPath -Recurse -Force
                Write-Ok "Virtual Directory 'webhook' entfernt"
            } else {
                Write-Ok "Virtual Directory 'webhook' nicht gefunden — übersprungen"
            }
        } catch {
            Write-Warn "IIS-Cleanup fehlgeschlagen: $_"
        }

        Write-Ok 'Deinstallation abgeschlossen'
        Write-Warn "Node-Module, .env, .pm2-Ordner und DB bleiben erhalten."
        Write-Warn "Manuell löschen falls gewünscht: $AppDir\.pm2"
    }
}