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:
@@ -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})."
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user