Add ReceiverApiClient for typed receiver API access

Introduced ReceiverApiClient.cs, a typed HTTP client for the EnvelopeGenerator receiver API. It provides strongly-typed methods for authentication, envelope and document retrieval, annotation/signature actions, read-only sharing, logout, and localization. The client uses dependency injection, handles error logging, and ensures authentication cookies are attached via same-origin requests.
This commit is contained in:
2026-05-13 22:43:00 +02:00
parent 0b9abc8fc1
commit 75bda545a8

View File

@@ -0,0 +1,214 @@
using System.Net;
using System.Net.Http.Json;
using EnvelopeGenerator.ReceiverUI.Web.Client.Api.Models;
namespace EnvelopeGenerator.ReceiverUI.Web.Client.Api;
/// <summary>
/// Typed HTTP client for the EnvelopeGenerator receiver API.
/// All endpoints are routed through the BFF (same origin), so the
/// authentication cookie set by the API is automatically attached
/// by the browser to every request issued by the injected HttpClient.
/// </summary>
public class ReceiverApiClient
{
private readonly HttpClient _http;
private readonly ILogger<ReceiverApiClient> _logger;
public ReceiverApiClient(HttpClient http, ILogger<ReceiverApiClient> logger)
{
_http = http;
_logger = logger;
}
// ?? Receiver Auth ????????????????????????????????????????????????
public Task<ReceiverAuthResponse?> GetStatusAsync(string envelopeKey, CancellationToken ct = default)
=> GetAuthAsync($"api/receiverauth/{Uri.EscapeDataString(envelopeKey)}/status", ct);
public Task<ReceiverAuthResponse?> SubmitAccessCodeAsync(string envelopeKey, AccessCodeRequest req, CancellationToken ct = default)
=> PostAuthAsync($"api/receiverauth/{Uri.EscapeDataString(envelopeKey)}/access-code", req, ct);
public Task<ReceiverAuthResponse?> SubmitTfaCodeAsync(string envelopeKey, TfaCodeRequest req, CancellationToken ct = default)
=> PostAuthAsync($"api/receiverauth/{Uri.EscapeDataString(envelopeKey)}/tfa", req, ct);
// ?? Envelope Receiver ????????????????????????????????????????????
public async Task<EnvelopeReceiverDto?> GetEnvelopeReceiverAsync(string envelopeKey, CancellationToken ct = default)
{
var res = await _http.GetAsync($"api/envelopereceiver/{Uri.EscapeDataString(envelopeKey)}", ct);
if (!res.IsSuccessStatusCode)
return null;
return await res.Content.ReadFromJsonAsync<EnvelopeReceiverDto>(cancellationToken: ct);
}
/// <summary>Downloads the document bytes for the receiver to display in a PDF viewer.</summary>
public async Task<byte[]?> GetDocumentAsync(string envelopeKey, CancellationToken ct = default)
{
var res = await _http.GetAsync($"api/document/{Uri.EscapeDataString(envelopeKey)}", ct);
if (!res.IsSuccessStatusCode)
return null;
return await res.Content.ReadAsByteArrayAsync(ct);
}
// ?? Annotation / Sign / Reject ???????????????????????????????????
/// <summary>
/// Returns the signature placeholders the authenticated receiver must sign.
/// </summary>
public async Task<List<SignatureElementDto>> GetSignatureElementsAsync(CancellationToken ct = default)
{
try
{
return await _http.GetFromJsonAsync<List<SignatureElementDto>>("api/annotation/elements", ct)
?? new List<SignatureElementDto>();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to fetch signature elements.");
return new List<SignatureElementDto>();
}
}
/// <summary>
/// Submits the signed envelope using the Blazor-friendly endpoint.
/// </summary>
public async Task<HttpStatusCode> SignBlazorAsync(BlazorSignaturePayload payload, CancellationToken ct = default)
{
var res = await _http.PostAsJsonAsync("api/annotation/blazor", payload, ct);
return res.StatusCode;
}
/// <summary>
/// Fetches the TOTP QR code + registration deadline for the given
/// envelope-receiver key (encoded uuid+signature). The API generates
/// a fresh secret on first call and persists it server-side.
/// </summary>
public async Task<(TfaRegistrationResponse? Data, HttpStatusCode Status)> GetTfaRegistrationAsync(string envelopeReceiverId, CancellationToken ct = default)
{
try
{
var res = await _http.GetAsync($"api/tfa/{Uri.EscapeDataString(envelopeReceiverId)}", ct);
if (!res.IsSuccessStatusCode)
return (null, res.StatusCode);
var data = await res.Content.ReadFromJsonAsync<TfaRegistrationResponse>(cancellationToken: ct);
return (data, res.StatusCode);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to fetch TFA registration for {Key}", envelopeReceiverId);
return (null, HttpStatusCode.InternalServerError);
}
}
/// <summary>Submits the signed annotations payload. Returns the HTTP status code.</summary>
public async Task<HttpStatusCode> SignAsync<TPayload>(TPayload? payload, CancellationToken ct = default)
{
var res = payload is null
? await _http.PostAsync("api/annotation", content: null, ct)
: await _http.PostAsJsonAsync("api/annotation", payload, ct);
return res.StatusCode;
}
public async Task<bool> RejectAsync(string reason, CancellationToken ct = default)
{
var res = await _http.PostAsJsonAsync("api/annotation/reject", reason, ct);
return res.IsSuccessStatusCode;
}
// ?? Read-only share ??????????????????????????????????????????????
public async Task<bool> ShareReadOnlyAsync(ReadOnlyShareRequest req, CancellationToken ct = default)
{
var res = await _http.PostAsJsonAsync("api/readonly", req, ct);
return res.IsSuccessStatusCode;
}
// ?? Auth (logout) ????????????????????????????????????????????????
public async Task<bool> LogoutAsync(CancellationToken ct = default)
{
var res = await _http.PostAsync("auth/logout", content: null, ct);
return res.IsSuccessStatusCode;
}
// ?? Localization ?????????????????????????????????????????????????
public async Task<Dictionary<string, string>?> GetLocalizationStringsAsync(CancellationToken ct = default)
{
try
{
return await _http.GetFromJsonAsync<Dictionary<string, string>>("api/Localization", ct);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to fetch localization strings.");
return null;
}
}
public async Task SetLanguageAsync(string language, CancellationToken ct = default)
{
try
{
await _http.PostAsync($"api/Localization/lang?language={Uri.EscapeDataString(language)}", content: null, ct);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to set language to {Lang}.", language);
}
}
/// <summary>
/// Returns the currently selected language code (e.g. "de", "en"), or
/// <c>null</c> if no language cookie has been set yet (the API answers
/// with HTTP 404 in that case).
/// </summary>
public async Task<string?> GetLanguageAsync(CancellationToken ct = default)
{
try
{
var res = await _http.GetAsync("api/Localization/lang", ct);
if (!res.IsSuccessStatusCode)
return null;
return (await res.Content.ReadAsStringAsync(ct))?.Trim('"');
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to read current language.");
return null;
}
}
// ?? Helpers ??????????????????????????????????????????????????????
private async Task<ReceiverAuthResponse?> GetAuthAsync(string url, CancellationToken ct)
{
var res = await _http.GetAsync(url, ct);
return await ReadAuthAsync(res, ct);
}
private async Task<ReceiverAuthResponse?> PostAuthAsync<TReq>(string url, TReq body, CancellationToken ct)
{
var res = await _http.PostAsJsonAsync(url, body, ct);
return await ReadAuthAsync(res, ct);
}
private static async Task<ReceiverAuthResponse?> ReadAuthAsync(HttpResponseMessage res, CancellationToken ct)
{
// ReceiverAuthController returns a ReceiverAuthResponse body for both
// 2xx and known error statuses (401/404/500). We always try to deserialize.
try
{
return await res.Content.ReadFromJsonAsync<ReceiverAuthResponse>(cancellationToken: ct);
}
catch
{
return new ReceiverAuthResponse
{
Status = ReceiverAuthStatus.Error,
ErrorMessage = $"Unexpected response ({(int)res.StatusCode})."
};
}
}
}