#Requires -Version 5.1 param( [switch]$SkipSelfUpdateCheck ) <# .SYNOPSIS KANBAN-Rational-Druck mit Self-Update, Private-Config-Ladung, SQL-Abfrage und n8n-Webhook. .DESCRIPTION Ablauf: 1. Optional Self-Update aus dem Public-Repo 2. AZ_GITHUB_TOKEN prüfen 3. Private Konfiguration aus dem Private-Repo laden 4. Genau eine Datei aus Input-Ordner lesen 5. Dateiname ohne Endung als Belegnummer verwenden 6. SQL-Abfrage ausführen 7. Ergebnis an n8n senden 8. Logging in Datei .NOTES Repo: AZ-PowerShell-Pub #> $ErrorActionPreference = 'Stop' $script:ScriptName = 'KANBAN-Rational-Druck' $script:PublicRawUrl = 'https://git.az-gruppe.com/AZ-Intec-GmbH/AZ-PowerShell-Pub/raw/branch/main/scripts/KANBAN-Rational-Druck/KANBAN-Rational-Druck.ps1' $script:PrivateRawUrl = 'https://git.az-gruppe.com/AZ-Intec-GmbH/AZ-PowerShell-Prv/raw/branch/main/scripts/KANBAN-Rational-Druck/KANBAN-Rational-Druck.ps1' $script:LogFilePath = $null #region Hilfsfunktionen function Write-ConsoleLog { param( [Parameter(Mandatory = $true)] [string]$Message, [ValidateSet('INFO', 'WARN', 'ERROR', 'SUCCESS')] [string]$Level = 'INFO' ) $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $line = "[$timestamp] [$Level] $Message" Write-Host $line } function Write-Log { param( [Parameter(Mandatory = $true)] [string]$Message, [ValidateSet('INFO', 'WARN', 'ERROR', 'SUCCESS')] [string]$Level = 'INFO' ) $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $line = "[$timestamp] [$Level] $Message" Write-Host $line if ($script:LogFilePath) { Add-Content -Path $script:LogFilePath -Value $line -Encoding UTF8 } } function Get-StringHashSha256 { param( [Parameter(Mandatory = $true)] [string]$Text ) $sha = [System.Security.Cryptography.SHA256]::Create() try { $bytes = [System.Text.Encoding]::UTF8.GetBytes($Text) $hashBytes = $sha.ComputeHash($bytes) return ([System.BitConverter]::ToString($hashBytes)).Replace('-', '') } finally { $sha.Dispose() } } function Test-AzGithubToken { if ([string]::IsNullOrWhiteSpace($env:AZ_GITHUB_TOKEN)) { throw "❌ Umgebungsvariable AZ_GITHUB_TOKEN ist nicht gesetzt." } } function Start-SelfUpdateIfNeeded { param( [Parameter(Mandatory = $true)] [string]$RemoteScriptUrl ) if ($SkipSelfUpdateCheck) { Write-ConsoleLog "Self-Update-Prüfung wurde übersprungen (Restart nach Update)." "INFO" return } $localScriptPath = $MyInvocation.MyCommand.Path if ([string]::IsNullOrWhiteSpace($localScriptPath)) { throw "❌ Lokaler Skriptpfad konnte nicht ermittelt werden." } Write-ConsoleLog "Prüfe auf neue Public-Version aus Git..." "INFO" try { $remoteResponse = Invoke-WebRequest -Uri $RemoteScriptUrl -UseBasicParsing $remoteContent = $remoteResponse.Content if ([string]::IsNullOrWhiteSpace($remoteContent)) { throw "Remote-Skriptinhalt ist leer." } $localContent = Get-Content -Path $localScriptPath -Raw -Encoding UTF8 $localHash = Get-StringHashSha256 -Text $localContent $remoteHash = Get-StringHashSha256 -Text $remoteContent if ($localHash -eq $remoteHash) { Write-ConsoleLog "✓ Public-Skript ist bereits aktuell." "SUCCESS" return } Write-ConsoleLog "⚠ Neue Public-Version gefunden. Aktualisiere lokales Skript..." "WARN" $tempUpdatedScript = Join-Path $env:TEMP ("{0}-{1}.ps1" -f $script:ScriptName, [guid]::NewGuid().ToString()) Set-Content -Path $tempUpdatedScript -Value $remoteContent -Encoding UTF8 Copy-Item -Path $tempUpdatedScript -Destination $localScriptPath -Force Write-ConsoleLog "✓ Lokales Public-Skript wurde aktualisiert." "SUCCESS" Write-ConsoleLog "Starte aktualisierte Version neu..." "INFO" $argumentList = @( '-ExecutionPolicy', 'Bypass', '-File', ('"{0}"' -f $localScriptPath), '-SkipSelfUpdateCheck' ) Start-Process -FilePath 'powershell.exe' -ArgumentList $argumentList | Out-Null Write-ConsoleLog "Aktuelle Instanz wird beendet, damit die neue Version ausgeführt wird." "INFO" exit 0 } catch { throw "❌ Fehler bei der Self-Update-Prüfung des Public-Skripts: $($_.Exception.Message)" } } function Get-PrivateConfig { param( [Parameter(Mandatory = $true)] [string]$RawConfigUrl ) $token = $env:AZ_GITHUB_TOKEN $headers = @{ Authorization = "token $token" } Write-ConsoleLog "Lade private Konfiguration aus dem Private-Repo..." "INFO" try { $response = Invoke-WebRequest -Uri $RawConfigUrl -Headers $headers -UseBasicParsing $content = $response.Content $tempFile = Join-Path $env:TEMP ("KANBAN-Rational-Druck-private-{0}.ps1" -f ([guid]::NewGuid().ToString())) Set-Content -Path $tempFile -Value $content -Encoding UTF8 . $tempFile Remove-Item -Path $tempFile -Force -ErrorAction SilentlyContinue if (-not $script:PrivateConfig) { throw "Private Konfiguration wurde geladen, aber `$script:PrivateConfig ist nicht gesetzt." } return $script:PrivateConfig } catch { throw "❌ Fehler beim Laden der privaten Konfiguration: $($_.Exception.Message)" } } function Test-RequiredConfig { param( [Parameter(Mandatory = $true)] [hashtable]$Config ) $requiredKeys = @( 'InputFolder', 'LogFolder', 'WebhookUrl', 'SqlServer', 'SqlDatabase', 'SqlUser', 'SqlPassword', 'SqlQuery', 'SqlParameterName' ) foreach ($key in $requiredKeys) { if (-not $Config.ContainsKey($key) -or [string]::IsNullOrWhiteSpace([string]$Config[$key])) { throw "❌ Fehlende Konfiguration: '$key'" } } } function Get-SingleInputFile { param( [Parameter(Mandatory = $true)] [string]$InputFolder ) if (-not (Test-Path -Path $InputFolder)) { throw "❌ Input-Ordner existiert nicht: $InputFolder" } $files = Get-ChildItem -Path $InputFolder -File if ($files.Count -eq 0) { throw "❌ Keine Datei im Input-Ordner gefunden: $InputFolder" } if ($files.Count -gt 1) { throw "❌ Es wurden mehrere Dateien gefunden. Erwartet wird genau eine Datei im Ordner: $InputFolder" } return $files[0] } function Invoke-SqlTopOneQuery { param( [Parameter(Mandatory = $true)] [hashtable]$Config, [Parameter(Mandatory = $true)] [string]$Belegnummer ) $SqlPasswordEncode = [System.Text.Encoding]::UTF8.GetString( [System.Convert]::FromBase64String($Config.SqlPassword) ) Write-Log "Starte SQL-Abfrage mit Parameter $($Config.SqlParameterName) = '$Belegnummer'..." "INFO" $connectionString = "Server={0};Database={1};User ID={2};Password={3};" -f ` $Config.SqlServer, $Config.SqlDatabase, $Config.SqlUser, $SqlPasswordEncode $connection = New-Object System.Data.SqlClient.SqlConnection $connection.ConnectionString = $connectionString try { $connection.Open() $command = $connection.CreateCommand() $command.CommandText = $Config.SqlQuery $command.CommandTimeout = 60 [void]$command.Parameters.AddWithValue($Config.SqlParameterName, $Belegnummer) $adapter = New-Object System.Data.SqlClient.SqlDataAdapter $command $dataTable = New-Object System.Data.DataTable [void]$adapter.Fill($dataTable) if ($dataTable.Rows.Count -eq 0) { Write-Log "⚠ Kein SQL-Datensatz gefunden." "WARN" return $null } $row = $dataTable.Rows[0] $result = [ordered]@{} foreach ($column in $dataTable.Columns) { $value = $row[$column.ColumnName] if ($value -eq [System.DBNull]::Value) { $result[$column.ColumnName] = $null } else { $result[$column.ColumnName] = $value } } Write-Log "✓ SQL-Datensatz erfolgreich gelesen." "SUCCESS" return [pscustomobject]$result } catch { throw "❌ Fehler bei SQL-Abfrage: $($_.Exception.Message)" } finally { if ($connection.State -ne [System.Data.ConnectionState]::Closed) { $connection.Close() } $connection.Dispose() } } function Send-N8nWebhook { param( [Parameter(Mandatory = $true)] [string]$WebhookUrl, [Parameter(Mandatory = $true)] [hashtable]$Payload ) Write-Log "Sende Payload an n8n-Webhook..." "INFO" try { $json = $Payload | ConvertTo-Json -Depth 10 $null = Invoke-RestMethod -Uri $WebhookUrl -Method Post -ContentType 'application/json' -Body $json Write-Log "✓ Webhook erfolgreich gesendet." "SUCCESS" } catch { throw "❌ Fehler beim Senden des Webhooks: $($_.Exception.Message)" } } #endregion #region Hauptlogik try { Start-SelfUpdateIfNeeded -RemoteScriptUrl $script:PublicRawUrl Test-AzGithubToken $config = Get-PrivateConfig -RawConfigUrl $script:PrivateRawUrl Test-RequiredConfig -Config $config if (-not (Test-Path -Path $config.LogFolder)) { New-Item -Path $config.LogFolder -ItemType Directory -Force | Out-Null } $script:LogFilePath = Join-Path $config.LogFolder ("KANBAN-Rational-Druck_{0}.log" -f (Get-Date -Format 'yyyy-MM-dd')) Write-Log "Starte Skriptausführung KANBAN-Rational-Druck..." "INFO" $inputFile = Get-SingleInputFile -InputFolder $config.InputFolder $fileName = $inputFile.Name $fileBaseName = [System.IO.Path]::GetFileNameWithoutExtension($inputFile.Name) $fileFullPath = $inputFile.FullName $fileExtension = $inputFile.Extension Write-Log "Gefundene Datei: $fileName" "INFO" Write-Log "Extrahierte Belegnummer: $fileBaseName" "INFO" $sqlResult = $null $sqlFound = $false $status = 'success' $message = 'SQL-Datensatz gefunden' try { $sqlResult = Invoke-SqlTopOneQuery -Config $config -Belegnummer $fileBaseName if ($null -eq $sqlResult) { $sqlFound = $false $status = 'warning' $message = 'Kein SQL-Datensatz gefunden' } else { $sqlFound = $true } } catch { Write-Log $_.Exception.Message "ERROR" $sqlFound = $false $status = 'error' $message = $_.Exception.Message } $payload = [ordered]@{ status = $status message = $message fileName = $fileName fileBaseName = $fileBaseName fileFullPath = $fileFullPath fileExtension = $fileExtension processedAt = (Get-Date).ToString('s') sqlFound = $sqlFound sqlResult = $sqlResult } Send-N8nWebhook -WebhookUrl $config.WebhookUrl -Payload $payload Write-Log "Skriptausführung abgeschlossen." "SUCCESS" } catch { try { if (-not $script:LogFilePath) { $fallbackFolder = Join-Path $env:TEMP 'KANBAN-Rational-Druck' if (-not (Test-Path $fallbackFolder)) { New-Item -Path $fallbackFolder -ItemType Directory -Force | Out-Null } $script:LogFilePath = Join-Path $fallbackFolder ("KANBAN-Rational-Druck_{0}.log" -f (Get-Date -Format 'yyyy-MM-dd')) } Write-Log $_.Exception.Message "ERROR" } catch { Write-Host "❌ Kritischer Fehler: $($_.Exception.Message)" } exit 1 } #endregion