Forgot password and email confirmations.

This commit is contained in:
Burak Kaan Köse
2026-03-19 16:41:35 +01:00
parent 873a7eca12
commit c2320de5c4
20 changed files with 643 additions and 44 deletions
+80 -2
View File
@@ -27,6 +27,9 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
private readonly IDatabaseService _databaseService;
private readonly bool _ownsHttpClient;
private const string ApiUrl = "https://localhost:7204/";
// private const string ApiUrl = "https://api.winomail.app/";
public WinoAccountApiClient(IDatabaseService databaseService, HttpClient? httpClient = null)
{
_databaseService = databaseService;
@@ -44,8 +47,9 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
_httpClient = new HttpClient(handler)
{
BaseAddress = new Uri("https://api.winomail.app/")
BaseAddress = new Uri(ApiUrl)
};
_ownsHttpClient = true;
}
@@ -58,6 +62,24 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
public Task<WinoAccountApiResult<AuthResultDto>> RefreshAsync(string refreshToken, CancellationToken cancellationToken = default)
=> SendAuthRequestAsync("api/v1/auth/refresh", new RefreshRequest(refreshToken), WinoAccountApiJsonContext.Default.RefreshRequest, cancellationToken);
public Task<ApiEnvelope<EmailConfirmationResendResultDto>> ResendEmailConfirmationAsync(string endpoint, string ticket, CancellationToken cancellationToken = default)
=> SendAnonymousRequestAsync(
HttpMethod.Post,
endpoint,
new ResendEmailConfirmationRequest(ticket),
WinoAccountApiJsonContext.Default.ResendEmailConfirmationRequest,
WinoAccountApiJsonContext.Default.ApiEnvelopeEmailConfirmationResendResultDto,
cancellationToken);
public Task<ApiEnvelope<JsonElement>> ForgotPasswordAsync(string email, CancellationToken cancellationToken = default)
=> SendAnonymousRequestAsync(
HttpMethod.Post,
"api/v1/auth/forgot-password",
new ForgotPasswordRequest(email),
WinoAccountApiJsonContext.Default.ForgotPasswordRequest,
WinoAccountApiJsonContext.Default.ApiEnvelopeJsonElement,
cancellationToken);
public async Task<ApiEnvelope<JsonElement>> LogoutAsync(string refreshToken, CancellationToken cancellationToken = default)
{
try
@@ -173,8 +195,9 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
var errorCode = envelope?.ErrorCode ?? $"HTTP {(int)response.StatusCode} {response.ReasonPhrase}".Trim();
var errorMessage = ExtractErrorMessage(payload) ?? response.ReasonPhrase;
var errorDetails = ExtractDetails(payload);
return WinoAccountApiResult<AuthResultDto>.Failure(errorCode, errorMessage);
return WinoAccountApiResult<AuthResultDto>.Failure(errorCode, errorMessage, errorDetails);
}
catch (Exception ex)
{
@@ -200,6 +223,29 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
}
}
private static JsonElement? ExtractDetails(string? payload)
{
if (string.IsNullOrWhiteSpace(payload))
{
return null;
}
try
{
using var document = JsonDocument.Parse(payload);
if (document.RootElement.ValueKind != JsonValueKind.Object || !document.RootElement.TryGetProperty("details", out var details))
{
return null;
}
return details.Clone();
}
catch (JsonException)
{
return null;
}
}
private static string? TryGetErrorMessage(JsonElement element)
{
if (TryGetStringProperty(element, "errorMessage", out var errorMessage))
@@ -246,6 +292,35 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
private Task<ApiEnvelope<TResponse>> SendAuthorizedRequestAsync<TResponse>(string endpoint, JsonTypeInfo<ApiEnvelope<TResponse>> typeInfo, CancellationToken cancellationToken)
=> SendAuthorizedRequestAsync(HttpMethod.Get, endpoint, typeInfo, cancellationToken);
private async Task<ApiEnvelope<TResponse>> SendAnonymousRequestAsync<TRequest, TResponse>(HttpMethod method,
string endpoint,
TRequest requestBody,
JsonTypeInfo<TRequest> requestTypeInfo,
JsonTypeInfo<ApiEnvelope<TResponse>> responseTypeInfo,
CancellationToken cancellationToken)
{
try
{
using var request = new HttpRequestMessage(method, endpoint)
{
Content = JsonContent.Create(requestBody, requestTypeInfo)
};
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var envelope = string.IsNullOrWhiteSpace(payload)
? null
: JsonSerializer.Deserialize(payload, responseTypeInfo);
return envelope ?? ApiEnvelope<TResponse>.Failure($"HTTP {(int)response.StatusCode} {response.ReasonPhrase}".Trim());
}
catch (Exception ex)
{
return ApiEnvelope<TResponse>.Failure(ex.Message);
}
}
private async Task<ApiEnvelope<TResponse>> SendAuthorizedRequestAsync<TResponse>(HttpMethod method, string endpoint, JsonTypeInfo<ApiEnvelope<TResponse>> typeInfo, CancellationToken cancellationToken)
{
try
@@ -310,7 +385,10 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
[JsonSerializable(typeof(LoginRequest))]
[JsonSerializable(typeof(RefreshRequest))]
[JsonSerializable(typeof(LogoutRequest))]
[JsonSerializable(typeof(ResendEmailConfirmationRequest))]
[JsonSerializable(typeof(ForgotPasswordRequest))]
[JsonSerializable(typeof(ApiEnvelope<AuthResultDto>))]
[JsonSerializable(typeof(ApiEnvelope<EmailConfirmationResendResultDto>))]
[JsonSerializable(typeof(ApiEnvelope<AuthUserDto>))]
[JsonSerializable(typeof(ApiEnvelope<AiStatusResultDto>))]
[JsonSerializable(typeof(ApiEnvelope<CheckoutSessionResultDto>))]