Import Excel DY

This commit is contained in:
2025-09-05 08:54:23 +02:00
parent ef62a1dc86
commit 167c250ee8
8 changed files with 1929 additions and 705 deletions

View File

@@ -44,11 +44,10 @@ Public Class cRelayHub
Public Class cRelayHubHeaderData
Public Property agentContact As cRelayHubAgentContact
Public Property declarantIsConsignee As Boolean
Public Property representationRelationshipCode As String
Public Property inputTaxDeduction As String
Public Property representationRelationshipCode As Integer ' <- war String
Public Property inputTaxDeduction As Boolean ' <- war String
Public Property procedureCodeRequested As String
Public Property goodsStatus As String
Public Property costs As List(Of cRelayHubCost)
' Entfernt: goodsStatus, costs (nicht im Schema an dieser Stelle)
Public Property transportMeansArrivalIdentity As String
Public Property transportMeansNationalityCode As String
Public Property previousAdministrativeReferenceType As String
@@ -57,7 +56,7 @@ Public Class cRelayHub
Public Property destinationCountry As String
Public Property departureCountry As String
Public Property addressedCustomsOffice As String
Public Property dv1CostAllocation As cRelayHubDv1CostAllocation ' <--- NEU
Public Property dv1CostAllocation As cRelayHubDv1CostAllocation
End Class
Public Class cRelayHubAddress
@@ -87,9 +86,12 @@ Public Class cRelayHub
Public Property dispatchCountry As String
Public Property destinationCountry As String
Public Property regimeType As String
Public Property customer As String
' Entfernt: customer (vom Schema abgelehnt)
Public Property documents As List(Of cRelayHubDocument)
Public Property additionalData As cRelayHubAdditionalData
' Neu: Pflichtfeld lt. Fehlermeldung
Public Property outputApplication As String ' "test" | "dakosy/sftp/vera" | "evrim/excel" | "sec/import/integration"
End Class
Public Class cRelayHubJobOrderResponse
@@ -117,9 +119,9 @@ Public Class cRelayHub
http.SetRequestHeader("Accept", "application/json")
' *** Token aus der separaten Token-Klasse beziehen ***
Dim token As String = cRelayHubToken.GetValidAccessToken()
Dim token As String = cRelayHubToken.GetAccessToken()
http.AuthToken = token ' -> setzt Authorization: Bearer <token>
Console.WriteLine("Using Token: " & token)
Return http.PostJson2(API_URL & "/job-orders/init", "application/json", jsonPayload)
End Function
@@ -140,29 +142,29 @@ Public Class cRelayHub
}
End If
' 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
'' 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
' ' 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
@@ -208,61 +210,58 @@ Public Class cRelayHub
' Beispielfall
Function CreateSampleJobOrderRequest() As cRelayHubJobOrderRequest
Dim request As New cRelayHubJobOrderRequest With {
.referenceNo = "1001K",
.dispatchCountry = "TR",
.destinationCountry = "EN",
.regimeType = "IM",
.customer = "AVISO",
.documents = New List(Of cRelayHubDocument) From {
New cRelayHubDocument With {.type = "base64", .base64String = "SGVsbG8sIHRoaXMgaXMgYSBzYW1wbGUgYmFzZTY0IHN0cmluZy4=", .filename = "test.txt", .fileType = "invoice"},
New cRelayHubDocument With {.type = "base64", .base64String = "SGVsbG8sIHRoaXMgaXMgYSBzYW1wbGUgYmFzZTY0IHN0cmluZy4=", .filename = "test2.txt", .fileType = "atr"},
New cRelayHubDocument With {.type = "base64", .base64String = "SGVsbG8sIHRoaXMgaXMgYSBzYW1wbGUgYmFzZTY0IHN0cmluZy4=", .filename = "test3.txt", .fileType = "cmr"}
},
.additionalData = New cRelayHubAdditionalData With {
.transaction = New cRelayHubTransaction With {
.ioPartner = "VERA",
.ioDivision3 = "SUB",
.ioReference = "4803/25001763_1301250935SS/samimx"
Dim req As New cRelayHubJobOrderRequest With {
.referenceNo = "1001K",
.dispatchCountry = "TR", ' ISO-2
.destinationCountry = "DE", ' ISO-2 (nicht "EN")
.regimeType = "IMPORT", ' "IMPORT" | "EXPORT"
.outputApplication = "test", ' Pflichtfeld lt. Validator
.documents = New List(Of cRelayHubDocument) From {
New cRelayHubDocument With {.type = "base64", .base64String = "SGVsbG8sIHRoaXMgaXMgYSBzYW1wbGUgYmFzZTY0IHN0cmluZy4=", .filename = "test.txt", .fileType = "invoice"},
New cRelayHubDocument With {.type = "base64", .base64String = "SGVsbG8sIHRoaXMgaXMgYSBzYW1wbGUgYmFzZTY0IHN0cmluZy4=", .filename = "test2.txt", .fileType = "atr"},
New cRelayHubDocument With {.type = "base64", .base64String = "SGVsbG8sIHRoaXMgaXMgYSBzYW1wbGUgYmFzZTY0IHN0cmluZy4=", .filename = "test3.txt", .fileType = "cmr"}
},
.additionalData = New cRelayHubAdditionalData With {
.transaction = New cRelayHubTransaction With {
.ioPartner = "VERA",
.ioDivision3 = "SUB",
.ioReference = "4803/25001763_1301250935SS/samimx"
},
.declaration = New List(Of cRelayHubDeclaration) From {
New cRelayHubDeclaration With {
.objectIdentification = New cRelayHubObjectIdentification With {
.objectName = "4803/25001763",
.objectAlias = "1349846",
.declarationType = "EZA-D"
},
.declaration = New List(Of cRelayHubDeclaration) From {
New cRelayHubDeclaration With {
.objectIdentification = New cRelayHubObjectIdentification With {
.objectName = "4803/25001763",
.objectAlias = "1349846",
.declarationType = "EZA-D"
},
.headerData = New cRelayHubHeaderData With {
.agentContact = New cRelayHubAgentContact With {
.contactPersonName = "AMANN",
.contactPersonPhoneNumber = "+49 123 456 789",
.contactPersonPosition = "Manager"
},
.declarantIsConsignee = True,
.representationRelationshipCode = "1",
.inputTaxDeduction = "true",
.procedureCodeRequested = "42",
.goodsStatus = "EU",
.costs = New List(Of cRelayHubCost) From {
New cRelayHubCost With {.costAmount = "25909.92", .costCurrency = "EUR"}
},
.transportMeansArrivalIdentity = "PB1552EC",
.transportMeansNationalityCode = "BG",
.previousAdministrativeReferenceType = "T1",
.previousAdministrativeReferenceNumber = "25TR160100001472M0",
.destinationFederalState = "07",
.destinationCountry = "DE",
.departureCountry = "TR"
},
.addresses = New List(Of cRelayHubAddress) From {
New cRelayHubAddress With {.addressType = "CZ", .participantEORI = "EORI12345", .participantSubsidiaryNumber = "001", .companyName = "SISECAM DIS TIC.A.S.", .streetAndNumber = "D-100 KARAYOLU CD.YAYLA MH.NO.70/C", .countryCode = "TR", .postalCode = "34949", .city = "TUZLA ISTANBUL"},
New cRelayHubAddress With {.addressType = "CN", .participantEORI = "EORI67890", .participantSubsidiaryNumber = "002", .companyName = "POLYNT COMPOSITES GERMANY GMBH", .streetAndNumber = "KIESELSTRASSE 2", .countryCode = "DE", .postalCode = "56357", .city = "MIEHLEN"}
}
}
.headerData = New cRelayHubHeaderData With {
.agentContact = New cRelayHubAgentContact With {
.contactPersonName = "AMANN",
.contactPersonPhoneNumber = "+49 123 456 789",
.contactPersonPosition = "Manager",
.contactPersonEmail = "a@example.com"
},
.declarantIsConsignee = True,
.representationRelationshipCode = 1, ' Integer
.inputTaxDeduction = True, ' Boolean
.procedureCodeRequested = "42", ' goodsStatus/costs entfernt
.transportMeansArrivalIdentity = "PB1552EC",
.transportMeansNationalityCode = "BG",
.previousAdministrativeReferenceType = "T1",
.previousAdministrativeReferenceNumber = "25TR160100001472M0",
.destinationFederalState = "07",
.destinationCountry = "DE",
.departureCountry = "TR"
},
.addresses = New List(Of cRelayHubAddress) From {
New cRelayHubAddress With {.addressType = "CZ", .participantEORI = "EORI12345", .participantSubsidiaryNumber = "001", .companyName = "SISECAM DIS TIC.A.S.", .streetAndNumber = "D-100 KARAYOLU CD.YAYLA MH.NO.70/C", .countryCode = "TR", .postalCode = "34949", .city = "TUZLA ISTANBUL"},
New cRelayHubAddress With {.addressType = "CN", .participantEORI = "EORI67890", .participantSubsidiaryNumber = "002", .companyName = "POLYNT COMPOSITES GERMANY GMBH", .streetAndNumber = "KIESELSTRASSE 2", .countryCode = "DE", .postalCode = "56357", .city = "MIEHLEN"}
}
}
}
Return request
}
}
Return req
End Function
End Class

View File

@@ -1,245 +1,199 @@
Imports Newtonsoft.Json
Imports System
Imports System.IO
Imports System.Net.Http
Imports System.Text
Imports System.Web
Imports Newtonsoft.Json
' NuGet: Newtonsoft.Json (>= 13.x)
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
' ======= KONFIG =======
Private Shared ReadOnly TOKEN_ENDPOINT As String =
"https://dev-kc.singlewindow.io/auth/realms/agsw/protocol/openid-connect/token"
' === 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)
Private Shared ReadOnly CLIENT_ID As String = "agsw-admin"
' === 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()
' Gewünscht: Zugangsdaten in der Klasse definieren
Private Shared ReadOnly USERNAME As String = "andreas.test@test.com"
Private Shared ReadOnly PASSWORD As String = "Password.123"
' -------------- 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)
' Token-File pro Benutzer unter %AppData%
Private Shared ReadOnly StorePath As String =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"RelayHub", "token.json")
' Sicherheitspuffer, bevor wir erneuern (Sekunden)
Private Const ExpirySkewSeconds As Integer = 60
' ======= ÖFFENTLICHE API =======
''' <summary>
''' Liefert einen gültigen Access Token (nie Leerstring).
''' </summary>
Public Shared Function GetAccessToken() As String
Dim store = LoadStore()
' 1) Wenn wir einen (noch) gültigen Token haben
If store IsNot Nothing AndAlso Not String.IsNullOrWhiteSpace(store.AccessToken) Then
If store.ExpiresAtUtc > DateTimeOffset.UtcNow.AddSeconds(ExpirySkewSeconds) Then
Return store.AccessToken
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")
' 2) Versuche Refresh, falls vorhanden
If store IsNot Nothing AndAlso Not String.IsNullOrWhiteSpace(store.RefreshToken) Then
store = TryRefresh(store.RefreshToken)
End If
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)
' 3) Fallback: Password-Grant Login
If store Is Nothing OrElse String.IsNullOrWhiteSpace(store.AccessToken) Then
store = PasswordLogin()
End If
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"))
' Validierung
If store Is Nothing OrElse String.IsNullOrWhiteSpace(store.AccessToken) Then
Throw New ApplicationException("Konnte keinen gültigen Access Token erhalten (leer).")
End If
Dim ts = New TokenState With {
.AccessToken = access,
.RefreshToken = refresh,
.AccessExpiryUtc = UtcNow().AddSeconds(exp),
.RefreshExpiryUtc = UtcNow().AddSeconds(rexp)
' Persistieren & zurück
SaveStore(store)
Return store.AccessToken
End Function
' ======= INTERNES =======
Private Shared Function PasswordLogin() As TokenStore
Dim form = New Dictionary(Of String, String) From {
{"grant_type", "password"},
{"username", USERNAME},
{"password", PASSWORD},
{"client_id", CLIENT_ID}
}
SaveTokenSecure(ts)
Return ts
Dim resp = PostForm(form)
Dim token = ParseTokenResponse(resp)
Return token
End Function
' -------------- Public API --------------
Public Shared Function GetValidAccessToken() As String
SyncLock _lockObj
If _ts Is Nothing Then _ts = LoadTokenSecure()
Private Shared Function TryRefresh(refreshToken As String) As TokenStore
Try
Dim form = New Dictionary(Of String, String) From {
{"grant_type", "refresh_token"},
{"refresh_token", refreshToken},
{"client_id", CLIENT_ID}
}
Dim resp = PostForm(form)
Dim token = ParseTokenResponse(resp)
If IsAccessValid(_ts) Then
Return _ts.AccessToken
' Nur speichern, wenn ein Access Token vorhanden ist
If Not String.IsNullOrWhiteSpace(token.AccessToken) Then
SaveStore(token)
Return token
End If
Catch ex As Exception
' Ignorieren -> fällt auf PasswordLogin zurück
End Try
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
Return Nothing
End Function
Public Shared Sub ResetTokenCache()
ClearToken()
Private Shared Function PostForm(formFields As Dictionary(Of String, String)) As String
Using client As New HttpClient()
Using content As New FormUrlEncodedContent(formFields)
Dim response = client.PostAsync(TOKEN_ENDPOINT, content).Result
Dim body = response.Content.ReadAsStringAsync().Result
If Not response.IsSuccessStatusCode Then
Throw New ApplicationException(
$"Token-Endpoint Fehler ({CInt(response.StatusCode)}): {body}")
End If
Return body
End Using
End Using
End Function
Private Shared Function ParseTokenResponse(json As String) As TokenStore
Dim r = JsonConvert.DeserializeObject(Of TokenResponse)(json)
If r Is Nothing OrElse String.IsNullOrWhiteSpace(r.access_token) Then
Throw New ApplicationException("Token-Antwort ungültig oder ohne access_token.")
End If
Dim now = DateTimeOffset.UtcNow
Dim expiresIn = If(r.expires_in <= 0, 3600, r.expires_in) ' Fallback 1h
Dim store = New TokenStore With {
.AccessToken = r.access_token.Trim(),
.RefreshToken = If(r.refresh_token, String.Empty),
.ExpiresAtUtc = now.AddSeconds(expiresIn)
}
' === Konsolen-Ausgabe ===
Console.WriteLine("== Neuer Token erhalten ==")
Console.WriteLine("Access Token: " & store.AccessToken)
Console.WriteLine("Refresh Token: " & store.RefreshToken)
Console.WriteLine("Gültig bis UTC: " & store.ExpiresAtUtc.ToString("yyyy-MM-dd HH:mm:ss"))
Return store
End Function
' ======= PERSISTENZ =======
Private Shared Function LoadStore() As TokenStore
Try
If File.Exists(StorePath) Then
Dim json = File.ReadAllText(StorePath, Encoding.UTF8)
Dim s = JsonConvert.DeserializeObject(Of TokenStore)(json)
' Ausgabe in Konsole
If s IsNot Nothing Then
Console.WriteLine("== Token aus Datei geladen ==")
Console.WriteLine("Access Token: " & (If(String.IsNullOrWhiteSpace(s.AccessToken), "<leer>", s.AccessToken)))
Console.WriteLine("Refresh Token: " & (If(String.IsNullOrWhiteSpace(s.RefreshToken), "<leer>", s.RefreshToken)))
Console.WriteLine("Gültig bis UTC: " & s.ExpiresAtUtc.ToString("yyyy-MM-dd HH:mm:ss"))
End If
Return s
Else
' Datei existiert nicht -> Info ausgeben
Console.WriteLine("Keine Token-Datei vorhanden, neuer Login erforderlich.")
End If
Catch ex As Exception
Console.WriteLine("Fehler beim Laden der Token-Datei: " & ex.Message)
' Datei defekt? -> Ignorieren, neu holen
End Try
Return Nothing
End Function
Private Shared Sub SaveStore(store As TokenStore)
Try
Dim dir = Path.GetDirectoryName(StorePath)
If Not Directory.Exists(dir) Then Directory.CreateDirectory(dir)
' Datei immer neu schreiben/überschreiben
Dim json = JsonConvert.SerializeObject(store, Formatting.Indented)
File.WriteAllText(StorePath, json, Encoding.UTF8)
Console.WriteLine("Token-Datei gespeichert: " & StorePath)
Catch ex As Exception
Console.WriteLine("Fehler beim Speichern der Token-Datei: " & ex.Message)
End Try
End Sub
End Class
' ======= DTOs =======
Private Class TokenResponse
Public Property access_token As String
Public Property expires_in As Integer
Public Property refresh_expires_in As Integer
Public Property refresh_token As String
Public Property token_type As String
Public Property scope As String
' … weitere Felder bei Bedarf
End Class
Private Class TokenStore
Public Property AccessToken As String
Public Property RefreshToken As String
Public Property ExpiresAtUtc As DateTimeOffset
End Class
End Class