From 75bda545a8e8bf602b32c22a45c5361f65bd0c91 Mon Sep 17 00:00:00 2001 From: TekH Date: Wed, 13 May 2026 22:43:00 +0200 Subject: [PATCH] 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. --- .../Api/ReceiverApiClient.cs | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 receiverUI/EnvelopeGenerator.ReceiverUI.Web/EnvelopeGenerator.ReceiverUI.Web.Client/Api/ReceiverApiClient.cs diff --git a/receiverUI/EnvelopeGenerator.ReceiverUI.Web/EnvelopeGenerator.ReceiverUI.Web.Client/Api/ReceiverApiClient.cs b/receiverUI/EnvelopeGenerator.ReceiverUI.Web/EnvelopeGenerator.ReceiverUI.Web.Client/Api/ReceiverApiClient.cs new file mode 100644 index 00000000..9cbddc59 --- /dev/null +++ b/receiverUI/EnvelopeGenerator.ReceiverUI.Web/EnvelopeGenerator.ReceiverUI.Web.Client/Api/ReceiverApiClient.cs @@ -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; + +/// +/// 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. +/// +public class ReceiverApiClient +{ + private readonly HttpClient _http; + private readonly ILogger _logger; + + public ReceiverApiClient(HttpClient http, ILogger logger) + { + _http = http; + _logger = logger; + } + + // ?? Receiver Auth ???????????????????????????????????????????????? + + public Task GetStatusAsync(string envelopeKey, CancellationToken ct = default) + => GetAuthAsync($"api/receiverauth/{Uri.EscapeDataString(envelopeKey)}/status", ct); + + public Task SubmitAccessCodeAsync(string envelopeKey, AccessCodeRequest req, CancellationToken ct = default) + => PostAuthAsync($"api/receiverauth/{Uri.EscapeDataString(envelopeKey)}/access-code", req, ct); + + public Task SubmitTfaCodeAsync(string envelopeKey, TfaCodeRequest req, CancellationToken ct = default) + => PostAuthAsync($"api/receiverauth/{Uri.EscapeDataString(envelopeKey)}/tfa", req, ct); + + // ?? Envelope Receiver ???????????????????????????????????????????? + + public async Task 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(cancellationToken: ct); + } + + /// Downloads the document bytes for the receiver to display in a PDF viewer. + public async Task 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 ??????????????????????????????????? + + /// + /// Returns the signature placeholders the authenticated receiver must sign. + /// + public async Task> GetSignatureElementsAsync(CancellationToken ct = default) + { + try + { + return await _http.GetFromJsonAsync>("api/annotation/elements", ct) + ?? new List(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to fetch signature elements."); + return new List(); + } + } + + /// + /// Submits the signed envelope using the Blazor-friendly endpoint. + /// + public async Task SignBlazorAsync(BlazorSignaturePayload payload, CancellationToken ct = default) + { + var res = await _http.PostAsJsonAsync("api/annotation/blazor", payload, ct); + return res.StatusCode; + } + + /// + /// 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. + /// + 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(cancellationToken: ct); + return (data, res.StatusCode); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to fetch TFA registration for {Key}", envelopeReceiverId); + return (null, HttpStatusCode.InternalServerError); + } + } + + /// Submits the signed annotations payload. Returns the HTTP status code. + public async Task SignAsync(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 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 ShareReadOnlyAsync(ReadOnlyShareRequest req, CancellationToken ct = default) + { + var res = await _http.PostAsJsonAsync("api/readonly", req, ct); + return res.IsSuccessStatusCode; + } + + // ?? Auth (logout) ???????????????????????????????????????????????? + + public async Task LogoutAsync(CancellationToken ct = default) + { + var res = await _http.PostAsync("auth/logout", content: null, ct); + return res.IsSuccessStatusCode; + } + + // ?? Localization ????????????????????????????????????????????????? + + public async Task?> GetLocalizationStringsAsync(CancellationToken ct = default) + { + try + { + return await _http.GetFromJsonAsync>("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); + } + } + + /// + /// Returns the currently selected language code (e.g. "de", "en"), or + /// null if no language cookie has been set yet (the API answers + /// with HTTP 404 in that case). + /// + public async Task 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 GetAuthAsync(string url, CancellationToken ct) + { + var res = await _http.GetAsync(url, ct); + return await ReadAuthAsync(res, ct); + } + + private async Task PostAuthAsync(string url, TReq body, CancellationToken ct) + { + var res = await _http.PostAsJsonAsync(url, body, ct); + return await ReadAuthAsync(res, ct); + } + + private static async Task 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(cancellationToken: ct); + } + catch + { + return new ReceiverAuthResponse + { + Status = ReceiverAuthStatus.Error, + ErrorMessage = $"Unexpected response ({(int)res.StatusCode})." + }; + } + } +}