Better profile caching.

This commit is contained in:
Burak Kaan Köse
2026-03-20 00:15:10 +01:00
parent d38317f0be
commit 4a20ea2577
10 changed files with 422 additions and 74 deletions
+105 -14
View File
@@ -25,6 +25,7 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
{
private readonly HttpClient _httpClient;
private readonly IDatabaseService _databaseService;
private readonly SemaphoreSlim _tokenRefreshLock = new(1, 1);
private readonly bool _ownsHttpClient;
// private const string ApiUrl = "https://localhost:7204/";
@@ -134,11 +135,12 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
{
try
{
using var request = await CreateAuthorizedRequestAsync(HttpMethod.Get, "api/v1/users/me/settings").ConfigureAwait(false);
if (request == null)
return null;
using var response = await SendAuthorizedAsync(
() => CreateAuthorizedRequestAsync(HttpMethod.Get, "api/v1/users/me/settings"),
cancellationToken).ConfigureAwait(false);
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (response == null)
return null;
if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
return null;
@@ -158,13 +160,16 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
{
try
{
using var request = await CreateAuthorizedRequestAsync(HttpMethod.Put, "api/v1/users/me/settings").ConfigureAwait(false);
if (request == null)
using var response = await SendAuthorizedAsync(
() => CreateAuthorizedRequestAsync(
HttpMethod.Put,
"api/v1/users/me/settings",
() => new StringContent(settingsJson, Encoding.UTF8, "application/json")),
cancellationToken).ConfigureAwait(false);
if (response == null)
return false;
request.Content = new StringContent(settingsJson, Encoding.UTF8, "application/json");
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
return response.IsSuccessStatusCode;
}
catch
@@ -325,11 +330,12 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
{
try
{
using var request = await CreateAuthorizedRequestAsync(method, endpoint).ConfigureAwait(false);
if (request == null)
return ApiEnvelope<TResponse>.Failure("MissingAccessToken");
using var response = await SendAuthorizedAsync(
() => CreateAuthorizedRequestAsync(method, endpoint),
cancellationToken).ConfigureAwait(false);
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (response == null)
return ApiEnvelope<TResponse>.Failure("MissingAccessToken");
var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var envelope = string.IsNullOrWhiteSpace(payload)
@@ -344,7 +350,7 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
}
}
private async Task<HttpRequestMessage?> CreateAuthorizedRequestAsync(HttpMethod method, string endpoint)
private async Task<HttpRequestMessage?> CreateAuthorizedRequestAsync(HttpMethod method, string endpoint, Func<HttpContent>? contentFactory = null)
{
var accessToken = await GetAccessTokenAsync().ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(accessToken))
@@ -352,15 +358,98 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
var request = new HttpRequestMessage(method, endpoint);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
request.Content = contentFactory?.Invoke();
return request;
}
private async Task<HttpResponseMessage?> SendAuthorizedAsync(Func<Task<HttpRequestMessage?>> requestFactory, CancellationToken cancellationToken)
{
using var initialRequest = await requestFactory().ConfigureAwait(false);
if (initialRequest == null)
{
return null;
}
var response = await _httpClient.SendAsync(initialRequest, cancellationToken).ConfigureAwait(false);
if (response.StatusCode != System.Net.HttpStatusCode.Unauthorized)
{
return response;
}
if (!await TryRefreshAccessTokenAsync(cancellationToken).ConfigureAwait(false))
{
return response;
}
response.Dispose();
using var retryRequest = await requestFactory().ConfigureAwait(false);
if (retryRequest == null)
{
return null;
}
return await _httpClient.SendAsync(retryRequest, cancellationToken).ConfigureAwait(false);
}
private async Task<string?> GetAccessTokenAsync()
{
var account = await _databaseService.Connection.Table<WinoAccount>().FirstOrDefaultAsync().ConfigureAwait(false);
return string.IsNullOrWhiteSpace(account?.AccessToken) ? null : account.AccessToken;
}
private async Task<bool> TryRefreshAccessTokenAsync(CancellationToken cancellationToken)
{
await _tokenRefreshLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
var account = await _databaseService.Connection.Table<WinoAccount>().FirstOrDefaultAsync().ConfigureAwait(false);
if (account == null || string.IsNullOrWhiteSpace(account.RefreshToken))
{
return false;
}
if (!string.IsNullOrWhiteSpace(account.AccessToken) && account.AccessTokenExpiresAtUtc > DateTime.UtcNow.AddMinutes(1))
{
return true;
}
var refreshResult = await RefreshAsync(account.RefreshToken, cancellationToken).ConfigureAwait(false);
if (!refreshResult.IsSuccess || refreshResult.Result == null)
{
return false;
}
var refreshedAccount = MapAccount(refreshResult.Result, account.LastAuthenticatedUtc);
await _databaseService.Connection.DeleteAllAsync<WinoAccount>().ConfigureAwait(false);
await _databaseService.Connection.InsertOrReplaceAsync(refreshedAccount, typeof(WinoAccount)).ConfigureAwait(false);
return true;
}
finally
{
_tokenRefreshLock.Release();
}
}
private static WinoAccount MapAccount(AuthResultDto result, DateTime lastAuthenticatedUtc)
=> new()
{
Id = result.User.UserId,
Email = result.User.Email,
AccountStatus = result.User.AccountStatus,
HasPassword = result.User.HasPassword,
HasGoogleLogin = result.User.HasGoogleLogin,
HasFacebookLogin = result.User.HasFacebookLogin,
AccessToken = result.AccessToken,
AccessTokenExpiresAtUtc = result.AccessTokenExpiresAtUtc.UtcDateTime,
RefreshToken = result.RefreshToken,
RefreshTokenExpiresAtUtc = result.RefreshTokenExpiresAtUtc.UtcDateTime,
LastAuthenticatedUtc = lastAuthenticatedUtc == default ? DateTime.UtcNow : lastAuthenticatedUtc
};
private static bool ValidateCertificate(HttpRequestMessage requestMessage, X509Certificate2? certificate, X509Chain? chain, System.Net.Security.SslPolicyErrors sslPolicyErrors)
{
if (requestMessage.RequestUri?.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase) == true)
@@ -377,6 +466,8 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
{
_httpClient.Dispose();
}
_tokenRefreshLock.Dispose();
}
}