Kopf-Sammelabrechnung

This commit is contained in:
2025-08-28 13:20:16 +02:00
parent 0100b3ed89
commit c126b4a064
11 changed files with 788 additions and 95 deletions

View File

@@ -21,7 +21,6 @@ Public Class cRelayHub
Public Property declarationType As String
Public Property referenceNumberOverlay As String ' <--- NEU
Public Property username As String ' <--- NEU
End Class
Public Class cRelayHubDv1CostAllocation
@@ -110,57 +109,68 @@ Public Class cRelayHub
Public Class cRelayHub_sendToRelayHub_JobOrderRequest
'Shared API_KEY = "2a6fe6bf-6547-4d56-b14a-8a18f94f9e94"
'Shared API_URL = "dev-relayhub.singlewindow.io/api"
Shared API_URL = "https://dev-relayhub.singlewindow.io/api/v1-0"
Shared API_URL As String = "https://dev-relayhub.singlewindow.io/api/v1-0"
' Low-level Sender: holt Access-Token aus cRelayHubToken und sendet JSON
Private Shared Function SendJobOrder(jsonPayload As String) As Chilkat.HttpResponse
Dim http As New Chilkat.Http
http.SetRequestHeader("Accept", "application/json")
' *** Token aus der separaten Token-Klasse beziehen ***
Dim token As String = cRelayHubToken.GetValidAccessToken()
http.AuthToken = token ' -> setzt Authorization: Bearer <token>
Return http.PostJson2(API_URL & "/job-orders/init", "application/json", jsonPayload)
End Function
' Public API: erstellt Job-Order mit 401-Retry
Public Shared Function query_declarations(request As cRelayHubJobOrderRequest) As cRelayHubApiResult
Dim result As New cRelayHubApiResult()
Try
' This example assumes the Chilkat API to have been previously unlocked.
' See Global Unlock Sample for sample code.
VERAG_PROG_ALLGEMEIN.cChilkat_Helper.UnlockCilkat()
Dim success As Boolean
' HTTP-Client initialisieren
Dim http As New Chilkat.Http
' JSON vorbereiten
' Request-Objekt in JSON-String umwandeln
Dim jsonPayload As String = JsonConvert.SerializeObject(request)
' MsgBox(jsonPayload)
' Größe in Bytes
Dim payloadSizeBytes As Integer = System.Text.Encoding.UTF8.GetByteCount(jsonPayload)
' Größe in Kilobytes (1 KB = 1024 Bytes)
Dim payloadSizeKb As Double = payloadSizeBytes / 1024.0
Console.WriteLine("📦 Größe des JSON-Payload:")
Console.WriteLine(payloadSizeBytes & " Bytes (" & Math.Round(payloadSizeKb, 2) & " KB)")
' Anfrage senden
Dim response As Chilkat.HttpResponse = http.PostJson2(API_URL & "/job-orders/init", "application/json", jsonPayload)
MsgBox(jsonPayload)
Console.WriteLine(jsonPayload)
If http.LastMethodSuccess <> True Then
result.Success = False
result.StatusCode = 0
result.Message = "Verbindungsfehler"
result.Details = http.LastErrorText
Return result
' 1. Versuch
Dim response As Chilkat.HttpResponse = SendJobOrder(jsonPayload)
If response Is Nothing Then
Return New cRelayHubApiResult With {
.Success = False, .StatusCode = 0, .Message = "Verbindungsfehler",
.Details = "Keine Antwort erhalten."
}
End If
result.StatusCode = response.StatusCode
' 401 → Token-Cache invalidieren und genau 1x erneut probieren
If response.StatusCode = 401 Then
' WICHTIG:
' Diese Methode sollte in cRelayHubToken als Public verfügbar sein:
' Public Shared Sub ResetTokenCache() : ClearToken() : End Sub
' → Falls noch nicht vorhanden, bitte dort ergänzen.
Try
cRelayHubToken.ResetTokenCache()
Catch
' Falls die Methode (noch) nicht existiert, kann man als Fallback
' hier eine kurze Wartezeit einbauen und anschließend erneut GetValidAccessToken() aufrufen.
' Threading.Thread.Sleep(100)
End Try
' Retry
response = SendJobOrder(jsonPayload)
If response Is Nothing Then
Return New cRelayHubApiResult With {
.Success = False, .StatusCode = 0, .Message = "Verbindungsfehler (nach Refresh)",
.Details = "Keine Antwort erhalten."
}
End If
End If
' Auswertung
result.StatusCode = response.StatusCode
Select Case response.StatusCode
Case 201
Try
Dim jobResponse As cRelayHubJobOrderResponse = JsonConvert.DeserializeObject(Of cRelayHubJobOrderResponse)(response.BodyStr)
Dim jobResponse As cRelayHubJobOrderResponse =
JsonConvert.DeserializeObject(Of cRelayHubJobOrderResponse)(response.BodyStr)
result.Success = True
result.Message = "Job Order erfolgreich erstellt"
result.Data = jobResponse
@@ -175,18 +185,12 @@ Public Class cRelayHub
Case 400 To 499
result.Success = False
result.Message = "Client-Fehler"
result.Message = "StatusCode: " & response.StatusCode
result.Details = "StatusLine: " & response.StatusLine
result.Details = "StatusText: " & response.StatusText
result.Details = "BodyStr: " & response.BodyStr
result.Details = response.BodyStr
Case 500 To 599
result.Success = False
result.Message = "Server-Fehler"
result.Message = "StatusCode: " & response.StatusCode
result.Details = "StatusLine: " & response.StatusLine
result.Details = "StatusText: " & response.StatusText
result.Details = "BodyStr: " & response.BodyStr
result.Details = response.BodyStr
Case Else
result.Success = False
@@ -194,19 +198,15 @@ Public Class cRelayHub
result.Details = response.BodyStr
End Select
Console.WriteLine(result.Message)
Console.WriteLine(result.Details)
Return result
Catch ex As Exception
VERAG_PROG_ALLGEMEIN.cErrorHandler.ERR(ex.Message, ex.StackTrace, System.Reflection.MethodInfo.GetCurrentMethod.Name)
Return New cRelayHubApiResult With {.Success = False, .StatusCode = 0, .Message = "Exception", .Details = ex.ToString()}
End Try
Return Nothing
End Function
' Beispielfall
Function CreateSampleJobOrderRequest() As cRelayHubJobOrderRequest
Dim request As New cRelayHubJobOrderRequest With {
.referenceNo = "1001K",
@@ -262,9 +262,8 @@ Public Class cRelayHub
}
}
}
Return request
End Function
End Class
End Class
End Class

View File

@@ -0,0 +1,245 @@
Imports Newtonsoft.Json
Imports System.IO
Imports System.Text
Public Class cRelayHubToken
' === Token-Datenmodell ===
Private Class TokenState
Public AccessToken As String
Public RefreshToken As String
Public AccessExpiryUtc As DateTime
Public RefreshExpiryUtc As DateTime
End Class
' === Keycloak-Config ===
Private Shared ReadOnly KC_BASE As String = "https://dev-kc.singlewindow.io"
Private Shared ReadOnly KC_TOKEN_PATH As String = "/auth/realms/agsw/protocol/openid-connect/token"
Private Shared ReadOnly KC_CLIENT_ID As String = "agsw-admin"
Private Shared ReadOnly KC_USERNAME As String = "andreas.test@test.com"
Private Shared ReadOnly KC_PASSWORD As String = "Password.123"
Private Shared ReadOnly SKEW As TimeSpan = TimeSpan.FromSeconds(30)
' === Cache/Persistenz ===
Private Shared _ts As TokenState = Nothing
Private Shared ReadOnly TOKEN_FILE As String = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"RelayHub", "token.cache"
)
Private Shared ReadOnly _lockObj As New Object()
' -------------- DPAPI via Reflection (keine Compile-Abhängigkeit!) --------------
Private Shared Function TryProtect(plain As Byte()) As Byte()
Try
' Versuche: Typen aus Assembly "System.Security" oder aus aktuellen Laufzeit-Assemblys laden
Dim dpType As Type = Type.GetType("System.Security.Cryptography.ProtectedData, System.Security", throwOnError:=False)
If dpType Is Nothing Then
dpType = Type.GetType("System.Security.Cryptography.ProtectedData", throwOnError:=False)
End If
Dim scopeType As Type = Type.GetType("System.Security.Cryptography.DataProtectionScope, System.Security", throwOnError:=False)
If dpType Is Nothing OrElse scopeType Is Nothing Then Return Nothing
Dim scopeObj As Object = [Enum].Parse(scopeType, "CurrentUser")
Dim mi = dpType.GetMethod("Protect", New Type() {GetType(Byte()), GetType(Byte()), scopeType})
If mi Is Nothing Then Return Nothing
Dim res = mi.Invoke(Nothing, New Object() {plain, Nothing, scopeObj})
Return TryCast(res, Byte())
Catch
Return Nothing
End Try
End Function
Private Shared Function TryUnprotect(protectedBytes As Byte()) As Byte()
Try
Dim dpType As Type = Type.GetType("System.Security.Cryptography.ProtectedData, System.Security", throwOnError:=False)
If dpType Is Nothing Then
dpType = Type.GetType("System.Security.Cryptography.ProtectedData", throwOnError:=False)
End If
Dim scopeType As Type = Type.GetType("System.Security.Cryptography.DataProtectionScope, System.Security", throwOnError:=False)
If dpType Is Nothing OrElse scopeType Is Nothing Then Return Nothing
Dim scopeObj As Object = [Enum].Parse(scopeType, "CurrentUser")
Dim mi = dpType.GetMethod("Unprotect", New Type() {GetType(Byte()), GetType(Byte()), scopeType})
If mi Is Nothing Then Return Nothing
Dim res = mi.Invoke(Nothing, New Object() {protectedBytes, Nothing, scopeObj})
Return TryCast(res, Byte())
Catch
Return Nothing
End Try
End Function
' -------------- Persistenz: bevorzugt DPAPI, Fallback Plain-File --------------
Private Shared Sub SaveTokenSecure(ts As TokenState)
Try
Dim dir = Path.GetDirectoryName(TOKEN_FILE)
If Not Directory.Exists(dir) Then Directory.CreateDirectory(dir)
Dim payload As String = String.Join(vbLf, {
ts.AccessToken,
ts.RefreshToken,
ts.AccessExpiryUtc.Ticks.ToString(),
ts.RefreshExpiryUtc.Ticks.ToString()
})
Dim plain = Encoding.UTF8.GetBytes(payload)
Dim protectedBytes = TryProtect(plain)
If protectedBytes IsNot Nothing Then
File.WriteAllBytes(TOKEN_FILE, protectedBytes)
Else
' Fallback (nur zu Testzwecken!)
File.WriteAllText(TOKEN_FILE, payload, Encoding.UTF8)
End If
Catch
' optional loggen
End Try
End Sub
Private Shared Function LoadTokenSecure() As TokenState
Try
If Not File.Exists(TOKEN_FILE) Then Return Nothing
' Zuerst versuchen wir, als DPAPI-Bytes zu lesen und zu entschlüsseln
Dim raw = File.ReadAllBytes(TOKEN_FILE)
Dim plain = TryUnprotect(raw)
Dim content As String
If plain Is Nothing Then
' Fallback: als Text lesen (falls zuvor ohne DPAPI gespeichert)
content = File.ReadAllText(TOKEN_FILE, Encoding.UTF8)
Else
content = Encoding.UTF8.GetString(plain)
End If
Dim s = content.Split({vbLf}, StringSplitOptions.None)
If s.Length < 4 Then Return Nothing
Return New TokenState With {
.AccessToken = s(0),
.RefreshToken = s(1),
.AccessExpiryUtc = New DateTime(Long.Parse(s(2)), DateTimeKind.Utc),
.RefreshExpiryUtc = New DateTime(Long.Parse(s(3)), DateTimeKind.Utc)
}
Catch
Return Nothing
End Try
End Function
Private Shared Sub ClearToken()
SyncLock _lockObj
_ts = Nothing
Try
If File.Exists(TOKEN_FILE) Then File.Delete(TOKEN_FILE)
Catch
End Try
End SyncLock
End Sub
' -------------- Utilities --------------
Private Shared Function UtcNow() As DateTime
Return DateTime.UtcNow
End Function
Private Shared Function IsAccessValid(ts As TokenState) As Boolean
Return ts IsNot Nothing AndAlso Not String.IsNullOrEmpty(ts.AccessToken) AndAlso UtcNow() < ts.AccessExpiryUtc - SKEW
End Function
Private Shared Function IsRefreshValid(ts As TokenState) As Boolean
Return ts IsNot Nothing AndAlso Not String.IsNullOrEmpty(ts.RefreshToken) AndAlso UtcNow() < ts.RefreshExpiryUtc - SKEW
End Function
' -------------- OAuth Flows --------------
Private Shared Function PasswordLogin() As TokenState
Dim http As New Chilkat.Http
Dim req As New Chilkat.HttpRequest
req.HttpVerb = "POST"
req.Path = KC_TOKEN_PATH
req.AddParam("grant_type", "password")
req.AddParam("username", KC_USERNAME)
req.AddParam("password", KC_PASSWORD)
req.AddParam("client_id", KC_CLIENT_ID)
req.AddParam("scope", "openid offline_access")
req.AddHeader("Content-Type", "application/x-www-form-urlencoded")
Dim resp = http.PostUrlEncoded(KC_BASE, req)
If resp Is Nothing Then Throw New Exception("Token-Request fehlgeschlagen: " & http.LastErrorText)
If resp.StatusCode <> 200 Then Throw New Exception("Password-Grant fehlgeschlagen: " & resp.StatusCode & " - " & resp.BodyStr)
Dim json As New Chilkat.JsonObject : json.Load(resp.BodyStr)
Dim access = json.StringOf("access_token")
Dim refresh = json.StringOf("refresh_token")
Dim exp = Math.Max(60, json.IntOf("expires_in"))
Dim rexp = Math.Max(300, json.IntOf("refresh_expires_in"))
Dim ts = New TokenState With {
.AccessToken = access,
.RefreshToken = refresh,
.AccessExpiryUtc = UtcNow().AddSeconds(exp),
.RefreshExpiryUtc = UtcNow().AddSeconds(rexp)
}
SaveTokenSecure(ts)
Return ts
End Function
Private Shared Function RefreshLogin(oldTs As TokenState) As TokenState
If oldTs Is Nothing OrElse String.IsNullOrEmpty(oldTs.RefreshToken) Then
Throw New Exception("Kein gültiger Refresh-Token vorhanden.")
End If
Dim http As New Chilkat.Http
Dim req As New Chilkat.HttpRequest
req.HttpVerb = "POST"
req.Path = KC_TOKEN_PATH
req.AddParam("grant_type", "refresh_token")
req.AddParam("refresh_token", oldTs.RefreshToken)
req.AddParam("client_id", KC_CLIENT_ID)
req.AddHeader("Content-Type", "application/x-www-form-urlencoded")
Dim resp = http.PostUrlEncoded(KC_BASE, req)
If resp Is Nothing Then Throw New Exception("Refresh-Request fehlgeschlagen: " & http.LastErrorText)
If resp.StatusCode <> 200 Then Throw New Exception("Refresh fehlgeschlagen: " & resp.StatusCode & " - " & resp.BodyStr)
Dim json As New Chilkat.JsonObject : json.Load(resp.BodyStr)
Dim access = json.StringOf("access_token")
Dim refresh = json.StringOf("refresh_token") ' Rotation beachten
Dim exp = Math.Max(60, json.IntOf("expires_in"))
Dim rexp = Math.Max(300, json.IntOf("refresh_expires_in"))
Dim ts = New TokenState With {
.AccessToken = access,
.RefreshToken = refresh,
.AccessExpiryUtc = UtcNow().AddSeconds(exp),
.RefreshExpiryUtc = UtcNow().AddSeconds(rexp)
}
SaveTokenSecure(ts)
Return ts
End Function
' -------------- Public API --------------
Public Shared Function GetValidAccessToken() As String
SyncLock _lockObj
If _ts Is Nothing Then _ts = LoadTokenSecure()
If IsAccessValid(_ts) Then
Return _ts.AccessToken
End If
If IsRefreshValid(_ts) Then
Try
_ts = RefreshLogin(_ts)
Return _ts.AccessToken
Catch
' fällt durch auf PasswordLogin
End Try
End If
_ts = PasswordLogin()
Return _ts.AccessToken
End SyncLock
End Function
Public Shared Sub ResetTokenCache()
ClearToken()
End Sub
End Class