-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a28ab44
commit 443794c
Showing
24 changed files
with
414 additions
and
382 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
using System.Text.Json.Serialization; | ||
|
||
namespace BTCPayApp.Core; | ||
|
||
public class BTCPayAccount(string baseUri, string email) | ||
{ | ||
public string BaseUri { get; set; } = baseUri; | ||
public string Email { get; set; } = email; | ||
public string? AccessToken { get; set; } | ||
public string? RefreshToken { get; set; } | ||
public DateTimeOffset? AccessExpiry { get; set; } | ||
|
||
[JsonConstructor] | ||
public BTCPayAccount() : this(string.Empty, string.Empty) {} | ||
|
||
public void SetAccess(string accessToken, string refreshToken, long expiresInSeconds, DateTimeOffset? expiryOffset = null) | ||
{ | ||
AccessToken = accessToken; | ||
RefreshToken = refreshToken; | ||
AccessExpiry = (expiryOffset ?? DateTimeOffset.Now) + TimeSpan.FromSeconds(expiresInSeconds); | ||
} | ||
|
||
public void ClearAccess() | ||
{ | ||
AccessToken = RefreshToken = null; | ||
AccessExpiry = null; | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
using System.Net; | ||
using System.Net.Http.Headers; | ||
using System.Net.Http.Json; | ||
using BTCPayApp.CommonServer; | ||
using Microsoft.AspNetCore.Authentication.BearerToken; | ||
using Microsoft.AspNetCore.Identity.Data; | ||
using Microsoft.AspNetCore.Mvc; | ||
|
||
namespace BTCPayApp.Core; | ||
|
||
public class BTCPayAppClient(IHttpClientFactory clientFactory) | ||
{ | ||
private readonly HttpClient _httpClient = clientFactory.CreateClient(); | ||
private readonly string[] _unauthenticatedPaths = ["login", "forgot-password", "reset-password"]; | ||
private DateTimeOffset? AccessExpiry { get; set; } // TODO: Incorporate in refresh check | ||
private string? AccessToken { get; set; } | ||
private string? RefreshToken { get; set; } | ||
|
||
public async Task<TResponse> Get<TResponse>(string baseUrl, string path, CancellationToken cancellation = default, bool isRetry = false) | ||
{ | ||
return await Send<EmptyRequestModel, TResponse>(HttpMethod.Get, baseUrl, path, null, cancellation, isRetry); | ||
} | ||
|
||
public async Task Post<TRequest>(string baseUrl, string path, TRequest payload, CancellationToken cancellation = default, bool isRetry = false) | ||
{ | ||
await Send<TRequest, EmptyResponseModel>(HttpMethod.Post, baseUrl, path, payload, cancellation, isRetry); | ||
} | ||
|
||
public async Task<TResponse> Post<TRequest, TResponse>(string baseUrl, string path, TRequest payload, CancellationToken cancellation = default, bool isRetry = false) | ||
{ | ||
return await Send<TRequest, TResponse>(HttpMethod.Post, baseUrl, path, payload, cancellation, isRetry); | ||
} | ||
|
||
private async Task<TResponse> Send<TRequest, TResponse>(HttpMethod method, string baseUrl, string path, TRequest? payload, CancellationToken cancellation, bool isRetry = false) | ||
{ | ||
var req = new HttpRequestMessage | ||
{ | ||
RequestUri = new Uri(WithTrailingSlash(baseUrl) + $"btcpayapp/{path}"), | ||
Method = method, | ||
Content = payload == null ? null : JsonContent.Create(payload) | ||
}; | ||
req.Headers.Accept.Clear(); | ||
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); | ||
req.Headers.Add("User-Agent", "BTCPayServerAppApiClient"); | ||
|
||
if (!_unauthenticatedPaths.Contains(path)) | ||
{ | ||
if (string.IsNullOrEmpty(AccessToken)) | ||
throw new BTCPayAppClientException(401, "Authentication required"); | ||
|
||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken); | ||
} | ||
|
||
var res = await _httpClient.SendAsync(req, cancellation); | ||
if (!res.IsSuccessStatusCode) | ||
{ | ||
if (res.StatusCode == HttpStatusCode.Unauthorized) | ||
{ | ||
// try refresh and recurse if the token is expired | ||
if (!string.IsNullOrEmpty(RefreshToken) && !isRetry) | ||
{ | ||
var (refresh, _) = await Refresh(baseUrl, RefreshToken, cancellation); | ||
if (refresh != null) return await Send<TRequest, TResponse>(method, baseUrl, path, payload, cancellation); | ||
} | ||
|
||
ClearAccess(); | ||
} | ||
// otherwise handle the error response | ||
var problem = await res.Content.ReadFromJsonAsync<ProblemDetails>(cancellationToken: cancellation); | ||
var statusCode = problem?.Status ?? (int)res.StatusCode; | ||
var message = problem?.Detail ?? res.ReasonPhrase; | ||
throw new BTCPayAppClientException(statusCode, message ?? "Request failed"); | ||
} | ||
|
||
if (typeof(TResponse) == typeof(EmptyResponseModel)) | ||
{ | ||
return (TResponse)(object)new EmptyResponseModel(); | ||
} | ||
|
||
var response = await res.Content.ReadFromJsonAsync<TResponse>(cancellationToken: cancellation); | ||
return response != null ? response : (TResponse)(object)new EmptyResponseModel(); | ||
} | ||
|
||
private AccessTokenResult HandleAccessTokenResponse(AccessTokenResponse response, DateTimeOffset expiryOffset) | ||
{ | ||
var expiry = expiryOffset + TimeSpan.FromSeconds(response.ExpiresIn); | ||
SetAccess(response.AccessToken, response.RefreshToken, expiry); | ||
return new AccessTokenResult(response.AccessToken, response.RefreshToken, expiry); | ||
} | ||
|
||
private async Task<(AccessTokenResult? success, string? errorCode)> Refresh(string serverUrl, string refreshToken, CancellationToken? cancellation = default) | ||
{ | ||
var payload = new RefreshRequest { RefreshToken = refreshToken }; | ||
var now = DateTimeOffset.Now; | ||
try | ||
{ | ||
var response = await Post<RefreshRequest, AccessTokenResponse>(serverUrl, "refresh", payload, cancellation.GetValueOrDefault(), true); | ||
var res = HandleAccessTokenResponse(response, now); | ||
return (res, null); | ||
} | ||
catch (BTCPayAppClientException e) | ||
{ | ||
return (null, e.Message); | ||
} | ||
} | ||
|
||
public void ClearAccess() | ||
{ | ||
AccessToken = RefreshToken = null; | ||
AccessExpiry = null; | ||
} | ||
|
||
public void SetAccess(string accessToken, string refreshToken, DateTimeOffset expiry) | ||
{ | ||
AccessToken = accessToken; | ||
RefreshToken = refreshToken; | ||
AccessExpiry = expiry; | ||
} | ||
|
||
private static string WithTrailingSlash(string str) => str.EndsWith('/') ? str : str + "/"; | ||
|
||
private class EmptyRequestModel; | ||
private class EmptyResponseModel; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
namespace BTCPayApp.Core; | ||
|
||
public class BTCPayAppClientException(int statusCode, string message) : Exception | ||
{ | ||
public int StatusCode { get; init; } = statusCode; | ||
public override string Message => message; | ||
} |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.