Refactor envelope receiver auth flow and state handling
- Introduce IReceiverAuthService and ReceiverAuthService for all envelope receiver authentication and status API calls - Add ReceiverAuthModel as a client-side DTO for API responses - Refactor EnvelopeState to store all relevant fields and update via ApplyApiResponse - Overhaul EnvelopePage.razor to use new service and state, with improved status handling and UI - Enhance ApiResponse and ApiServiceBase to support structured error deserialization - Register IReceiverAuthService in DI container
This commit is contained in:
@@ -19,6 +19,14 @@ public record ApiResponse<T>
|
||||
|
||||
public static ApiResponse<T> Failure(int statusCode, string? error = null)
|
||||
=> new() { IsSuccess = false, StatusCode = statusCode, ErrorMessage = error };
|
||||
|
||||
/// <summary>
|
||||
/// Failure mit deserialisiertem Body — für Fälle wo die API
|
||||
/// bei 401/404 trotzdem ein strukturiertes JSON zurückgibt
|
||||
/// (z.B. ReceiverAuthResponse mit ErrorMessage + Status).
|
||||
/// </summary>
|
||||
public static ApiResponse<T> Failure(int statusCode, string? error, T? data)
|
||||
=> new() { IsSuccess = false, StatusCode = statusCode, ErrorMessage = error, Data = data };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -37,7 +37,13 @@ public abstract class ApiServiceBase
|
||||
var errorBody = await response.Content.ReadAsStringAsync(ct);
|
||||
Logger.LogWarning("GET {Endpoint} failed: {Status} - {Body}",
|
||||
endpoint, (int)response.StatusCode, errorBody);
|
||||
return ApiResponse<T>.Failure((int)response.StatusCode, errorBody);
|
||||
|
||||
// Versuche den Body trotzdem zu deserialisieren —
|
||||
// die API gibt bei 401/404 oft strukturierte JSON-Antworten zurück
|
||||
// (z.B. ReceiverAuthResponse mit ErrorMessage + Status)
|
||||
var errorData = await TryDeserializeAsync<T>(response, ct);
|
||||
|
||||
return ApiResponse<T>.Failure((int)response.StatusCode, errorBody, errorData);
|
||||
}
|
||||
|
||||
var data = await response.Content.ReadFromJsonAsync<T>(cancellationToken: ct);
|
||||
@@ -70,7 +76,10 @@ public abstract class ApiServiceBase
|
||||
var errorBody = await response.Content.ReadAsStringAsync(ct);
|
||||
Logger.LogWarning("POST {Endpoint} failed: {Status} - {Body}",
|
||||
endpoint, (int)response.StatusCode, errorBody);
|
||||
return ApiResponse<TResponse>.Failure((int)response.StatusCode, errorBody);
|
||||
|
||||
var errorData = await TryDeserializeAsync<TResponse>(response, ct);
|
||||
|
||||
return ApiResponse<TResponse>.Failure((int)response.StatusCode, errorBody, errorData);
|
||||
}
|
||||
|
||||
var data = await response.Content.ReadFromJsonAsync<TResponse>(cancellationToken: ct);
|
||||
@@ -107,4 +116,26 @@ public abstract class ApiServiceBase
|
||||
return ApiResponse.Failure(0, "Verbindung zum Server fehlgeschlagen.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Versucht den Response-Body als JSON zu deserialisieren.
|
||||
/// Gibt null zurück wenn es nicht klappt (z.B. bei HTML-Fehlerseiten).
|
||||
/// </summary>
|
||||
private static async Task<T?> TryDeserializeAsync<T>(HttpResponseMessage response, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Nur versuchen wenn der Content-Type JSON ist
|
||||
if (response.Content.Headers.ContentType?.MediaType == "application/json")
|
||||
{
|
||||
return await response.Content.ReadFromJsonAsync<T>(cancellationToken: ct);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignorieren — der Body war kein valides JSON
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using EnvelopeGenerator.ReceiverUI.Client.Models;
|
||||
using EnvelopeGenerator.ReceiverUI.Client.Services.Base;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Kommuniziert mit dem ReceiverAuthController der API.
|
||||
///
|
||||
/// Drei Methoden — eine pro Endpunkt:
|
||||
/// 1. GetStatusAsync → GET /api/receiverauth/{key}/status
|
||||
/// 2. SubmitAccessCodeAsync → POST /api/receiverauth/{key}/access-code
|
||||
/// 3. SubmitTfaCodeAsync → POST /api/receiverauth/{key}/tfa
|
||||
/// </summary>
|
||||
public interface IReceiverAuthService
|
||||
{
|
||||
/// <summary>Prüft den aktuellen Status des Empfänger-Flows</summary>
|
||||
Task<ApiResponse<ReceiverAuthModel>> GetStatusAsync(string key, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Sendet den Zugangscode zur Prüfung</summary>
|
||||
Task<ApiResponse<ReceiverAuthModel>> SubmitAccessCodeAsync(
|
||||
string key, string accessCode, bool preferSms, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Sendet den TFA-Code (SMS oder Authenticator) zur Prüfung</summary>
|
||||
Task<ApiResponse<ReceiverAuthModel>> SubmitTfaCodeAsync(
|
||||
string key, string code, string type, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using EnvelopeGenerator.ReceiverUI.Client.Models;
|
||||
using EnvelopeGenerator.ReceiverUI.Client.Services.Base;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Spricht mit dem ReceiverAuthController der API.
|
||||
///
|
||||
/// Nutzt die Basisklasse ApiServiceBase für einheitliches Error-Handling.
|
||||
/// Jede Methode gibt ApiResponse<ReceiverAuthModel> zurück —
|
||||
/// egal ob Erfolg oder Fehler. Die aufrufende Komponente prüft dann
|
||||
/// result.IsSuccess und result.Data.Status.
|
||||
///
|
||||
/// WARUM gibt die API bei 401 trotzdem ein ReceiverAuthModel zurück?
|
||||
/// Weil auch bei "falscher Code" der Client wissen muss, welchen
|
||||
/// Status er anzeigen soll (z.B. "requires_access_code" + ErrorMessage).
|
||||
/// Deshalb deserialisieren wir auch bei Fehler-Statuscodes den Body.
|
||||
/// </summary>
|
||||
public class ReceiverAuthService : ApiServiceBase, IReceiverAuthService
|
||||
{
|
||||
public ReceiverAuthService(HttpClient http, ILogger<ReceiverAuthService> logger)
|
||||
: base(http, logger) { }
|
||||
|
||||
public Task<ApiResponse<ReceiverAuthModel>> GetStatusAsync(
|
||||
string key, CancellationToken ct = default)
|
||||
=> GetAsync<ReceiverAuthModel>($"api/receiverauth/{key}/status", ct);
|
||||
|
||||
public Task<ApiResponse<ReceiverAuthModel>> SubmitAccessCodeAsync(
|
||||
string key, string accessCode, bool preferSms, CancellationToken ct = default)
|
||||
=> PostAsync<object, ReceiverAuthModel>(
|
||||
$"api/receiverauth/{key}/access-code",
|
||||
new { AccessCode = accessCode, PreferSms = preferSms },
|
||||
ct);
|
||||
|
||||
public Task<ApiResponse<ReceiverAuthModel>> SubmitTfaCodeAsync(
|
||||
string key, string code, string type, CancellationToken ct = default)
|
||||
=> PostAsync<object, ReceiverAuthModel>(
|
||||
$"api/receiverauth/{key}/tfa",
|
||||
new { Code = code, Type = type },
|
||||
ct);
|
||||
}
|
||||
Reference in New Issue
Block a user