Sign in , out ,register.

This commit is contained in:
Burak Kaan Köse
2026-03-16 01:33:27 +01:00
parent 921c3bef93
commit 37c1bd3f62
35 changed files with 1195 additions and 30 deletions
+3 -2
View File
@@ -67,8 +67,8 @@ public class DatabaseService : IDatabaseService
Connection.CreateTableAsync<CalendarItem>(),
Connection.CreateTableAsync<CalendarAttachment>(),
Connection.CreateTableAsync<Reminder>(),
Connection.CreateTableAsync<MailInvitationCalendarMapping>()
);
Connection.CreateTableAsync<MailInvitationCalendarMapping>(),
Connection.CreateTableAsync<WinoAccount>());
await EnsureSchemaUpgradesAsync().ConfigureAwait(false);
await EnsureIndexesAsync().ConfigureAwait(false);
@@ -206,6 +206,7 @@ SET {nameof(KeyboardShortcut.Action)} =
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailAccountAlias_AccountId_AliasAddress ON MailAccountAlias(AccountId, AliasAddress)").ConfigureAwait(false);
await Connection.ExecuteAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_MailAccountPreferences_AccountId ON MailAccountPreferences(AccountId)").ConfigureAwait(false);
await Connection.ExecuteAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_CustomServerInformation_AccountId ON CustomServerInformation(AccountId)").ConfigureAwait(false);
await Connection.ExecuteAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_WinoAccount_Email ON WinoAccount(Email)").ConfigureAwait(false);
// Calendar indexes
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_AccountCalendar_AccountId ON AccountCalendar(AccountId)").ConfigureAwait(false);
+2
View File
@@ -27,6 +27,8 @@ public static class ServicesContainerSetup
services.AddTransient<IContextMenuItemService, ContextMenuItemService>();
services.AddTransient<ISpecialImapProviderConfigResolver, SpecialImapProviderConfigResolver>();
services.AddTransient<IKeyboardShortcutService, KeyboardShortcutService>();
services.AddSingleton<IWinoAccountApiClient, WinoAccountApiClient>();
services.AddTransient<IWinoAccountProfileService, WinoAccountProfileService>();
services.AddSingleton<IContactPictureFileService, ContactPictureFileService>();
services.AddTransient<ICalDavClient, CalDavClient>();
+2 -1
View File
@@ -25,6 +25,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
<ProjectReference Include="..\Wino.Mail.Contracts\Wino.Mail.Contracts.csproj" />
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
</ItemGroup>
</Project>
</Project>
+123
View File
@@ -0,0 +1,123 @@
#nullable enable
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Threading.Tasks;
using Wino.Core.Domain.Interfaces;
using Wino.Mail.Api.Contracts.Auth;
using Wino.Mail.Api.Contracts.Common;
namespace Wino.Services;
public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
{
private readonly HttpClient _httpClient;
private readonly bool _ownsHttpClient;
public WinoAccountApiClient(HttpClient? httpClient = null)
{
if (httpClient != null)
{
_httpClient = httpClient;
return;
}
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = ValidateCertificate
};
_httpClient = new HttpClient(handler)
{
BaseAddress = new Uri("https://localhost:7204/")
};
_ownsHttpClient = true;
}
public Task<ApiEnvelope<AuthResultDto>> RegisterAsync(string email, string password, CancellationToken cancellationToken = default)
=> SendAuthRequestAsync("api/v1/auth/register", new RegisterRequest(email, password), WinoAccountApiJsonContext.Default.RegisterRequest, cancellationToken);
public Task<ApiEnvelope<AuthResultDto>> LoginAsync(string email, string password, CancellationToken cancellationToken = default)
=> SendAuthRequestAsync("api/v1/auth/login", new LoginRequest(email, password), WinoAccountApiJsonContext.Default.LoginRequest, cancellationToken);
public Task<ApiEnvelope<AuthResultDto>> RefreshAsync(string refreshToken, CancellationToken cancellationToken = default)
=> SendAuthRequestAsync("api/v1/auth/refresh", new RefreshRequest(refreshToken), WinoAccountApiJsonContext.Default.RefreshRequest, cancellationToken);
public async Task<ApiEnvelope<JsonElement>> LogoutAsync(string refreshToken, CancellationToken cancellationToken = default)
{
try
{
using var response = await _httpClient.PostAsJsonAsync(
"api/v1/auth/logout",
new LogoutRequest(refreshToken),
WinoAccountApiJsonContext.Default.LogoutRequest,
cancellationToken).ConfigureAwait(false);
var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var envelope = string.IsNullOrWhiteSpace(payload)
? null
: JsonSerializer.Deserialize(payload, WinoAccountApiJsonContext.Default.ApiEnvelopeJsonElement);
return envelope ?? ApiEnvelope<JsonElement>.Failure($"HTTP {(int)response.StatusCode} {response.ReasonPhrase}".Trim());
}
catch (Exception ex)
{
return ApiEnvelope<JsonElement>.Failure(ex.Message);
}
}
private async Task<ApiEnvelope<AuthResultDto>> SendAuthRequestAsync<TRequest>(string endpoint, TRequest request, JsonTypeInfo<TRequest> typeInfo, CancellationToken cancellationToken)
{
try
{
using var response = await _httpClient.PostAsJsonAsync(
endpoint,
request,
typeInfo,
cancellationToken).ConfigureAwait(false);
var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var envelope = string.IsNullOrWhiteSpace(payload)
? null
: JsonSerializer.Deserialize(payload, WinoAccountApiJsonContext.Default.ApiEnvelopeAuthResultDto);
return envelope ?? ApiEnvelope<AuthResultDto>.Failure($"HTTP {(int)response.StatusCode} {response.ReasonPhrase}".Trim());
}
catch (Exception ex)
{
return ApiEnvelope<AuthResultDto>.Failure(ex.Message);
}
}
private static bool ValidateCertificate(HttpRequestMessage requestMessage, X509Certificate2? certificate, X509Chain? chain, System.Net.Security.SslPolicyErrors sslPolicyErrors)
{
if (requestMessage.RequestUri?.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase) == true)
{
return true;
}
return sslPolicyErrors == System.Net.Security.SslPolicyErrors.None;
}
public void Dispose()
{
if (_ownsHttpClient)
{
_httpClient.Dispose();
}
}
}
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
[JsonSerializable(typeof(RegisterRequest))]
[JsonSerializable(typeof(LoginRequest))]
[JsonSerializable(typeof(RefreshRequest))]
[JsonSerializable(typeof(LogoutRequest))]
[JsonSerializable(typeof(ApiEnvelope<AuthResultDto>))]
[JsonSerializable(typeof(ApiEnvelope<JsonElement>))]
internal sealed partial class WinoAccountApiJsonContext : JsonSerializerContext;
+130
View File
@@ -0,0 +1,130 @@
#nullable enable
using System;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Mail.Api.Contracts.Auth;
using Wino.Mail.Api.Contracts.Common;
using Wino.Messaging.UI;
namespace Wino.Services;
public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccountProfileService
{
private readonly IWinoAccountApiClient _apiClient;
private readonly ILogger _logger = Log.ForContext<WinoAccountProfileService>();
public WinoAccountProfileService(IDatabaseService databaseService, IWinoAccountApiClient apiClient) : base(databaseService)
{
_apiClient = apiClient;
}
public async Task<WinoAccountOperationResult> RegisterAsync(string email, string password, CancellationToken cancellationToken = default)
{
var response = await _apiClient.RegisterAsync(email, password, cancellationToken).ConfigureAwait(false);
var result = await PersistResponseAsync(response).ConfigureAwait(false);
if (result.IsSuccess && result.Account != null)
{
ReportUIChange(new WinoAccountSignedInMessage(result.Account));
}
return result;
}
public async Task<WinoAccountOperationResult> LoginAsync(string email, string password, CancellationToken cancellationToken = default)
{
var response = await _apiClient.LoginAsync(email, password, cancellationToken).ConfigureAwait(false);
var result = await PersistResponseAsync(response).ConfigureAwait(false);
if (result.IsSuccess && result.Account != null)
{
ReportUIChange(new WinoAccountSignedInMessage(result.Account));
}
return result;
}
public async Task<WinoAccountOperationResult> RefreshAsync(CancellationToken cancellationToken = default)
{
var account = await GetActiveAccountAsync().ConfigureAwait(false);
if (account == null || string.IsNullOrWhiteSpace(account.RefreshToken))
{
return WinoAccountOperationResult.Failure(ApiErrorCodes.RefreshTokenInvalid);
}
var response = await _apiClient.RefreshAsync(account.RefreshToken, cancellationToken).ConfigureAwait(false);
return await PersistResponseAsync(response).ConfigureAwait(false);
}
public async Task<WinoAccount?> GetActiveAccountAsync()
{
var account = await Connection.Table<WinoAccount>().FirstOrDefaultAsync().ConfigureAwait(false);
return account;
}
public async Task<bool> HasActiveAccountAsync()
=> await Connection.Table<WinoAccount>().CountAsync().ConfigureAwait(false) > 0;
public async Task SignOutAsync(CancellationToken cancellationToken = default)
{
var account = await GetActiveAccountAsync().ConfigureAwait(false);
if (account != null && !string.IsNullOrWhiteSpace(account.RefreshToken))
{
try
{
var result = await _apiClient.LogoutAsync(account.RefreshToken, cancellationToken).ConfigureAwait(false);
if (!result.IsSuccess && !string.IsNullOrWhiteSpace(result.ErrorCode))
{
_logger.Warning("Wino account remote sign-out failed with error code {ErrorCode}", result.ErrorCode);
}
}
catch (Exception ex)
{
_logger.Warning(ex, "Wino account remote sign-out failed.");
}
}
await Connection.DeleteAllAsync<WinoAccount>().ConfigureAwait(false);
if (account != null)
{
ReportUIChange(new WinoAccountSignedOutMessage(account));
}
}
private async Task<WinoAccountOperationResult> PersistResponseAsync(ApiEnvelope<AuthResultDto> response)
{
if (!response.IsSuccess || response.Result == null)
{
return WinoAccountOperationResult.Failure(response.ErrorCode);
}
var account = Map(response.Result);
await Connection.DeleteAllAsync<WinoAccount>().ConfigureAwait(false);
await Connection.InsertOrReplaceAsync(account, typeof(WinoAccount)).ConfigureAwait(false);
return WinoAccountOperationResult.Success(account);
}
private static WinoAccount Map(AuthResultDto result)
=> 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 = DateTime.UtcNow
};
}