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})." }; } } }